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
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cropQuad — item-7 perspective crop: rectify a user-dragged
|
|
3
|
+
* quadrilateral to an upright rectangle.
|
|
4
|
+
*
|
|
5
|
+
* The post-capture crop editor (`src/camera/RectCropPreview.tsx`) lets the
|
|
6
|
+
* user drag 4 independent corners over the stitched result. When that
|
|
7
|
+
* quad isn't ~axis-aligned, the host calls THIS wrapper instead of the
|
|
8
|
+
* cheap `cropToRect`: it hands the 4 IMAGE-PIXEL corners to the native
|
|
9
|
+
* `BatchStitcher.cropToQuad`, which runs
|
|
10
|
+
* `cv::getPerspectiveTransform` + `cv::warpPerspective` to produce an
|
|
11
|
+
* upright rectangle (averaged opposite-edge dimensions) and overwrites the
|
|
12
|
+
* file in place.
|
|
13
|
+
*
|
|
14
|
+
* This is the typed twin of the `cropToRect` call in
|
|
15
|
+
* `example/InscribedRectDebug.tsx` — same native module (`BatchStitcher`),
|
|
16
|
+
* same in-place overwrite + `{ width, height }` result contract, same
|
|
17
|
+
* platform-availability fallback posture as
|
|
18
|
+
* `src/quality/normaliseOrientation.ts`.
|
|
19
|
+
*
|
|
20
|
+
* Corner-order contract: `quadImagePoints` MUST be in canonical
|
|
21
|
+
* [TL, TR, BR, BL] (clockwise from top-left) order — exactly what
|
|
22
|
+
* `cropGeometry.ts:orderQuadCorners` produces and `RectCropResult.quad`
|
|
23
|
+
* carries. The native side rectifies into a rectangle whose corners map
|
|
24
|
+
* TL→(0,0), TR→(w,0), BR→(w,h), BL→(0,h); pass un-ordered points and the
|
|
25
|
+
* output is mirrored / rotated.
|
|
26
|
+
*/
|
|
27
|
+
import type { Quad } from '../camera/cropGeometry';
|
|
28
|
+
/** Options for {@link cropQuad}. */
|
|
29
|
+
export interface CropQuadOptions {
|
|
30
|
+
/**
|
|
31
|
+
* JPEG quality for the re-encoded output, 1–100. Defaults to 90 (the
|
|
32
|
+
* native default, matching `cropToRect`).
|
|
33
|
+
*/
|
|
34
|
+
quality?: number;
|
|
35
|
+
}
|
|
36
|
+
/** Resolved result of a successful {@link cropQuad}. */
|
|
37
|
+
export interface CropQuadResult {
|
|
38
|
+
/**
|
|
39
|
+
* The file the rectified image was written to. Equals the input
|
|
40
|
+
* `imagePath` (the native crop overwrites in place) — surfaced
|
|
41
|
+
* explicitly so callers don't have to assume the in-place contract.
|
|
42
|
+
*/
|
|
43
|
+
outputPath: string;
|
|
44
|
+
/** Width of the rectified rectangle, in pixels. */
|
|
45
|
+
width: number;
|
|
46
|
+
/** Height of the rectified rectangle, in pixels. */
|
|
47
|
+
height: number;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Flatten the 4 ordered ([TL, TR, BR, BL]) image-pixel corners into the
|
|
51
|
+
* `[tlX, tlY, trX, trY, brX, brY, blX, blY]` array the native module
|
|
52
|
+
* expects. Exported for unit tests + reuse.
|
|
53
|
+
*/
|
|
54
|
+
export declare function flattenQuad(quad: Quad): number[];
|
|
55
|
+
/**
|
|
56
|
+
* Perspective-rectify `quadImagePoints` out of `imagePath` into an upright
|
|
57
|
+
* rectangle, overwriting the file in place, and resolve the output path +
|
|
58
|
+
* rectified dimensions.
|
|
59
|
+
*
|
|
60
|
+
* @param imagePath file:// URI (or bare path) of the image to crop.
|
|
61
|
+
* @param quadImagePoints the 4 corners in IMAGE-PIXEL space, canonically
|
|
62
|
+
* ordered [TL, TR, BR, BL] (use
|
|
63
|
+
* `orderQuadCorners`). This is exactly
|
|
64
|
+
* `RectCropResult.quad`.
|
|
65
|
+
* @param outPath where to write the result. The native crop
|
|
66
|
+
* OVERWRITES IN PLACE, so this currently MUST equal
|
|
67
|
+
* `imagePath` (or be omitted — defaults to it).
|
|
68
|
+
* Passing a different path throws, surfacing the
|
|
69
|
+
* limitation rather than silently ignoring it; see
|
|
70
|
+
* the integrator note in the item-7 handoff.
|
|
71
|
+
* @param opts optional `{ quality }`.
|
|
72
|
+
*
|
|
73
|
+
* @throws if the native module isn't registered, if `outPath` differs from
|
|
74
|
+
* `imagePath`, or if the native crop rejects (degenerate quad,
|
|
75
|
+
* canvas guard, write failure).
|
|
76
|
+
*/
|
|
77
|
+
export declare function cropQuad(imagePath: string, quadImagePoints: Quad, outPath?: string, opts?: CropQuadOptions): Promise<CropQuadResult>;
|
|
78
|
+
//# sourceMappingURL=cropQuad.d.ts.map
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
/**
|
|
4
|
+
* cropQuad — item-7 perspective crop: rectify a user-dragged
|
|
5
|
+
* quadrilateral to an upright rectangle.
|
|
6
|
+
*
|
|
7
|
+
* The post-capture crop editor (`src/camera/RectCropPreview.tsx`) lets the
|
|
8
|
+
* user drag 4 independent corners over the stitched result. When that
|
|
9
|
+
* quad isn't ~axis-aligned, the host calls THIS wrapper instead of the
|
|
10
|
+
* cheap `cropToRect`: it hands the 4 IMAGE-PIXEL corners to the native
|
|
11
|
+
* `BatchStitcher.cropToQuad`, which runs
|
|
12
|
+
* `cv::getPerspectiveTransform` + `cv::warpPerspective` to produce an
|
|
13
|
+
* upright rectangle (averaged opposite-edge dimensions) and overwrites the
|
|
14
|
+
* file in place.
|
|
15
|
+
*
|
|
16
|
+
* This is the typed twin of the `cropToRect` call in
|
|
17
|
+
* `example/InscribedRectDebug.tsx` — same native module (`BatchStitcher`),
|
|
18
|
+
* same in-place overwrite + `{ width, height }` result contract, same
|
|
19
|
+
* platform-availability fallback posture as
|
|
20
|
+
* `src/quality/normaliseOrientation.ts`.
|
|
21
|
+
*
|
|
22
|
+
* Corner-order contract: `quadImagePoints` MUST be in canonical
|
|
23
|
+
* [TL, TR, BR, BL] (clockwise from top-left) order — exactly what
|
|
24
|
+
* `cropGeometry.ts:orderQuadCorners` produces and `RectCropResult.quad`
|
|
25
|
+
* carries. The native side rectifies into a rectangle whose corners map
|
|
26
|
+
* TL→(0,0), TR→(w,0), BR→(w,h), BL→(0,h); pass un-ordered points and the
|
|
27
|
+
* output is mirrored / rotated.
|
|
28
|
+
*/
|
|
29
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
30
|
+
exports.flattenQuad = flattenQuad;
|
|
31
|
+
exports.cropQuad = cropQuad;
|
|
32
|
+
const react_native_1 = require("react-native");
|
|
33
|
+
/**
|
|
34
|
+
* Resolve the native `cropToQuad` function off `NativeModules.BatchStitcher`,
|
|
35
|
+
* or `null` when the module / method isn't registered (e.g. an older native
|
|
36
|
+
* build). Same defensive lookup as `normaliseOrientation`.
|
|
37
|
+
*/
|
|
38
|
+
function resolveCropToQuad() {
|
|
39
|
+
const native = react_native_1.NativeModules['BatchStitcher'];
|
|
40
|
+
if (native
|
|
41
|
+
&& typeof native === 'object'
|
|
42
|
+
&& typeof native.cropToQuad === 'function') {
|
|
43
|
+
return native.cropToQuad;
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Flatten the 4 ordered ([TL, TR, BR, BL]) image-pixel corners into the
|
|
49
|
+
* `[tlX, tlY, trX, trY, brX, brY, blX, blY]` array the native module
|
|
50
|
+
* expects. Exported for unit tests + reuse.
|
|
51
|
+
*/
|
|
52
|
+
function flattenQuad(quad) {
|
|
53
|
+
const out = [];
|
|
54
|
+
for (const p of quad) {
|
|
55
|
+
out.push(p.x, p.y);
|
|
56
|
+
}
|
|
57
|
+
return out;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Perspective-rectify `quadImagePoints` out of `imagePath` into an upright
|
|
61
|
+
* rectangle, overwriting the file in place, and resolve the output path +
|
|
62
|
+
* rectified dimensions.
|
|
63
|
+
*
|
|
64
|
+
* @param imagePath file:// URI (or bare path) of the image to crop.
|
|
65
|
+
* @param quadImagePoints the 4 corners in IMAGE-PIXEL space, canonically
|
|
66
|
+
* ordered [TL, TR, BR, BL] (use
|
|
67
|
+
* `orderQuadCorners`). This is exactly
|
|
68
|
+
* `RectCropResult.quad`.
|
|
69
|
+
* @param outPath where to write the result. The native crop
|
|
70
|
+
* OVERWRITES IN PLACE, so this currently MUST equal
|
|
71
|
+
* `imagePath` (or be omitted — defaults to it).
|
|
72
|
+
* Passing a different path throws, surfacing the
|
|
73
|
+
* limitation rather than silently ignoring it; see
|
|
74
|
+
* the integrator note in the item-7 handoff.
|
|
75
|
+
* @param opts optional `{ quality }`.
|
|
76
|
+
*
|
|
77
|
+
* @throws if the native module isn't registered, if `outPath` differs from
|
|
78
|
+
* `imagePath`, or if the native crop rejects (degenerate quad,
|
|
79
|
+
* canvas guard, write failure).
|
|
80
|
+
*/
|
|
81
|
+
async function cropQuad(imagePath, quadImagePoints, outPath, opts) {
|
|
82
|
+
if (outPath !== undefined && outPath !== imagePath) {
|
|
83
|
+
// The native cropToQuad (like cropToRect) only overwrites in place.
|
|
84
|
+
// Fail loudly rather than silently writing to imagePath and returning
|
|
85
|
+
// a path the file isn't at.
|
|
86
|
+
throw new Error('[capture-sdk] cropQuad: native crop overwrites in place; '
|
|
87
|
+
+ 'outPath must equal imagePath (or be omitted).');
|
|
88
|
+
}
|
|
89
|
+
const fn = resolveCropToQuad();
|
|
90
|
+
if (!fn) {
|
|
91
|
+
throw new Error(`[capture-sdk] cropQuad: native module BatchStitcher.cropToQuad not `
|
|
92
|
+
+ `available on ${react_native_1.Platform.OS}. Ensure the native module is registered.`);
|
|
93
|
+
}
|
|
94
|
+
const quality = clampQuality(opts?.quality);
|
|
95
|
+
const dims = await fn({
|
|
96
|
+
imagePath,
|
|
97
|
+
quad: flattenQuad(quadImagePoints),
|
|
98
|
+
quality,
|
|
99
|
+
});
|
|
100
|
+
return {
|
|
101
|
+
outputPath: imagePath,
|
|
102
|
+
width: dims.width,
|
|
103
|
+
height: dims.height,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
/** Clamp the requested JPEG quality into [1, 100]; default 90. */
|
|
107
|
+
function clampQuality(quality) {
|
|
108
|
+
if (quality === undefined || Number.isNaN(quality))
|
|
109
|
+
return 90;
|
|
110
|
+
if (quality < 1)
|
|
111
|
+
return 1;
|
|
112
|
+
if (quality > 100)
|
|
113
|
+
return 100;
|
|
114
|
+
return Math.round(quality);
|
|
115
|
+
}
|
|
116
|
+
//# sourceMappingURL=cropQuad.js.map
|
|
@@ -639,6 +639,50 @@ export interface IncrementalFinalizeResult {
|
|
|
639
639
|
* on the just-completed capture.
|
|
640
640
|
*/
|
|
641
641
|
stitchModeResolved?: 'panorama' | 'scans';
|
|
642
|
+
/**
|
|
643
|
+
* 2026-06-15 (DEV) — gyro rotation magnitude of the capture, in RADIANS
|
|
644
|
+
* (angle between the first and last accepted keyframe camera-forward vectors).
|
|
645
|
+
* Surfaced so a dev tool can display it and tune the panorama-vs-SCANS
|
|
646
|
+
* rotation threshold from real captures. `0` when there is no pose-derived
|
|
647
|
+
* rotation signal (non-AR with no poses) — not necessarily "no rotation".
|
|
648
|
+
*/
|
|
649
|
+
rRadians?: number;
|
|
650
|
+
/**
|
|
651
|
+
* 2026-06-16 (DEV) — translation magnitude (metres) and the auto decision
|
|
652
|
+
* ratio (`tScore/(tScore+rScore)`, `>=0.55` → SCANS) that drove the
|
|
653
|
+
* panorama-vs-SCANS choice. Surfaced alongside `rRadians` so a dev tool can
|
|
654
|
+
* display the full decision inputs and tune the threshold from real captures.
|
|
655
|
+
* `0` when there is no motion signal (non-AR with no poses / no movement).
|
|
656
|
+
*/
|
|
657
|
+
tMeters?: number;
|
|
658
|
+
decisionRatio?: number;
|
|
659
|
+
/**
|
|
660
|
+
* 2026-06-14 (DEV overlay) — a semicolon-separated `key=value` trace of the
|
|
661
|
+
* stitcher's RUNTIME choices for this output, e.g.
|
|
662
|
+
* `"pipe=manual;warp=spherical;route=batch;seam=graphcut;blend=multiband"`.
|
|
663
|
+
* pipe: `manual` (cv::detail) | `highlevel` (cv::Stitcher)
|
|
664
|
+
* warp: `plane` | `cylindrical` | `spherical`
|
|
665
|
+
* route: `batch` (warp-all + seam) | `stream` (low-memory per-frame)
|
|
666
|
+
* seam: `graphcut` | `none`
|
|
667
|
+
* blend: `multiband` | `feather`
|
|
668
|
+
* Intended for a __DEV__-only overlay so the operator can see HOW the
|
|
669
|
+
* panorama was built (which warper, whether the low-memory stream/feather
|
|
670
|
+
* fallback kicked in, etc.). iOS only for now; undefined elsewhere.
|
|
671
|
+
*/
|
|
672
|
+
debugSummary?: string;
|
|
673
|
+
/**
|
|
674
|
+
* 2026-06-15 (iOS) — the exact keyframe JPEG paths used for this stitch.
|
|
675
|
+
* Lets the host re-stitch the SAME frames on demand via `refinePanorama`
|
|
676
|
+
* (e.g. the high-level preview tab) without re-running the capture or
|
|
677
|
+
* enumerating the session directory. iOS only; undefined elsewhere.
|
|
678
|
+
*/
|
|
679
|
+
batchKeyframePaths?: string[];
|
|
680
|
+
/**
|
|
681
|
+
* 2026-06-15 (iOS) — the capture orientation this stitch baked into the
|
|
682
|
+
* output. An on-demand re-stitch (refinePanorama) MUST pass this back or the
|
|
683
|
+
* result comes out in the raw sensor landscape (sideways). iOS only.
|
|
684
|
+
*/
|
|
685
|
+
captureOrientation?: string;
|
|
642
686
|
}
|
|
643
687
|
/**
|
|
644
688
|
* 2026-05-16 — input to `refinePanorama`. Mirrors the subset of
|
|
@@ -701,6 +745,15 @@ export interface IncrementalRefineOptions {
|
|
|
701
745
|
stitchMode?: 'auto' | 'panorama' | 'scans';
|
|
702
746
|
/** JPEG quality 1..100, default 90. */
|
|
703
747
|
jpegQuality?: number;
|
|
748
|
+
/**
|
|
749
|
+
* 2026-06-15 (iOS) — which stitch pipeline to run. `true` = the manual
|
|
750
|
+
* `cv::detail` pipeline (the default batch-capture output); `false` = stock
|
|
751
|
+
* high-level `cv::Stitcher`. Default `false` on the refine path. This is
|
|
752
|
+
* how the on-demand "high-level" preview tab re-stitches the captured
|
|
753
|
+
* keyframes via cv::Stitcher without re-running the whole capture. iOS only
|
|
754
|
+
* (Android refine is always cv::Stitcher).
|
|
755
|
+
*/
|
|
756
|
+
useManualPipeline?: boolean;
|
|
704
757
|
}
|
|
705
758
|
/**
|
|
706
759
|
* 2026-05-16 — result of an explicit `refinePanorama` call. Mirrors
|
|
@@ -720,6 +773,15 @@ export interface IncrementalRefineResult {
|
|
|
720
773
|
framesDropped: number;
|
|
721
774
|
/** The confidence threshold that succeeded. -1 when not applicable. */
|
|
722
775
|
finalConfidenceThresh: number;
|
|
776
|
+
/**
|
|
777
|
+
* 2026-06-15 (DEV overlay A/B-aware) — the stitcher's own semicolon-separated
|
|
778
|
+
* `key=value` runtime recipe for THIS refined output, e.g.
|
|
779
|
+
* `"pipe=highlevel;warp=spherical;route=batch;seam=graphcut;blend=multiband"`.
|
|
780
|
+
* Mirrors `IncrementalFinalizeResult.debugSummary`. Lets the on-demand
|
|
781
|
+
* high-level preview tab show its OWN recipe in the __DEV__ overlay pill
|
|
782
|
+
* instead of the manual primary's recipe. iOS only; undefined elsewhere.
|
|
783
|
+
*/
|
|
784
|
+
debugSummary?: string;
|
|
723
785
|
}
|
|
724
786
|
/**
|
|
725
787
|
* V15.0e — ARKit plane detection state, polled by the capture screen
|
|
@@ -799,6 +861,14 @@ interface NativeIncrementalModule {
|
|
|
799
861
|
* are zero, matching legacy behaviour.
|
|
800
862
|
*/
|
|
801
863
|
imuTranslationMetres?: number;
|
|
864
|
+
/**
|
|
865
|
+
* 2026-06-16 — the explicit lens the user selected (`'1x'` | `'0.5x'`).
|
|
866
|
+
* The reliable zoom signal for the high-level warper tree: `'0.5x'`
|
|
867
|
+
* (ultra-wide) → spherical warper. Replaces deriving zoom from the
|
|
868
|
+
* intrinsics FOV (unreliable on multi-cam 0.5x / non-AR fx=0). Omitted →
|
|
869
|
+
* treated as `'1x'`.
|
|
870
|
+
*/
|
|
871
|
+
lens?: string;
|
|
802
872
|
}): Promise<IncrementalFinalizeResult>;
|
|
803
873
|
cancel(): Promise<{
|
|
804
874
|
ok: true;
|
|
@@ -830,6 +900,10 @@ interface NativeIncrementalModule {
|
|
|
830
900
|
* one-true-number for "how close are we to OOM?". Returns -1
|
|
831
901
|
* on task_info failure (very rare). Resolves immediately. */
|
|
832
902
|
getMemoryFootprintMB(): Promise<number>;
|
|
903
|
+
/** 2026-06-16 — total physical RAM in MB. Lets the DEV memory pill derive
|
|
904
|
+
* RAM-aware pressure bands instead of iPhone-fixed thresholds. -1 on
|
|
905
|
+
* failure. Resolves immediately. */
|
|
906
|
+
getDeviceTotalRamMB?(): Promise<number>;
|
|
833
907
|
/**
|
|
834
908
|
* 2026-05-16 — realtime+batch fusion API foundation. Run the
|
|
835
909
|
* shared C++ `cv::Stitcher` pipeline over a caller-supplied list
|
|
@@ -61,7 +61,13 @@ export interface UseIncrementalStitcherReturn {
|
|
|
61
61
|
* (e.g. in AR mode the native side has its own pose-driven
|
|
62
62
|
* translation magnitude and prefers that).
|
|
63
63
|
*/
|
|
64
|
-
imuTranslationMetres?: number
|
|
64
|
+
imuTranslationMetres?: number,
|
|
65
|
+
/**
|
|
66
|
+
* 2026-06-16 — the EXPLICIT lens the user selected (`'1x'` | `'0.5x'`).
|
|
67
|
+
* The reliable zoom signal for the high-level warper tree (`'0.5x'`
|
|
68
|
+
* ultra-wide → spherical). Omit ⇒ treated as `'1x'`.
|
|
69
|
+
*/
|
|
70
|
+
lens?: string) => Promise<IncrementalFinalizeResult>;
|
|
65
71
|
/** Abort the capture without producing output. */
|
|
66
72
|
cancel: () => Promise<void>;
|
|
67
73
|
}
|
|
@@ -110,7 +110,7 @@ function useIncrementalStitcher() {
|
|
|
110
110
|
setState(null);
|
|
111
111
|
lastHintRef.current = null;
|
|
112
112
|
}, [native]);
|
|
113
|
-
const finalize = (0, react_1.useCallback)(async (outputPath, quality = 90, captureOrientation, imuTranslationMetres) => {
|
|
113
|
+
const finalize = (0, react_1.useCallback)(async (outputPath, quality = 90, captureOrientation, imuTranslationMetres, lens) => {
|
|
114
114
|
if (!native) {
|
|
115
115
|
throw new Error('useIncrementalStitcher: native module unavailable');
|
|
116
116
|
}
|
|
@@ -128,6 +128,12 @@ function useIncrementalStitcher() {
|
|
|
128
128
|
// doesn't carry tx/ty/tz, so pose-derived translation is 0).
|
|
129
129
|
// Native side treats it as a magnitude (always ≥ 0).
|
|
130
130
|
imuTranslationMetres: Math.max(0, imuTranslationMetres ?? 0),
|
|
131
|
+
// 2026-06-16 — the EXPLICIT lens the user selected ('1x' | '0.5x').
|
|
132
|
+
// This is the reliable zoom signal for the high-level warper tree
|
|
133
|
+
// (0.5x ultra-wide → spherical); deriving zoom from intrinsics FOV was
|
|
134
|
+
// unreliable (multi-cam 0.5x reaches the ultra-wide by zoom without
|
|
135
|
+
// changing the reported fx, and the non-AR path may supply fx=0).
|
|
136
|
+
lens,
|
|
131
137
|
});
|
|
132
138
|
setIsRunning(false);
|
|
133
139
|
// Clear React state on finalize so the next start doesn't
|
|
@@ -223,6 +223,15 @@ struct FinalizePayload {
|
|
|
223
223
|
/// resolved upstream by `resolveStitchModeAuto` before this snapshot
|
|
224
224
|
/// is captured; this field never carries 'auto'.
|
|
225
225
|
let batchStitchModeResolved: String
|
|
226
|
+
/// Gyro rotation magnitude (radians) of the capture — surfaced to JS for the
|
|
227
|
+
/// dev 3-tab preview's rRadians readout (threshold tuning). 0.0 when there
|
|
228
|
+
/// is no pose-derived rotation signal (non-AR with no poses).
|
|
229
|
+
let rRadians: Double
|
|
230
|
+
/// Translation magnitude (metres) + the auto decision ratio
|
|
231
|
+
/// (tScore/(tScore+rScore), >=0.55 → SCANS) that drove the panorama-vs-SCANS
|
|
232
|
+
/// choice — surfaced to JS for the dev tuning readout alongside rRadians.
|
|
233
|
+
let tMeters: Double
|
|
234
|
+
let decisionRatio: Double
|
|
226
235
|
let keyframeExifOrientation: Int
|
|
227
236
|
/// AR-STITCHING-TWO-MODES (memory/ar-stitching-two-modes.md):
|
|
228
237
|
/// capture-time hold orientation for the bake-rotation pass.
|
|
@@ -430,6 +439,10 @@ public final class IncrementalStitcher: NSObject {
|
|
|
430
439
|
/// AR mode (where pose-derived tx/ty/tz is always 0). Set to 0
|
|
431
440
|
/// at start() and overwritten at finalize() entry.
|
|
432
441
|
private var batchImuTranslationMetres: Double = 0.0
|
|
442
|
+
/// 2026-06-16 — the explicit lens the user selected ('1x' | '0.5x'), set at
|
|
443
|
+
/// finalize() entry from JS. The zoom signal for the high-level warper tree
|
|
444
|
+
/// (0.5x ultra-wide → spherical). Defaults to '1x'.
|
|
445
|
+
private var batchLens: String = "1x"
|
|
433
446
|
/// AR-STITCHING-TWO-MODES — see memory/ar-stitching-two-modes.md
|
|
434
447
|
///
|
|
435
448
|
/// Physical phone orientation at start() time, sourced from the
|
|
@@ -492,6 +505,14 @@ public final class IncrementalStitcher: NSObject {
|
|
|
492
505
|
stateLock.unlock()
|
|
493
506
|
}
|
|
494
507
|
|
|
508
|
+
/// 2026-06-16 — store the explicit lens ('1x' | '0.5x') JS supplies at
|
|
509
|
+
/// finalize() entry; the high-level warper tree reads it (0.5x → spherical).
|
|
510
|
+
@objc public func updateLens(_ lens: String) {
|
|
511
|
+
stateLock.lock()
|
|
512
|
+
self.batchLens = lens
|
|
513
|
+
stateLock.unlock()
|
|
514
|
+
}
|
|
515
|
+
|
|
495
516
|
/// 2026-05-18 (Iss 3) — return the current capture's keyframe
|
|
496
517
|
/// session directory, or nil if no capture is in flight / engine
|
|
497
518
|
/// isn't using a per-session keyframe collector.
|
|
@@ -829,6 +850,7 @@ public final class IncrementalStitcher: NSObject {
|
|
|
829
850
|
// too. Updated at finalize() entry from JS-supplied
|
|
830
851
|
// option value.
|
|
831
852
|
self.batchImuTranslationMetres = 0.0
|
|
853
|
+
self.batchLens = "1x" // overwritten at finalize() from JS (updateLens)
|
|
832
854
|
self.batchKeyframeMode = true
|
|
833
855
|
os_log(.fault, log: Self.diagLog,
|
|
834
856
|
"[V16-batch-keyframe] start mode=batch-keyframe rotation=0 (was %d, forced to 0 to match pose intrinsics) sessionDir=%{public}@",
|
|
@@ -943,7 +965,7 @@ public final class IncrementalStitcher: NSObject {
|
|
|
943
965
|
} else if let v = configOverrides["maxKeyframeIntervalMs"] as? Int {
|
|
944
966
|
self.keyframeGate.maxKeyframeIntervalMs = max(0.0, Double(v))
|
|
945
967
|
} else {
|
|
946
|
-
self.keyframeGate.maxKeyframeIntervalMs =
|
|
968
|
+
self.keyframeGate.maxKeyframeIntervalMs = 1500.0
|
|
947
969
|
}
|
|
948
970
|
// V16 — novelty aggregation percentile. Clamp at start to
|
|
949
971
|
// [0.5, 0.99]; the bridge re-clamps but matching it here
|
|
@@ -1147,20 +1169,33 @@ public final class IncrementalStitcher: NSObject {
|
|
|
1147
1169
|
// translation/rotation magnitude ratio between first + last
|
|
1148
1170
|
// accepted keyframe poses → SCANS (translation-heavy) or
|
|
1149
1171
|
// PANORAMA (rotation-heavy). Non-auto values pass through.
|
|
1172
|
+
// Resolve once so the dev readout gets the SAME tMeters / ratio / rRadians
|
|
1173
|
+
// that drove the decision — and gets them even when the mode is forced
|
|
1174
|
+
// (informative: shows what auto WOULD have picked). Captured into the
|
|
1175
|
+
// payload here so the C2-invariant finalize closure can read them via
|
|
1176
|
+
// payload (no self/ivar access inside the closure).
|
|
1177
|
+
let autoResolution = resolveStitchModeAuto(
|
|
1178
|
+
first: batchFirstAcceptedPose,
|
|
1179
|
+
last: batchLastAcceptedPose,
|
|
1180
|
+
imuTranslationMetres: batchImuTranslationMetres)
|
|
1150
1181
|
let stitchModeResolved: String
|
|
1151
1182
|
switch batchStitchMode {
|
|
1152
1183
|
case "panorama": stitchModeResolved = "panorama"
|
|
1153
1184
|
case "scans": stitchModeResolved = "scans"
|
|
1154
|
-
default: stitchModeResolved =
|
|
1155
|
-
first: batchFirstAcceptedPose,
|
|
1156
|
-
last: batchLastAcceptedPose,
|
|
1157
|
-
imuTranslationMetres: batchImuTranslationMetres
|
|
1158
|
-
)
|
|
1185
|
+
default: stitchModeResolved = autoResolution.mode
|
|
1159
1186
|
}
|
|
1187
|
+
let rRadiansResolved = autoResolution.rRadians
|
|
1188
|
+
// 2026-06-16 — HIGH-LEVEL ACROSS THE BOARD (mirrors Android). Pick the
|
|
1189
|
+
// warper from the (motion, Mode A/B, lens) tree; the dispatch below now
|
|
1190
|
+
// forces useManualPipeline=false + stitchMode="panorama". batchWarperType
|
|
1191
|
+
// (settings) is superseded by the tree.
|
|
1192
|
+
let highLevelWarper = pickHighLevelWarper(
|
|
1193
|
+
orientation: captureOrientation,
|
|
1194
|
+
lens: batchLens)
|
|
1160
1195
|
os_log(.fault, log: Self.diagLog,
|
|
1161
|
-
"[V16-batch-keyframe.stitchMode] configured=%{public}@ resolved=%{public}@ paths=%d imuT=%.3fm",
|
|
1162
|
-
batchStitchMode, stitchModeResolved,
|
|
1163
|
-
batchImuTranslationMetres)
|
|
1196
|
+
"[V16-batch-keyframe.stitchMode] configured=%{public}@ resolved=%{public}@ warper=%{public}@ lens=%{public}@ paths=%d imuT=%.3fm",
|
|
1197
|
+
batchStitchMode, stitchModeResolved, highLevelWarper, batchLens,
|
|
1198
|
+
Int32(paths.count), batchImuTranslationMetres)
|
|
1164
1199
|
|
|
1165
1200
|
let payload = FinalizePayload(
|
|
1166
1201
|
cleaned: cleaned,
|
|
@@ -1168,11 +1203,14 @@ public final class IncrementalStitcher: NSObject {
|
|
|
1168
1203
|
inBatchKeyframeMode: inBatchKeyframeMode,
|
|
1169
1204
|
collector: collector,
|
|
1170
1205
|
paths: paths,
|
|
1171
|
-
batchWarperType:
|
|
1206
|
+
batchWarperType: highLevelWarper,
|
|
1172
1207
|
batchBlenderType: batchBlenderType,
|
|
1173
1208
|
batchSeamFinderType: batchSeamFinderType,
|
|
1174
1209
|
batchEnableInscribedRectCrop: batchEnableInscribedRectCrop,
|
|
1175
1210
|
batchStitchModeResolved: stitchModeResolved,
|
|
1211
|
+
rRadians: rRadiansResolved,
|
|
1212
|
+
tMeters: autoResolution.tMeters,
|
|
1213
|
+
decisionRatio: autoResolution.ratio,
|
|
1176
1214
|
keyframeExifOrientation: keyframeExifOrientation,
|
|
1177
1215
|
captureOrientation: captureOrientation,
|
|
1178
1216
|
drops: drops,
|
|
@@ -1438,7 +1476,15 @@ public final class IncrementalStitcher: NSObject {
|
|
|
1438
1476
|
seamFinderType: payload.batchSeamFinderType,
|
|
1439
1477
|
captureOrientation: payload.captureOrientation,
|
|
1440
1478
|
useInscribedRectCrop: payload.batchEnableInscribedRectCrop,
|
|
1441
|
-
|
|
1479
|
+
// 2026-06-16 — HIGH-LEVEL ACROSS THE BOARD (mirrors
|
|
1480
|
+
// Android): always cv::Stitcher PANORAMA with the
|
|
1481
|
+
// tree-chosen warper (payload.batchWarperType is now
|
|
1482
|
+
// highLevelWarper). The manual path's OOM hardening
|
|
1483
|
+
// was ported to high-level (catch ladder + two-phase
|
|
1484
|
+
// canvas guard + RAM-aware compositingResol + spherical
|
|
1485
|
+
// rescue), so this is now memory-safe.
|
|
1486
|
+
stitchMode: "panorama",
|
|
1487
|
+
useManualPipeline: false
|
|
1442
1488
|
)
|
|
1443
1489
|
// V16 fix-attempt 9 (verified on device,
|
|
1444
1490
|
// 2026-05-13) — sentinel-result detection.
|
|
@@ -1501,6 +1547,17 @@ public final class IncrementalStitcher: NSObject {
|
|
|
1501
1547
|
"batchKeyframeSessionDir":
|
|
1502
1548
|
payload.collector?.sessionDir ?? "",
|
|
1503
1549
|
"batchKeyframeCount": payload.paths.count,
|
|
1550
|
+
// 2026-06-15 — the exact keyframe JPEG paths used for
|
|
1551
|
+
// this stitch, so JS can re-stitch them ON DEMAND via
|
|
1552
|
+
// refinePanorama (the high-level tab) without listing
|
|
1553
|
+
// the session dir itself.
|
|
1554
|
+
"batchKeyframePaths": payload.paths,
|
|
1555
|
+
// The orientation this stitch baked into the output.
|
|
1556
|
+
// The on-demand high-level re-stitch MUST pass the
|
|
1557
|
+
// same value or it comes out in the raw sensor
|
|
1558
|
+
// landscape (sideways) — refinePanorama otherwise
|
|
1559
|
+
// defaults to "portrait" (no bake-rotation).
|
|
1560
|
+
"captureOrientation": payload.captureOrientation,
|
|
1504
1561
|
]
|
|
1505
1562
|
if r.framesRequested >= 0 {
|
|
1506
1563
|
batchDict["framesRequested"] = Int(r.framesRequested)
|
|
@@ -1523,6 +1580,17 @@ public final class IncrementalStitcher: NSObject {
|
|
|
1523
1580
|
// helps the operator understand why the
|
|
1524
1581
|
// panorama looks the way it does.
|
|
1525
1582
|
batchDict["stitchModeResolved"] = payload.batchStitchModeResolved
|
|
1583
|
+
batchDict["rRadians"] = payload.rRadians
|
|
1584
|
+
// Dev tuning readout — translation magnitude + the auto
|
|
1585
|
+
// decision ratio that drove panorama-vs-SCANS.
|
|
1586
|
+
batchDict["tMeters"] = payload.tMeters
|
|
1587
|
+
batchDict["decisionRatio"] = payload.decisionRatio
|
|
1588
|
+
// 2026-06-14 (DEV overlay) — the stitcher's runtime
|
|
1589
|
+
// choices (pipeline/warper/route/seam/blend) for this
|
|
1590
|
+
// output, shown on the preview in __DEV__.
|
|
1591
|
+
if !r.debugSummary.isEmpty {
|
|
1592
|
+
batchDict["debugSummary"] = r.debugSummary
|
|
1593
|
+
}
|
|
1526
1594
|
completion(batchDict, nil)
|
|
1527
1595
|
} catch let stitchErr as NSError {
|
|
1528
1596
|
completion(nil, stitchErr)
|
|
@@ -1641,6 +1709,11 @@ public final class IncrementalStitcher: NSObject {
|
|
|
1641
1709
|
// at line 738 of src/stitching/incremental.ts). JS callers
|
|
1642
1710
|
// can override by passing config["stitchMode"].
|
|
1643
1711
|
let refineStitchMode = (config["stitchMode"] as? String) ?? "scans"
|
|
1712
|
+
// 2026-06-15 — pipeline is caller-selectable. The on-demand high-level
|
|
1713
|
+
// tab calls refinePanorama with useManualPipeline:false to re-stitch the
|
|
1714
|
+
// captured keyframes via stock cv::Stitcher. Default false (high-level)
|
|
1715
|
+
// preserves the refine path's historical cv::Stitcher behaviour.
|
|
1716
|
+
let refineManual = (config["useManualPipeline"] as? Bool) ?? false
|
|
1644
1717
|
let quality = max(1, min(100, (config["jpegQuality"] as? Int) ?? 90))
|
|
1645
1718
|
let cleanedOutput = outputPath.hasPrefix("file://")
|
|
1646
1719
|
? String(outputPath.dropFirst(7))
|
|
@@ -1679,7 +1752,8 @@ public final class IncrementalStitcher: NSObject {
|
|
|
1679
1752
|
seamFinderType: seam,
|
|
1680
1753
|
captureOrientation: orientation,
|
|
1681
1754
|
useInscribedRectCrop: useInscribed,
|
|
1682
|
-
stitchMode: refineStitchMode
|
|
1755
|
+
stitchMode: refineStitchMode,
|
|
1756
|
+
useManualPipeline: refineManual
|
|
1683
1757
|
)
|
|
1684
1758
|
// fix-9 sentinel detection — see the finalize() path
|
|
1685
1759
|
// for the full rationale. A 0×0 result means
|
|
@@ -1717,7 +1791,13 @@ public final class IncrementalStitcher: NSObject {
|
|
|
1717
1791
|
frames: frameCount,
|
|
1718
1792
|
errorMessage: nil
|
|
1719
1793
|
)
|
|
1720
|
-
|
|
1794
|
+
// 2026-06-15 (DEV overlay A/B-aware) — carry the stitcher's
|
|
1795
|
+
// own runtime recipe up to JS so the preview's DEV pill shows
|
|
1796
|
+
// the HIGH-LEVEL recipe (pipe=highlevel;warp=spherical;…) while
|
|
1797
|
+
// the user views the high-level tab, instead of the manual
|
|
1798
|
+
// primary's recipe. Mirrors the batch finalize's batchDict
|
|
1799
|
+
// (guard empty — empty string means unavailable).
|
|
1800
|
+
var refineDict: [String: Any] = [
|
|
1721
1801
|
"panoramaPath": r.outputPath,
|
|
1722
1802
|
"width": Int(r.width),
|
|
1723
1803
|
"height": Int(r.height),
|
|
@@ -1725,7 +1805,11 @@ public final class IncrementalStitcher: NSObject {
|
|
|
1725
1805
|
"framesIncluded": frameCount,
|
|
1726
1806
|
"framesDropped": 0,
|
|
1727
1807
|
"finalConfidenceThresh": -1.0,
|
|
1728
|
-
]
|
|
1808
|
+
]
|
|
1809
|
+
if !r.debugSummary.isEmpty {
|
|
1810
|
+
refineDict["debugSummary"] = r.debugSummary
|
|
1811
|
+
}
|
|
1812
|
+
completion(refineDict, nil)
|
|
1729
1813
|
} catch let err as NSError {
|
|
1730
1814
|
self?.emitRefineProgress(
|
|
1731
1815
|
stage: "error",
|
|
@@ -2413,13 +2497,13 @@ public final class IncrementalStitcher: NSObject {
|
|
|
2413
2497
|
first: [Double]?,
|
|
2414
2498
|
last: [Double]?,
|
|
2415
2499
|
imuTranslationMetres: Double
|
|
2416
|
-
) -> String {
|
|
2500
|
+
) -> (mode: String, rRadians: Double, tMeters: Double, ratio: Double) {
|
|
2417
2501
|
guard let firstPose = first, firstPose.count == 7,
|
|
2418
2502
|
let lastPose = last, lastPose.count == 7 else {
|
|
2419
2503
|
// No pose data at all — fall back on whichever signal we
|
|
2420
2504
|
// do have. imuTranslationMetres > 0 hints "scans"; 0
|
|
2421
|
-
// hints "panorama".
|
|
2422
|
-
return imuTranslationMetres > 0.05 ? "scans" : "panorama"
|
|
2505
|
+
// hints "panorama". rRadians 0.0 — no gyro signal.
|
|
2506
|
+
return (imuTranslationMetres > 0.05 ? "scans" : "panorama", 0.0, 0.0, 0.0)
|
|
2423
2507
|
}
|
|
2424
2508
|
// Translation magnitude (Euclidean, in metres).
|
|
2425
2509
|
let dtx = lastPose[0] - firstPose[0]
|
|
@@ -2431,14 +2515,7 @@ public final class IncrementalStitcher: NSObject {
|
|
|
2431
2515
|
// the only signal we have.
|
|
2432
2516
|
let tMeters = max(tPose, imuTranslationMetres)
|
|
2433
2517
|
// Rotation magnitude — angle between camera-forward vectors.
|
|
2434
|
-
|
|
2435
|
-
let fwdFirst = qrotForwardZneg(
|
|
2436
|
-
firstPose[3], firstPose[4], firstPose[5], firstPose[6])
|
|
2437
|
-
let fwdLast = qrotForwardZneg(
|
|
2438
|
-
lastPose[3], lastPose[4], lastPose[5], lastPose[6])
|
|
2439
|
-
let dot = max(-1.0, min(1.0,
|
|
2440
|
-
fwdFirst.0 * fwdLast.0 + fwdFirst.1 * fwdLast.1 + fwdFirst.2 * fwdLast.2))
|
|
2441
|
-
let rRadians = acos(dot)
|
|
2518
|
+
let rRadians = rotationRadians(first: firstPose, last: lastPose)
|
|
2442
2519
|
// Normalisation: 10 cm of translation ≈ 1 rad of rotation as
|
|
2443
2520
|
// "equivalent magnitude" for the ratio. Shelf scans cover
|
|
2444
2521
|
// ~30 cm translation with ~10° (0.17 rad) rotation:
|
|
@@ -2448,13 +2525,61 @@ public final class IncrementalStitcher: NSObject {
|
|
|
2448
2525
|
let tScore = tMeters / 0.10
|
|
2449
2526
|
let rScore = rRadians / 1.00
|
|
2450
2527
|
let denom = tScore + rScore
|
|
2451
|
-
if denom <= 1e-9 { return "panorama" } // no motion either way
|
|
2528
|
+
if denom <= 1e-9 { return ("panorama", rRadians, tMeters, 0.0) } // no motion either way
|
|
2452
2529
|
let ratio = tScore / denom
|
|
2530
|
+
|
|
2531
|
+
// 2026-06-15 — LOW-ROTATION GUARD. The gyro rotation (rRadians) is
|
|
2532
|
+
// trustworthy; the IMU translation (tMeters, in non-AR) is NOT — a
|
|
2533
|
+
// continuous rotation leaks gravity into the double-integrated accel and
|
|
2534
|
+
// inflates it, which can falsely push `ratio` over 0.55 → SCANS, whose
|
|
2535
|
+
// affine warper can't represent the rotation. When the gyro shows a
|
|
2536
|
+
// clear pan (> ~20°) with only modest translation, force PANORAMA
|
|
2537
|
+
// regardless of the (possibly-inflated) translation. Genuine shelf
|
|
2538
|
+
// scans (low rotation, large real translation) skip this and still
|
|
2539
|
+
// reach SCANS via the ratio.
|
|
2540
|
+
let lowRotationGuard = rRadians > 0.35 && tMeters < 0.25
|
|
2541
|
+
let mode = (!lowRotationGuard && ratio >= 0.55) ? "scans" : "panorama"
|
|
2453
2542
|
os_log(.fault, log: Self.diagLog,
|
|
2454
|
-
"[stitchMode.auto] tPose=%.3fm tImu=%.3fm r=%.3frad ratio=%.3f → %{public}@",
|
|
2543
|
+
"[stitchMode.auto] tPose=%.3fm tImu=%.3fm r=%.3frad ratio=%.3f rotGuard=%d → %{public}@",
|
|
2455
2544
|
tPose, imuTranslationMetres, rRadians, ratio,
|
|
2456
|
-
|
|
2457
|
-
return
|
|
2545
|
+
lowRotationGuard ? 1 : 0, mode)
|
|
2546
|
+
return (mode, rRadians, tMeters, ratio)
|
|
2547
|
+
}
|
|
2548
|
+
|
|
2549
|
+
/// 2026-06-16 — high-level warper decision tree (mirrors Android's
|
|
2550
|
+
/// pickHighLevelWarper). The pipeline is now ALWAYS high-level cv::Stitcher
|
|
2551
|
+
/// PANORAMA. Warper is a pure function of (lens, pan direction); the
|
|
2552
|
+
/// rotation-vs-translation (ex-SCANS) distinction was DROPPED as redundant —
|
|
2553
|
+
/// at 1x the same direction-based warpers serve both, and 0.5x is always
|
|
2554
|
+
/// spherical. orientation = capture hold ("landscape*" = Mode A vertical
|
|
2555
|
+
/// pan; else Mode B horizontal); lens = the EXPLICIT lens ("0.5x" | "1x").
|
|
2556
|
+
///
|
|
2557
|
+
/// 0.5x ultra-wide → spherical (bounded both axes; any pan)
|
|
2558
|
+
/// 1x + Mode A (vertical) → plane
|
|
2559
|
+
/// 1x + Mode B (horizontal) → cylindrical
|
|
2560
|
+
///
|
|
2561
|
+
/// Quality-preferred warper; the C++ memory ladder force-falls to spherical
|
|
2562
|
+
/// (and downscales compositingResol) under pressure.
|
|
2563
|
+
private func pickHighLevelWarper(
|
|
2564
|
+
orientation: String,
|
|
2565
|
+
lens: String
|
|
2566
|
+
) -> String {
|
|
2567
|
+
if lens == "0.5x" { return "spherical" } // ultra-wide → always spherical
|
|
2568
|
+
let verticalPanModeA = orientation.hasPrefix("landscape")
|
|
2569
|
+
return verticalPanModeA ? "plane" : "cylindrical" // 1x: A→plane, B→cylindrical
|
|
2570
|
+
}
|
|
2571
|
+
|
|
2572
|
+
/// Gyro rotation magnitude (radians) between two 7-element poses
|
|
2573
|
+
/// `[tx,ty,tz,qx,qy,qz,qw]` — angle between camera-forward vectors.
|
|
2574
|
+
/// Returns 0.0 if either pose is missing/malformed (non-AR, no pose).
|
|
2575
|
+
/// Shared by `resolveStitchModeAuto` + the finalize `rRadians` readout (DRY).
|
|
2576
|
+
private func rotationRadians(first: [Double]?, last: [Double]?) -> Double {
|
|
2577
|
+
guard let f = first, f.count == 7, let l = last, l.count == 7 else { return 0.0 }
|
|
2578
|
+
let fwdFirst = qrotForwardZneg(f[3], f[4], f[5], f[6])
|
|
2579
|
+
let fwdLast = qrotForwardZneg(l[3], l[4], l[5], l[6])
|
|
2580
|
+
let dot = max(-1.0, min(1.0,
|
|
2581
|
+
fwdFirst.0 * fwdLast.0 + fwdFirst.1 * fwdLast.1 + fwdFirst.2 * fwdLast.2))
|
|
2582
|
+
return acos(dot)
|
|
2458
2583
|
}
|
|
2459
2584
|
|
|
2460
2585
|
/// Closed-form q · (0,0,-1) · q⁻¹ — rotates the camera-forward
|