react-native-image-stitcher 0.15.2 → 0.16.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 +171 -1
- package/README.md +131 -5
- package/android/src/main/cpp/image_stitcher_jni.cpp +154 -13
- package/android/src/main/cpp/keyframe_gate_jni.cpp +15 -8
- package/android/src/main/java/io/imagestitcher/rn/BatchStitcher.kt +223 -1
- package/android/src/main/java/io/imagestitcher/rn/IncrementalStitcher.kt +220 -87
- package/android/src/main/java/io/imagestitcher/rn/KeyframeGate.kt +1 -1
- package/android/src/main/java/io/imagestitcher/rn/RNSARCameraView.kt +7 -36
- package/android/src/main/java/io/imagestitcher/rn/RNSARSession.kt +14 -8
- package/android/src/main/java/io/imagestitcher/rn/ar/YuvImageConverter.kt +39 -1
- package/cpp/crop_quad.cpp +162 -0
- package/cpp/crop_quad.hpp +163 -0
- package/cpp/keyframe_gate.cpp +54 -15
- package/cpp/keyframe_gate.hpp +33 -0
- package/cpp/stitcher.cpp +1122 -132
- package/cpp/stitcher.hpp +62 -0
- package/cpp/warp_guard.hpp +212 -0
- package/dist/camera/Camera.d.ts +209 -12
- package/dist/camera/Camera.js +575 -36
- package/dist/camera/CameraView.js +35 -16
- package/dist/camera/CaptureCountdownOverlay.d.ts +70 -0
- package/dist/camera/CaptureCountdownOverlay.js +239 -0
- package/dist/camera/CaptureFrameCounterOverlay.d.ts +58 -0
- package/dist/camera/CaptureFrameCounterOverlay.js +153 -0
- package/dist/camera/CaptureMemoryPill.d.ts +24 -8
- package/dist/camera/CaptureMemoryPill.js +37 -12
- package/dist/camera/CapturePreview.js +2 -1
- package/dist/camera/CaptureStatusOverlay.d.ts +11 -4
- package/dist/camera/CaptureStatusOverlay.js +22 -5
- package/dist/camera/CaptureThumbnailStrip.js +2 -1
- package/dist/camera/LateralMotionModal.d.ts +85 -0
- package/dist/camera/LateralMotionModal.js +134 -0
- package/dist/camera/PanHowToOverlay.d.ts +76 -0
- package/dist/camera/PanHowToOverlay.js +222 -0
- package/dist/camera/PanoramaBandOverlay.d.ts +2 -1
- package/dist/camera/PanoramaBandOverlay.js +9 -3
- package/dist/camera/PanoramaSettings.d.ts +8 -6
- package/dist/camera/PanoramaSettings.js +19 -1
- package/dist/camera/PanoramaSettingsModal.js +4 -4
- package/dist/camera/RectCropPreview.d.ts +135 -0
- package/dist/camera/RectCropPreview.js +370 -0
- package/dist/camera/RotateToLandscapePrompt.d.ts +87 -0
- package/dist/camera/RotateToLandscapePrompt.js +138 -0
- package/dist/camera/buildPanoramaInitialSettings.d.ts +19 -2
- package/dist/camera/buildPanoramaInitialSettings.js +9 -0
- package/dist/camera/cameraErrorMessages.d.ts +30 -1
- package/dist/camera/cameraErrorMessages.js +26 -10
- package/dist/camera/cameraGuidanceCopy.d.ts +87 -0
- package/dist/camera/cameraGuidanceCopy.js +80 -0
- package/dist/camera/captureCountdown.d.ts +52 -0
- package/dist/camera/captureCountdown.js +76 -0
- package/dist/camera/captureWarnings.d.ts +90 -0
- package/dist/camera/captureWarnings.js +108 -0
- package/dist/camera/classifyStitchError.d.ts +30 -0
- package/dist/camera/classifyStitchError.js +42 -0
- package/dist/camera/cropGeometry.d.ts +136 -0
- package/dist/camera/cropGeometry.js +223 -0
- package/dist/camera/displayDecodeImageProps.d.ts +25 -0
- package/dist/camera/displayDecodeImageProps.js +29 -0
- package/dist/camera/guidanceGraphics.d.ts +58 -0
- package/dist/camera/guidanceGraphics.js +280 -0
- package/dist/camera/guidanceTokens.d.ts +54 -0
- package/dist/camera/guidanceTokens.js +58 -0
- package/dist/camera/panModeGate.d.ts +54 -0
- package/dist/camera/panModeGate.js +62 -0
- package/dist/camera/pickCaptureFormat.d.ts +71 -0
- package/dist/camera/pickCaptureFormat.js +85 -0
- package/dist/camera/stitchDebugInfo.d.ts +27 -0
- package/dist/camera/stitchDebugInfo.js +55 -0
- package/dist/camera/usePanMotion.d.ts +250 -0
- package/dist/camera/usePanMotion.js +451 -0
- package/dist/index.d.ts +24 -3
- package/dist/index.js +33 -2
- package/dist/stitching/computeInscribedRect.d.ts +40 -0
- package/dist/stitching/computeInscribedRect.js +55 -0
- package/dist/stitching/cropQuad.d.ts +78 -0
- package/dist/stitching/cropQuad.js +116 -0
- package/dist/stitching/incremental.d.ts +74 -0
- package/dist/stitching/useIncrementalStitcher.d.ts +7 -1
- package/dist/stitching/useIncrementalStitcher.js +7 -1
- package/ios/Sources/RNImageStitcher/IncrementalStitcher.swift +154 -29
- package/ios/Sources/RNImageStitcher/IncrementalStitcherBridge.m +4 -0
- package/ios/Sources/RNImageStitcher/IncrementalStitcherBridge.swift +15 -0
- package/ios/Sources/RNImageStitcher/KeyframeGate.swift +2 -2
- package/ios/Sources/RNImageStitcher/OpenCVKeyframeCollector.mm +48 -5
- package/ios/Sources/RNImageStitcher/OpenCVStitcher.h +27 -0
- package/ios/Sources/RNImageStitcher/OpenCVStitcher.mm +211 -7
- package/ios/Sources/RNImageStitcher/RNSARSession.swift +25 -1
- package/ios/Sources/RNImageStitcher/Stitcher.swift +34 -1
- package/ios/Sources/RNImageStitcher/StitcherBridge.m +5 -0
- package/ios/Sources/RNImageStitcher/StitcherBridge.swift +56 -0
- package/package.json +5 -1
- package/src/camera/Camera.tsx +945 -47
- package/src/camera/CameraView.tsx +48 -16
- package/src/camera/CaptureCountdownOverlay.tsx +272 -0
- package/src/camera/CaptureFrameCounterOverlay.tsx +197 -0
- package/src/camera/CaptureMemoryPill.tsx +50 -12
- package/src/camera/CapturePreview.tsx +5 -0
- package/src/camera/CaptureStatusOverlay.tsx +35 -7
- package/src/camera/CaptureThumbnailStrip.tsx +4 -0
- package/src/camera/LateralMotionModal.tsx +199 -0
- package/src/camera/PanHowToOverlay.tsx +246 -0
- package/src/camera/PanoramaBandOverlay.tsx +9 -1
- package/src/camera/PanoramaSettings.ts +27 -7
- package/src/camera/PanoramaSettingsModal.tsx +4 -4
- package/src/camera/RectCropPreview.tsx +638 -0
- package/src/camera/RotateToLandscapePrompt.tsx +188 -0
- package/src/camera/buildPanoramaInitialSettings.ts +30 -1
- package/src/camera/cameraErrorMessages.ts +39 -2
- package/src/camera/cameraGuidanceCopy.ts +145 -0
- package/src/camera/captureCountdown.ts +83 -0
- package/src/camera/captureWarnings.ts +190 -0
- package/src/camera/classifyStitchError.ts +68 -0
- package/src/camera/cropGeometry.ts +268 -0
- package/src/camera/displayDecodeImageProps.ts +25 -0
- package/src/camera/guidanceGraphics.tsx +347 -0
- package/src/camera/guidanceTokens.ts +57 -0
- package/src/camera/panModeGate.ts +81 -0
- package/src/camera/pickCaptureFormat.ts +130 -0
- package/src/camera/stitchDebugInfo.ts +71 -0
- package/src/camera/usePanMotion.ts +667 -0
- package/src/index.ts +66 -3
- package/src/stitching/computeInscribedRect.ts +81 -0
- package/src/stitching/cropQuad.ts +167 -0
- package/src/stitching/incremental.ts +74 -0
- package/src/stitching/useIncrementalStitcher.ts +13 -0
- package/android/src/main/java/io/imagestitcher/rn/TransferredNV21.kt +0 -100
- package/cpp/tests/CMakeLists.txt +0 -104
- package/cpp/tests/README.md +0 -86
- package/cpp/tests/keyframe_timebudget_test.cpp +0 -65
- package/cpp/tests/pose_test.cpp +0 -74
- package/cpp/tests/stitcher_frame_data_test.cpp +0 -132
- package/cpp/tests/stubs/jsi/jsi.h +0 -33
- package/cpp/tests/stubs/react-native-worklets-core/WKTJsiWorklet.h +0 -34
- package/cpp/tests/warp_guard_test.cpp +0 -48
- package/src/camera/__tests__/PanoramaSettingsBridge.test.ts +0 -190
- package/src/camera/__tests__/bandThumbRotation.test.ts +0 -120
- package/src/camera/__tests__/buildPanoramaInitialSettings.test.ts +0 -160
- package/src/camera/__tests__/cameraErrorMessages.test.ts +0 -76
- package/src/camera/__tests__/homeIndicatorEdge.test.ts +0 -116
- package/src/camera/__tests__/lowMemDevice.test.ts +0 -52
- package/src/camera/__tests__/selectCaptureDevice.test.ts +0 -210
- package/src/camera/__tests__/useContentRotation.test.ts +0 -89
- package/src/camera/__tests__/useOrientationDrift.test.ts +0 -169
- package/src/stitching/__tests__/subscribeIncrementalState.refine.test.ts +0 -276
- package/src/stitching/__tests__/useStitcherWorklet.test.ts +0 -202
package/src/index.ts
CHANGED
|
@@ -28,6 +28,7 @@ export { Camera, CameraError } from './camera/Camera';
|
|
|
28
28
|
export type {
|
|
29
29
|
CameraProps,
|
|
30
30
|
CameraCaptureResult,
|
|
31
|
+
PanoramaCaptureResult,
|
|
31
32
|
CameraErrorCode,
|
|
32
33
|
CaptureSource,
|
|
33
34
|
CaptureSourcesMode,
|
|
@@ -38,12 +39,29 @@ export type {
|
|
|
38
39
|
Warper,
|
|
39
40
|
FramesDroppedInfo,
|
|
40
41
|
} from './camera/Camera';
|
|
42
|
+
// Non-fatal capture quality signals carried on `CameraCaptureResult.warnings`.
|
|
43
|
+
export type {
|
|
44
|
+
CaptureWarning,
|
|
45
|
+
CaptureWarningCode,
|
|
46
|
+
CaptureWarningCopy,
|
|
47
|
+
} from './camera/captureWarnings';
|
|
48
|
+
// Default English warning templates (single source of truth; re-used by
|
|
49
|
+
// `DEFAULT_GUIDANCE_COPY`). Exposed so a host can diff / extend them.
|
|
50
|
+
export { DEFAULT_CAPTURE_WARNING_COPY } from './camera/captureWarnings';
|
|
41
51
|
|
|
42
52
|
// Recoverable-stitch-failure → friendly Alert copy. Hosts call this in
|
|
43
53
|
// their onError handler to surface actionable guidance ("pan more slowly",
|
|
44
|
-
// "pivot in place") instead of the raw cv::Stitcher diagnostic.
|
|
45
|
-
|
|
46
|
-
export
|
|
54
|
+
// "pivot in place") instead of the raw cv::Stitcher diagnostic. Pass an
|
|
55
|
+
// `overrides` map (keyed by `RECOVERABLE_STITCH_CODES`) to localise it.
|
|
56
|
+
export {
|
|
57
|
+
userFacingStitchError,
|
|
58
|
+
RECOVERABLE_STITCH_GUIDANCE,
|
|
59
|
+
RECOVERABLE_STITCH_CODES,
|
|
60
|
+
} from './camera/cameraErrorMessages';
|
|
61
|
+
export type {
|
|
62
|
+
UserFacingStitchError,
|
|
63
|
+
UserFacingStitchErrorOverrides,
|
|
64
|
+
} from './camera/cameraErrorMessages';
|
|
47
65
|
|
|
48
66
|
// ─────────────────────────────────────────────────────────────────────
|
|
49
67
|
// AR foundation (public since 0.1.0)
|
|
@@ -174,6 +192,51 @@ export type { UseOrientationDriftReturn } from './camera/useOrientationDrift';
|
|
|
174
192
|
export { OrientationDriftModal } from './camera/OrientationDriftModal';
|
|
175
193
|
export type { OrientationDriftModalProps } from './camera/OrientationDriftModal';
|
|
176
194
|
|
|
195
|
+
// ── Panorama capture GUIDANCE (feature/pano-ux-guidance) ──────────────
|
|
196
|
+
// The first-time-user pan-capture guidance surfaces, wired into Layer-1
|
|
197
|
+
// <Camera> automatically (panMode / panGuidance / maxPanDurationMs /
|
|
198
|
+
// lateralBudgetCm / rectCrop / showPreview / guidanceCopy props). Exported for
|
|
199
|
+
// Layer-2 hosts composing their own capture UX on CameraView + the
|
|
200
|
+
// incremental engine.
|
|
201
|
+
//
|
|
202
|
+
// `PanMode` is the landscape-only-vs-both flag; `GuidanceCopy` +
|
|
203
|
+
// `DEFAULT_GUIDANCE_COPY` are the overridable copy surface.
|
|
204
|
+
export type { PanMode } from './camera/panModeGate';
|
|
205
|
+
export {
|
|
206
|
+
DEFAULT_GUIDANCE_COPY,
|
|
207
|
+
} from './camera/cameraGuidanceCopy';
|
|
208
|
+
export type { GuidanceCopy } from './camera/cameraGuidanceCopy';
|
|
209
|
+
// Shared motion hook — one gyro + one accelerometer subscription feeding
|
|
210
|
+
// the pan-speed bucket (item 4) and the lateral-drift latch (item 6).
|
|
211
|
+
export { usePanMotion } from './camera/usePanMotion';
|
|
212
|
+
export type {
|
|
213
|
+
UsePanMotionOptions,
|
|
214
|
+
UsePanMotionReturn,
|
|
215
|
+
PanSpeedBucket,
|
|
216
|
+
PanAxis,
|
|
217
|
+
} from './camera/usePanMotion';
|
|
218
|
+
// Presentational guidance surfaces (each renders null when not visible).
|
|
219
|
+
export { RotateToLandscapePrompt } from './camera/RotateToLandscapePrompt';
|
|
220
|
+
export type { RotateToLandscapePromptProps } from './camera/RotateToLandscapePrompt';
|
|
221
|
+
export { PanHowToOverlay } from './camera/PanHowToOverlay';
|
|
222
|
+
export type { PanHowToOverlayProps } from './camera/PanHowToOverlay';
|
|
223
|
+
export { CaptureCountdownOverlay } from './camera/CaptureCountdownOverlay';
|
|
224
|
+
export type { CaptureCountdownOverlayProps } from './camera/CaptureCountdownOverlay';
|
|
225
|
+
export { CaptureFrameCounterOverlay } from './camera/CaptureFrameCounterOverlay';
|
|
226
|
+
export type { CaptureFrameCounterOverlayProps } from './camera/CaptureFrameCounterOverlay';
|
|
227
|
+
export { LateralMotionModal } from './camera/LateralMotionModal';
|
|
228
|
+
export type { LateralMotionModalProps } from './camera/LateralMotionModal';
|
|
229
|
+
export { RectCropPreview } from './camera/RectCropPreview';
|
|
230
|
+
export type {
|
|
231
|
+
RectCropPreviewProps,
|
|
232
|
+
RectCropResult,
|
|
233
|
+
ImageRect,
|
|
234
|
+
} from './camera/RectCropPreview';
|
|
235
|
+
// Native perspective-rectify crop used by RectCropPreview's confirm
|
|
236
|
+
// path; hosts driving their own crop UI call it directly.
|
|
237
|
+
export { cropQuad } from './stitching/cropQuad';
|
|
238
|
+
export type { CropQuadOptions, CropQuadResult } from './stitching/cropQuad';
|
|
239
|
+
|
|
177
240
|
// ── Incremental stitching engine ──────────────────────────────────────
|
|
178
241
|
// JS bindings around the native `IncrementalStitcher` module. Use
|
|
179
242
|
// these when you need finer control than <Camera>'s built-in
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/**
|
|
3
|
+
* computeInscribedRect — resolve the largest axis-aligned rectangle that
|
|
4
|
+
* fits entirely inside the non-black (coverage) region of a stitched
|
|
5
|
+
* panorama, via native `BatchStitcher.computeInscribedRect`.
|
|
6
|
+
*
|
|
7
|
+
* Used to SEED the post-capture crop editor (`RectCropPreview`): instead of
|
|
8
|
+
* a blind 8 %-inset rectangle, the editor opens on the max-inscribed rect —
|
|
9
|
+
* the tightest clean rectangle with no black corners — which the user then
|
|
10
|
+
* fine-tunes. Best-effort: callers fall back to the default inset seed if
|
|
11
|
+
* the native module is absent or the call rejects.
|
|
12
|
+
*
|
|
13
|
+
* Same native module + defensive-availability posture as
|
|
14
|
+
* `src/stitching/cropQuad.ts` and `src/quality/normaliseOrientation.ts`.
|
|
15
|
+
* The native side (Android `BatchStitcher.computeInscribedRect`, iOS
|
|
16
|
+
* `OpenCVStitcher.computeInscribedRect`) is the exact `maxInscribedRectFromMask`
|
|
17
|
+
* port used by the opt-in auto-crop, so the seed matches what that crop would
|
|
18
|
+
* pick — only here the user can then drag outward to keep more content.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { NativeModules } from 'react-native';
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
/** Resolved max-inscribed rectangle, in image-pixel coords. */
|
|
25
|
+
export interface InscribedRect {
|
|
26
|
+
x: number;
|
|
27
|
+
y: number;
|
|
28
|
+
width: number;
|
|
29
|
+
height: number;
|
|
30
|
+
/** Intrinsic dimensions of the source image the rect was computed on. */
|
|
31
|
+
imageWidth: number;
|
|
32
|
+
imageHeight: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** The shape of the native module method we call. */
|
|
36
|
+
interface InscribedRectNativeModule {
|
|
37
|
+
computeInscribedRect: (options: {
|
|
38
|
+
imagePath: string;
|
|
39
|
+
}) => Promise<InscribedRect>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Resolve the native `computeInscribedRect` function off
|
|
45
|
+
* `NativeModules.BatchStitcher`, or `null` when the module / method isn't
|
|
46
|
+
* registered (e.g. an older native build).
|
|
47
|
+
*/
|
|
48
|
+
function resolveComputeInscribedRect():
|
|
49
|
+
| InscribedRectNativeModule['computeInscribedRect']
|
|
50
|
+
| null {
|
|
51
|
+
const native: unknown =
|
|
52
|
+
(NativeModules as Record<string, unknown>)['BatchStitcher'];
|
|
53
|
+
if (
|
|
54
|
+
native
|
|
55
|
+
&& typeof native === 'object'
|
|
56
|
+
&& typeof (native as { computeInscribedRect?: unknown })
|
|
57
|
+
.computeInscribedRect === 'function'
|
|
58
|
+
) {
|
|
59
|
+
return (native as InscribedRectNativeModule).computeInscribedRect;
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Compute the max-inscribed rectangle of `imagePath`'s coverage mask.
|
|
67
|
+
*
|
|
68
|
+
* @param imagePath file:// URI or bare path of the stitched image (the
|
|
69
|
+
* native side strips the scheme).
|
|
70
|
+
* @returns the inscribed rect, or `null` when the native module isn't
|
|
71
|
+
* registered (older native build) — callers then fall back to the default
|
|
72
|
+
* seed. REJECTS only if the native call itself errors (decode / read
|
|
73
|
+
* failure); callers should catch and treat the rejection as "no seed".
|
|
74
|
+
*/
|
|
75
|
+
export async function computeInscribedRect(
|
|
76
|
+
imagePath: string,
|
|
77
|
+
): Promise<InscribedRect | null> {
|
|
78
|
+
const fn = resolveComputeInscribedRect();
|
|
79
|
+
if (!fn) return null;
|
|
80
|
+
return fn({ imagePath });
|
|
81
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/**
|
|
3
|
+
* cropQuad — item-7 perspective crop: rectify a user-dragged
|
|
4
|
+
* quadrilateral to an upright rectangle.
|
|
5
|
+
*
|
|
6
|
+
* The post-capture crop editor (`src/camera/RectCropPreview.tsx`) lets the
|
|
7
|
+
* user drag 4 independent corners over the stitched result. When that
|
|
8
|
+
* quad isn't ~axis-aligned, the host calls THIS wrapper instead of the
|
|
9
|
+
* cheap `cropToRect`: it hands the 4 IMAGE-PIXEL corners to the native
|
|
10
|
+
* `BatchStitcher.cropToQuad`, which runs
|
|
11
|
+
* `cv::getPerspectiveTransform` + `cv::warpPerspective` to produce an
|
|
12
|
+
* upright rectangle (averaged opposite-edge dimensions) and overwrites the
|
|
13
|
+
* file in place.
|
|
14
|
+
*
|
|
15
|
+
* This is the typed twin of the `cropToRect` call in
|
|
16
|
+
* `example/InscribedRectDebug.tsx` — same native module (`BatchStitcher`),
|
|
17
|
+
* same in-place overwrite + `{ width, height }` result contract, same
|
|
18
|
+
* platform-availability fallback posture as
|
|
19
|
+
* `src/quality/normaliseOrientation.ts`.
|
|
20
|
+
*
|
|
21
|
+
* Corner-order contract: `quadImagePoints` MUST be in canonical
|
|
22
|
+
* [TL, TR, BR, BL] (clockwise from top-left) order — exactly what
|
|
23
|
+
* `cropGeometry.ts:orderQuadCorners` produces and `RectCropResult.quad`
|
|
24
|
+
* carries. The native side rectifies into a rectangle whose corners map
|
|
25
|
+
* TL→(0,0), TR→(w,0), BR→(w,h), BL→(0,h); pass un-ordered points and the
|
|
26
|
+
* output is mirrored / rotated.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { NativeModules, Platform } from 'react-native';
|
|
30
|
+
|
|
31
|
+
import type { Point, Quad } from '../camera/cropGeometry';
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
/** Options for {@link cropQuad}. */
|
|
35
|
+
export interface CropQuadOptions {
|
|
36
|
+
/**
|
|
37
|
+
* JPEG quality for the re-encoded output, 1–100. Defaults to 90 (the
|
|
38
|
+
* native default, matching `cropToRect`).
|
|
39
|
+
*/
|
|
40
|
+
quality?: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Resolved result of a successful {@link cropQuad}. */
|
|
44
|
+
export interface CropQuadResult {
|
|
45
|
+
/**
|
|
46
|
+
* The file the rectified image was written to. Equals the input
|
|
47
|
+
* `imagePath` (the native crop overwrites in place) — surfaced
|
|
48
|
+
* explicitly so callers don't have to assume the in-place contract.
|
|
49
|
+
*/
|
|
50
|
+
outputPath: string;
|
|
51
|
+
/** Width of the rectified rectangle, in pixels. */
|
|
52
|
+
width: number;
|
|
53
|
+
/** Height of the rectified rectangle, in pixels. */
|
|
54
|
+
height: number;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
/** The shape of the native module method we call. */
|
|
59
|
+
interface CropQuadNativeModule {
|
|
60
|
+
cropToQuad: (options: {
|
|
61
|
+
imagePath: string;
|
|
62
|
+
quad: number[];
|
|
63
|
+
quality: number;
|
|
64
|
+
}) => Promise<{ width: number; height: number }>;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Resolve the native `cropToQuad` function off `NativeModules.BatchStitcher`,
|
|
70
|
+
* or `null` when the module / method isn't registered (e.g. an older native
|
|
71
|
+
* build). Same defensive lookup as `normaliseOrientation`.
|
|
72
|
+
*/
|
|
73
|
+
function resolveCropToQuad(): CropQuadNativeModule['cropToQuad'] | null {
|
|
74
|
+
const native: unknown =
|
|
75
|
+
(NativeModules as Record<string, unknown>)['BatchStitcher'];
|
|
76
|
+
if (
|
|
77
|
+
native
|
|
78
|
+
&& typeof native === 'object'
|
|
79
|
+
&& typeof (native as { cropToQuad?: unknown }).cropToQuad === 'function'
|
|
80
|
+
) {
|
|
81
|
+
return (native as CropQuadNativeModule).cropToQuad;
|
|
82
|
+
}
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Flatten the 4 ordered ([TL, TR, BR, BL]) image-pixel corners into the
|
|
89
|
+
* `[tlX, tlY, trX, trY, brX, brY, blX, blY]` array the native module
|
|
90
|
+
* expects. Exported for unit tests + reuse.
|
|
91
|
+
*/
|
|
92
|
+
export function flattenQuad(quad: Quad): number[] {
|
|
93
|
+
const out: number[] = [];
|
|
94
|
+
for (const p of quad as ReadonlyArray<Point>) {
|
|
95
|
+
out.push(p.x, p.y);
|
|
96
|
+
}
|
|
97
|
+
return out;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Perspective-rectify `quadImagePoints` out of `imagePath` into an upright
|
|
103
|
+
* rectangle, overwriting the file in place, and resolve the output path +
|
|
104
|
+
* rectified dimensions.
|
|
105
|
+
*
|
|
106
|
+
* @param imagePath file:// URI (or bare path) of the image to crop.
|
|
107
|
+
* @param quadImagePoints the 4 corners in IMAGE-PIXEL space, canonically
|
|
108
|
+
* ordered [TL, TR, BR, BL] (use
|
|
109
|
+
* `orderQuadCorners`). This is exactly
|
|
110
|
+
* `RectCropResult.quad`.
|
|
111
|
+
* @param outPath where to write the result. The native crop
|
|
112
|
+
* OVERWRITES IN PLACE, so this currently MUST equal
|
|
113
|
+
* `imagePath` (or be omitted — defaults to it).
|
|
114
|
+
* Passing a different path throws, surfacing the
|
|
115
|
+
* limitation rather than silently ignoring it; see
|
|
116
|
+
* the integrator note in the item-7 handoff.
|
|
117
|
+
* @param opts optional `{ quality }`.
|
|
118
|
+
*
|
|
119
|
+
* @throws if the native module isn't registered, if `outPath` differs from
|
|
120
|
+
* `imagePath`, or if the native crop rejects (degenerate quad,
|
|
121
|
+
* canvas guard, write failure).
|
|
122
|
+
*/
|
|
123
|
+
export async function cropQuad(
|
|
124
|
+
imagePath: string,
|
|
125
|
+
quadImagePoints: Quad,
|
|
126
|
+
outPath?: string,
|
|
127
|
+
opts?: CropQuadOptions,
|
|
128
|
+
): Promise<CropQuadResult> {
|
|
129
|
+
if (outPath !== undefined && outPath !== imagePath) {
|
|
130
|
+
// The native cropToQuad (like cropToRect) only overwrites in place.
|
|
131
|
+
// Fail loudly rather than silently writing to imagePath and returning
|
|
132
|
+
// a path the file isn't at.
|
|
133
|
+
throw new Error(
|
|
134
|
+
'[capture-sdk] cropQuad: native crop overwrites in place; '
|
|
135
|
+
+ 'outPath must equal imagePath (or be omitted).',
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const fn = resolveCropToQuad();
|
|
140
|
+
if (!fn) {
|
|
141
|
+
throw new Error(
|
|
142
|
+
`[capture-sdk] cropQuad: native module BatchStitcher.cropToQuad not `
|
|
143
|
+
+ `available on ${Platform.OS}. Ensure the native module is registered.`,
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const quality = clampQuality(opts?.quality);
|
|
148
|
+
const dims = await fn({
|
|
149
|
+
imagePath,
|
|
150
|
+
quad: flattenQuad(quadImagePoints),
|
|
151
|
+
quality,
|
|
152
|
+
});
|
|
153
|
+
return {
|
|
154
|
+
outputPath: imagePath,
|
|
155
|
+
width: dims.width,
|
|
156
|
+
height: dims.height,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
/** Clamp the requested JPEG quality into [1, 100]; default 90. */
|
|
162
|
+
function clampQuality(quality?: number): number {
|
|
163
|
+
if (quality === undefined || Number.isNaN(quality)) return 90;
|
|
164
|
+
if (quality < 1) return 1;
|
|
165
|
+
if (quality > 100) return 100;
|
|
166
|
+
return Math.round(quality);
|
|
167
|
+
}
|
|
@@ -682,6 +682,50 @@ export interface IncrementalFinalizeResult {
|
|
|
682
682
|
* on the just-completed capture.
|
|
683
683
|
*/
|
|
684
684
|
stitchModeResolved?: 'panorama' | 'scans';
|
|
685
|
+
/**
|
|
686
|
+
* 2026-06-15 (DEV) — gyro rotation magnitude of the capture, in RADIANS
|
|
687
|
+
* (angle between the first and last accepted keyframe camera-forward vectors).
|
|
688
|
+
* Surfaced so a dev tool can display it and tune the panorama-vs-SCANS
|
|
689
|
+
* rotation threshold from real captures. `0` when there is no pose-derived
|
|
690
|
+
* rotation signal (non-AR with no poses) — not necessarily "no rotation".
|
|
691
|
+
*/
|
|
692
|
+
rRadians?: number;
|
|
693
|
+
/**
|
|
694
|
+
* 2026-06-16 (DEV) — translation magnitude (metres) and the auto decision
|
|
695
|
+
* ratio (`tScore/(tScore+rScore)`, `>=0.55` → SCANS) that drove the
|
|
696
|
+
* panorama-vs-SCANS choice. Surfaced alongside `rRadians` so a dev tool can
|
|
697
|
+
* display the full decision inputs and tune the threshold from real captures.
|
|
698
|
+
* `0` when there is no motion signal (non-AR with no poses / no movement).
|
|
699
|
+
*/
|
|
700
|
+
tMeters?: number;
|
|
701
|
+
decisionRatio?: number;
|
|
702
|
+
/**
|
|
703
|
+
* 2026-06-14 (DEV overlay) — a semicolon-separated `key=value` trace of the
|
|
704
|
+
* stitcher's RUNTIME choices for this output, e.g.
|
|
705
|
+
* `"pipe=manual;warp=spherical;route=batch;seam=graphcut;blend=multiband"`.
|
|
706
|
+
* pipe: `manual` (cv::detail) | `highlevel` (cv::Stitcher)
|
|
707
|
+
* warp: `plane` | `cylindrical` | `spherical`
|
|
708
|
+
* route: `batch` (warp-all + seam) | `stream` (low-memory per-frame)
|
|
709
|
+
* seam: `graphcut` | `none`
|
|
710
|
+
* blend: `multiband` | `feather`
|
|
711
|
+
* Intended for a __DEV__-only overlay so the operator can see HOW the
|
|
712
|
+
* panorama was built (which warper, whether the low-memory stream/feather
|
|
713
|
+
* fallback kicked in, etc.). iOS only for now; undefined elsewhere.
|
|
714
|
+
*/
|
|
715
|
+
debugSummary?: string;
|
|
716
|
+
/**
|
|
717
|
+
* 2026-06-15 (iOS) — the exact keyframe JPEG paths used for this stitch.
|
|
718
|
+
* Lets the host re-stitch the SAME frames on demand via `refinePanorama`
|
|
719
|
+
* (e.g. the high-level preview tab) without re-running the capture or
|
|
720
|
+
* enumerating the session directory. iOS only; undefined elsewhere.
|
|
721
|
+
*/
|
|
722
|
+
batchKeyframePaths?: string[];
|
|
723
|
+
/**
|
|
724
|
+
* 2026-06-15 (iOS) — the capture orientation this stitch baked into the
|
|
725
|
+
* output. An on-demand re-stitch (refinePanorama) MUST pass this back or the
|
|
726
|
+
* result comes out in the raw sensor landscape (sideways). iOS only.
|
|
727
|
+
*/
|
|
728
|
+
captureOrientation?: string;
|
|
685
729
|
}
|
|
686
730
|
|
|
687
731
|
|
|
@@ -750,6 +794,15 @@ export interface IncrementalRefineOptions {
|
|
|
750
794
|
stitchMode?: 'auto' | 'panorama' | 'scans';
|
|
751
795
|
/** JPEG quality 1..100, default 90. */
|
|
752
796
|
jpegQuality?: number;
|
|
797
|
+
/**
|
|
798
|
+
* 2026-06-15 (iOS) — which stitch pipeline to run. `true` = the manual
|
|
799
|
+
* `cv::detail` pipeline (the default batch-capture output); `false` = stock
|
|
800
|
+
* high-level `cv::Stitcher`. Default `false` on the refine path. This is
|
|
801
|
+
* how the on-demand "high-level" preview tab re-stitches the captured
|
|
802
|
+
* keyframes via cv::Stitcher without re-running the whole capture. iOS only
|
|
803
|
+
* (Android refine is always cv::Stitcher).
|
|
804
|
+
*/
|
|
805
|
+
useManualPipeline?: boolean;
|
|
753
806
|
}
|
|
754
807
|
|
|
755
808
|
|
|
@@ -771,6 +824,15 @@ export interface IncrementalRefineResult {
|
|
|
771
824
|
framesDropped: number;
|
|
772
825
|
/** The confidence threshold that succeeded. -1 when not applicable. */
|
|
773
826
|
finalConfidenceThresh: number;
|
|
827
|
+
/**
|
|
828
|
+
* 2026-06-15 (DEV overlay A/B-aware) — the stitcher's own semicolon-separated
|
|
829
|
+
* `key=value` runtime recipe for THIS refined output, e.g.
|
|
830
|
+
* `"pipe=highlevel;warp=spherical;route=batch;seam=graphcut;blend=multiband"`.
|
|
831
|
+
* Mirrors `IncrementalFinalizeResult.debugSummary`. Lets the on-demand
|
|
832
|
+
* high-level preview tab show its OWN recipe in the __DEV__ overlay pill
|
|
833
|
+
* instead of the manual primary's recipe. iOS only; undefined elsewhere.
|
|
834
|
+
*/
|
|
835
|
+
debugSummary?: string;
|
|
774
836
|
}
|
|
775
837
|
|
|
776
838
|
|
|
@@ -852,6 +914,14 @@ interface NativeIncrementalModule {
|
|
|
852
914
|
* are zero, matching legacy behaviour.
|
|
853
915
|
*/
|
|
854
916
|
imuTranslationMetres?: number;
|
|
917
|
+
/**
|
|
918
|
+
* 2026-06-16 — the explicit lens the user selected (`'1x'` | `'0.5x'`).
|
|
919
|
+
* The reliable zoom signal for the high-level warper tree: `'0.5x'`
|
|
920
|
+
* (ultra-wide) → spherical warper. Replaces deriving zoom from the
|
|
921
|
+
* intrinsics FOV (unreliable on multi-cam 0.5x / non-AR fx=0). Omitted →
|
|
922
|
+
* treated as `'1x'`.
|
|
923
|
+
*/
|
|
924
|
+
lens?: string;
|
|
855
925
|
}): Promise<IncrementalFinalizeResult>;
|
|
856
926
|
cancel(): Promise<{ ok: true }>;
|
|
857
927
|
getState(): Promise<IncrementalState | null>;
|
|
@@ -877,6 +947,10 @@ interface NativeIncrementalModule {
|
|
|
877
947
|
* one-true-number for "how close are we to OOM?". Returns -1
|
|
878
948
|
* on task_info failure (very rare). Resolves immediately. */
|
|
879
949
|
getMemoryFootprintMB(): Promise<number>;
|
|
950
|
+
/** 2026-06-16 — total physical RAM in MB. Lets the DEV memory pill derive
|
|
951
|
+
* RAM-aware pressure bands instead of iPhone-fixed thresholds. -1 on
|
|
952
|
+
* failure. Resolves immediately. */
|
|
953
|
+
getDeviceTotalRamMB?(): Promise<number>;
|
|
880
954
|
/**
|
|
881
955
|
* 2026-05-16 — realtime+batch fusion API foundation. Run the
|
|
882
956
|
* shared C++ `cv::Stitcher` pipeline over a caller-supplied list
|
|
@@ -85,6 +85,12 @@ export interface UseIncrementalStitcherReturn {
|
|
|
85
85
|
* translation magnitude and prefers that).
|
|
86
86
|
*/
|
|
87
87
|
imuTranslationMetres?: number,
|
|
88
|
+
/**
|
|
89
|
+
* 2026-06-16 — the EXPLICIT lens the user selected (`'1x'` | `'0.5x'`).
|
|
90
|
+
* The reliable zoom signal for the high-level warper tree (`'0.5x'`
|
|
91
|
+
* ultra-wide → spherical). Omit ⇒ treated as `'1x'`.
|
|
92
|
+
*/
|
|
93
|
+
lens?: string,
|
|
88
94
|
) => Promise<IncrementalFinalizeResult>;
|
|
89
95
|
/** Abort the capture without producing output. */
|
|
90
96
|
cancel: () => Promise<void>;
|
|
@@ -198,6 +204,7 @@ export function useIncrementalStitcher(): UseIncrementalStitcherReturn {
|
|
|
198
204
|
quality = 90,
|
|
199
205
|
captureOrientation?: string,
|
|
200
206
|
imuTranslationMetres?: number,
|
|
207
|
+
lens?: string,
|
|
201
208
|
): Promise<IncrementalFinalizeResult> => {
|
|
202
209
|
if (!native) {
|
|
203
210
|
throw new Error('useIncrementalStitcher: native module unavailable');
|
|
@@ -216,6 +223,12 @@ export function useIncrementalStitcher(): UseIncrementalStitcherReturn {
|
|
|
216
223
|
// doesn't carry tx/ty/tz, so pose-derived translation is 0).
|
|
217
224
|
// Native side treats it as a magnitude (always ≥ 0).
|
|
218
225
|
imuTranslationMetres: Math.max(0, imuTranslationMetres ?? 0),
|
|
226
|
+
// 2026-06-16 — the EXPLICIT lens the user selected ('1x' | '0.5x').
|
|
227
|
+
// This is the reliable zoom signal for the high-level warper tree
|
|
228
|
+
// (0.5x ultra-wide → spherical); deriving zoom from intrinsics FOV was
|
|
229
|
+
// unreliable (multi-cam 0.5x reaches the ultra-wide by zoom without
|
|
230
|
+
// changing the reported fx, and the non-AR path may supply fx=0).
|
|
231
|
+
lens,
|
|
219
232
|
});
|
|
220
233
|
setIsRunning(false);
|
|
221
234
|
// Clear React state on finalize so the next start doesn't
|
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
-
package io.imagestitcher.rn
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* v0.10.0 (audit #4A) — single-use NV21 byte-array handle that
|
|
6
|
-
* enforces the engine's pixel-data ownership contract at runtime.
|
|
7
|
-
*
|
|
8
|
-
* ## Why this exists
|
|
9
|
-
*
|
|
10
|
-
* `IncrementalStitcher.ingestFromARCameraView` accepts an
|
|
11
|
-
* `nv21PixelData` parameter that the engine retains for ~50 ms
|
|
12
|
-
* after the producer thread returns (until the `workScope`
|
|
13
|
-
* coroutine consumes it). The documented contract is
|
|
14
|
-
* "callers MUST treat the array as transferred — do not mutate it
|
|
15
|
-
* or return it to a buffer pool after calling this method."
|
|
16
|
-
*
|
|
17
|
-
* The v0.10.0 audit (`docs/plans/handoff/2026-05-26-autonomous-run-handoff.md`
|
|
18
|
-
* finding #4A) noted this is by-convention only. The current AR
|
|
19
|
-
* caller (`RNSARCameraView`) passes the same `packed.nv21` array
|
|
20
|
-
* as BOTH `grayData` (consumed synchronously inside the gate)
|
|
21
|
-
* AND `nv21PixelData` (consumed asynchronously). Today no race
|
|
22
|
-
* because the sync read finishes before the async coroutine reads,
|
|
23
|
-
* but a future refactor that reorders consumption would silently
|
|
24
|
-
* corrupt frames.
|
|
25
|
-
*
|
|
26
|
-
* Wrapping the bytes in `TransferredNV21` turns the documentation
|
|
27
|
-
* contract into a runtime contract: callers can only extract the
|
|
28
|
-
* bytes once via `takeOnce()`; the second call throws. The
|
|
29
|
-
* misuse is caught at the call site, not at the engine.
|
|
30
|
-
*
|
|
31
|
-
* ## Cost
|
|
32
|
-
*
|
|
33
|
-
* Construction: tens of ns (one heap allocation for the wrapper +
|
|
34
|
-
* one volatile write of the bytes reference). `takeOnce()`: tens
|
|
35
|
-
* of ns (one synchronized read + one null-out). Negligible vs the
|
|
36
|
-
* underlying NV21 array's KB-scale memory footprint and the
|
|
37
|
-
* ms-scale frame-processing cost — but not a free pointer hop.
|
|
38
|
-
*
|
|
39
|
-
* ## Thread-safety
|
|
40
|
-
*
|
|
41
|
-
* `takeOnce()` and `available` are `synchronized` on the wrapper
|
|
42
|
-
* itself. Producers should still extract on a single thread (the
|
|
43
|
-
* frame producer); the synchronization defends against the
|
|
44
|
-
* pathological case where two threads race to extract.
|
|
45
|
-
*/
|
|
46
|
-
class TransferredNV21(bytes: ByteArray) {
|
|
47
|
-
init {
|
|
48
|
-
// Empty arrays would propagate as "0 bytes of pixel data with
|
|
49
|
-
// a non-zero width/height" downstream and crash inside the
|
|
50
|
-
// C++ ingest with a far less actionable error. Catch at
|
|
51
|
-
// construction. Critic-finding [MAJOR][B].
|
|
52
|
-
require(bytes.isNotEmpty()) {
|
|
53
|
-
"TransferredNV21 requires a non-empty byte array " +
|
|
54
|
-
"(received zero-length)"
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
@Volatile
|
|
59
|
-
private var bytes: ByteArray? = bytes
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Take the wrapped bytes. Throws on second call.
|
|
63
|
-
*
|
|
64
|
-
* Consumers should call this exactly once — typically once per
|
|
65
|
-
* frame, on the producer thread, immediately before handing
|
|
66
|
-
* the bytes to the async work queue:
|
|
67
|
-
*
|
|
68
|
-
* ```kotlin
|
|
69
|
-
* val pixelBytes: ByteArray? = if (hasPixelData) nv21PixelData!!.takeOnce() else null
|
|
70
|
-
* workScope.launch {
|
|
71
|
-
* // pixelBytes is captured by value; no race.
|
|
72
|
-
* engine.addFramePixelData(nv21 = pixelBytes!!, ...)
|
|
73
|
-
* }
|
|
74
|
-
* ```
|
|
75
|
-
*
|
|
76
|
-
* Concurrency note: `@Volatile` on the bytes field plus the
|
|
77
|
-
* `synchronized(this)` block here together guarantee both
|
|
78
|
-
* visibility AND atomicity across threads. The `@Volatile` is
|
|
79
|
-
* defensive for any future non-synchronized read; today every
|
|
80
|
-
* accessor goes through the synchronized block.
|
|
81
|
-
*/
|
|
82
|
-
fun takeOnce(): ByteArray = synchronized(this) {
|
|
83
|
-
val b = bytes ?: error(
|
|
84
|
-
"TransferredNV21.takeOnce() called twice — bytes already transferred. " +
|
|
85
|
-
"Check that you're not passing the same TransferredNV21 instance to " +
|
|
86
|
-
"two consumers (e.g., a sync gate-eval call AND an async workScope.launch)."
|
|
87
|
-
)
|
|
88
|
-
bytes = null
|
|
89
|
-
b
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// Note: an `available` property was considered and removed in
|
|
93
|
-
// pre-merge review (critic-finding [MAJOR][B]). Any
|
|
94
|
-
// `if (handle.available) handle.takeOnce()` pattern is
|
|
95
|
-
// inherently TOCTOU-racy — another thread could win the
|
|
96
|
-
// takeOnce() between the check and the use. Consumers should
|
|
97
|
-
// call `takeOnce()` directly and catch the `IllegalStateException`
|
|
98
|
-
// if they need recovery semantics. No internal caller used
|
|
99
|
-
// `available`; YAGNI removed it.
|
|
100
|
-
}
|