react-native-image-stitcher 0.15.2 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +124 -1
- package/README.md +116 -5
- package/android/src/main/cpp/image_stitcher_jni.cpp +107 -11
- package/android/src/main/java/io/imagestitcher/rn/BatchStitcher.kt +223 -1
- package/android/src/main/java/io/imagestitcher/rn/IncrementalStitcher.kt +87 -30
- package/android/src/main/java/io/imagestitcher/rn/KeyframeGate.kt +1 -1
- 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/stitcher.cpp +651 -55
- package/cpp/stitcher.hpp +10 -0
- package/cpp/warp_guard.hpp +212 -0
- package/dist/camera/Camera.d.ts +196 -12
- package/dist/camera/Camera.js +629 -35
- 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 +142 -0
- package/dist/camera/CaptureMemoryPill.d.ts +9 -1
- package/dist/camera/CaptureMemoryPill.js +3 -3
- 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/PanoramaSettings.d.ts +8 -6
- package/dist/camera/PanoramaSettings.js +26 -5
- package/dist/camera/PanoramaSettingsModal.js +4 -4
- package/dist/camera/RectCropPreview.d.ts +161 -0
- package/dist/camera/RectCropPreview.js +480 -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 +45 -0
- package/ios/Sources/RNImageStitcher/IncrementalStitcher.swift +56 -8
- 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 +191 -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 +994 -47
- package/src/camera/CameraView.tsx +48 -16
- package/src/camera/CaptureCountdownOverlay.tsx +272 -0
- package/src/camera/CaptureFrameCounterOverlay.tsx +183 -0
- package/src/camera/CaptureMemoryPill.tsx +17 -3
- 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/PanoramaSettings.ts +34 -11
- package/src/camera/PanoramaSettingsModal.tsx +4 -4
- package/src/camera/RectCropPreview.tsx +820 -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 +45 -0
- 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/CHANGELOG.md
CHANGED
|
@@ -14,7 +14,130 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
14
14
|
> during 0.x are bumped to a new MINOR (e.g., 0.1 → 0.2), and the
|
|
15
15
|
> upgrade path is documented in this CHANGELOG.
|
|
16
16
|
|
|
17
|
-
## [
|
|
17
|
+
## [0.16.0] — 2026-06-15
|
|
18
|
+
|
|
19
|
+
### Added — first-time-user panorama capture GUIDANCE
|
|
20
|
+
|
|
21
|
+
A set of opt-in-by-default guidance surfaces that coach the operator
|
|
22
|
+
through a non-AR hold-and-pan panorama. All seven are wired into
|
|
23
|
+
`<Camera>` automatically and read directly from new props (none are
|
|
24
|
+
threaded through `PanoramaSettings`):
|
|
25
|
+
|
|
26
|
+
1. **Mode gate + 2. rotate-to-landscape prompt.** Starting a panorama
|
|
27
|
+
while the phone is held portrait under Mode A is blocked behind a
|
|
28
|
+
"Rotate to landscape" caption; the capture starts the instant the
|
|
29
|
+
user rotates to landscape (either way up). Releasing the shutter
|
|
30
|
+
before rotating cancels the pending start.
|
|
31
|
+
3. **Pan how-to overlay.** A brief code-drawn looping graphic (phone +
|
|
32
|
+
sweeping band) + bouncing direction arrow (down for landscape Mode A,
|
|
33
|
+
right for portrait Mode B) shown for ~2.5 s at the start of each
|
|
34
|
+
recording.
|
|
35
|
+
4. **"Moving too fast" pill.** A transient amber pill while the gyro
|
|
36
|
+
pan rate exceeds the warn threshold.
|
|
37
|
+
5. **Blinking countdown + auto-finalize.** A blinking whole-seconds
|
|
38
|
+
countdown; at 0 the capture auto-finalizes (stitches what was
|
|
39
|
+
captured — same path as releasing the shutter).
|
|
40
|
+
6. **Lateral-drift stop.** If the operator drifts sideways out of the
|
|
41
|
+
pan plane beyond the budget, the capture FINALIZES what was captured
|
|
42
|
+
and a one-button popup explains why.
|
|
43
|
+
7. **Post-stitch review surface.** Optional. `rectCrop` shows a
|
|
44
|
+
draggable-quad crop editor (drag four corners; confirm perspective-
|
|
45
|
+
rectifies in place via `cv::warpPerspective` when the quad isn't
|
|
46
|
+
axis-aligned, "Use original" emits un-cropped, "Retake" discards).
|
|
47
|
+
`showPreview` shows the same screen with NO crop box — just the
|
|
48
|
+
stitched image with [Retake]/[Confirm]. With both off, `onCapture`
|
|
49
|
+
fires immediately.
|
|
50
|
+
|
|
51
|
+
New `<Camera>` props (all optional): `panMode`, `panGuidance`
|
|
52
|
+
(default `true`), `maxPanDurationMs` (default `9000`; `0` disables the
|
|
53
|
+
countdown + auto-finalize), `panTooFastThreshold`, `lateralBudgetCm`
|
|
54
|
+
(default `5`; `0` disables the lateral stop), `rectCrop`
|
|
55
|
+
(default `false`), `showPreview` (default `false`), and
|
|
56
|
+
`guidanceCopy` (partial override of every guidance string). A skewed
|
|
57
|
+
crop quad is always perspective-rectified (there is no opt-out flag).
|
|
58
|
+
|
|
59
|
+
New public exports: the `PanMode` type, `GuidanceCopy` +
|
|
60
|
+
`DEFAULT_GUIDANCE_COPY`, the `usePanMotion` hook, the five guidance
|
|
61
|
+
components (`RotateToLandscapePrompt`, `PanHowToOverlay`,
|
|
62
|
+
`CaptureCountdownOverlay`, `LateralMotionModal`, `RectCropPreview`) with
|
|
63
|
+
their prop types, and the `cropQuad` perspective-rectify helper.
|
|
64
|
+
|
|
65
|
+
### Added — capture hardening
|
|
66
|
+
|
|
67
|
+
Follow-up hardening on top of the guidance set, driven by on-device
|
|
68
|
+
testing:
|
|
69
|
+
|
|
70
|
+
- **Guidance graphics are now code-drawn, not GIFs.** The rotate-to-
|
|
71
|
+
landscape and pan-capture animations are rendered with pure RN
|
|
72
|
+
`View` + `Animated` (`guidanceGraphics.tsx`) — resolution-independent
|
|
73
|
+
(no pixelation on high-density screens) and themeable via
|
|
74
|
+
`GUIDANCE_TOKENS`. Removes the bundled GIF assets AND the Android
|
|
75
|
+
host's previous need to add Fresco's `animated-gif` module.
|
|
76
|
+
- **Crop editor seeds from the max-inscribed rectangle.** With
|
|
77
|
+
`rectCrop`, the draggable quad now opens on the tightest clean
|
|
78
|
+
rectangle (native `computeInscribedRect`) instead of a blind 8 %
|
|
79
|
+
inset, and the editor gains an explicit **"Use original"** button
|
|
80
|
+
(emit the stitch un-cropped) plus a warning banner. When the editor is
|
|
81
|
+
on, the native auto-crop is forced off so the full bordered panorama
|
|
82
|
+
is available to drag.
|
|
83
|
+
- **`onCapture` carries `warnings`.** Both success and failure results
|
|
84
|
+
include `warnings: CaptureWarning[]` — `LOW_FRAME_UTILIZATION` (<70 %
|
|
85
|
+
of captured frames used) and `LATERAL_DRIFT_FINALIZE`. New exports:
|
|
86
|
+
`CaptureWarning`, `CaptureWarningCode`, `PanoramaCaptureResult`.
|
|
87
|
+
- **Post-stitch validation.** A disjoint / fragmented stitch (frames
|
|
88
|
+
that survived confidence but didn't fuse into one panorama) is now
|
|
89
|
+
rejected with the new `STITCH_LOW_QUALITY` error code + "try again"
|
|
90
|
+
copy, instead of emitting a broken image.
|
|
91
|
+
- **Quality-driven warper.** Wide pans switch from plane to the bounded
|
|
92
|
+
cylindrical projection based on the estimated sweep angle (not only on
|
|
93
|
+
an OOM-divergence fallback), reducing end-of-pan perspective stretch.
|
|
94
|
+
- **Headroom-based memory gating.** The flat process-RSS pre-stitch
|
|
95
|
+
abort is replaced by a per-process headroom model: under memory
|
|
96
|
+
pressure the pipeline routes to the lighter STREAM+feather path rather
|
|
97
|
+
than hard-aborting, and the pre-stitch abort fires only when there's
|
|
98
|
+
no room for even a minimal stitch on top of the current footprint —
|
|
99
|
+
so a memory-heavy host app no longer trips it spuriously.
|
|
100
|
+
|
|
101
|
+
### Added — `stitcher` / `frameSelection` config as JSON-object props
|
|
102
|
+
|
|
103
|
+
`<Camera>` now accepts the full stitcher and frame-gate config as JSON
|
|
104
|
+
objects — `stitcher={{ warperType, blenderType, seamFinderType,
|
|
105
|
+
stitchMode, enableMaxInscribedRectCrop }}` and
|
|
106
|
+
`frameSelection={{ mode, maxKeyframes, overlapThreshold,
|
|
107
|
+
maxKeyframeIntervalMs, flow }}` (both partial; `flow` is deep-merged).
|
|
108
|
+
Object fields win over the matching flat `default*` props, which remain
|
|
109
|
+
supported. This is the recommended way to configure the pipeline.
|
|
110
|
+
|
|
111
|
+
### Changed (BREAKING)
|
|
112
|
+
|
|
113
|
+
- **`onCapture` is now a discriminated union keyed on `ok`.** It fires
|
|
114
|
+
once per capture attempt — on success (`ok:true`, discriminated
|
|
115
|
+
further by `type`) AND on failure (`ok:false`, carrying `error:
|
|
116
|
+
CameraError`); previously it fired only on success and failures went
|
|
117
|
+
solely to `onError`. `onError` STILL fires on failure as an unchanged
|
|
118
|
+
mirror. **Migration:** gate on `result.ok` before reading
|
|
119
|
+
`uri`/`width`/`height` — `if (!result.ok) { handle(result.error);
|
|
120
|
+
return; }`. Both branches also carry the new `warnings` array.
|
|
121
|
+
- **`<Camera>` now defaults to `panMode='vertical'` (landscape-only,
|
|
122
|
+
top→bottom panorama).** Previously the component accepted both
|
|
123
|
+
landscape and portrait holds with no gate. `panMode` options are now
|
|
124
|
+
`'vertical'` (landscape-only; portrait holds gated behind the
|
|
125
|
+
rotate-to-landscape prompt), `'horizontal'` (portrait-only, left→right;
|
|
126
|
+
landscape holds gated behind the rotate-to-portrait prompt), and
|
|
127
|
+
`'both'` (either, ungated). **Hosts that want portrait/left→right
|
|
128
|
+
panoramas pass `panMode='horizontal'` or `'both'`.**
|
|
129
|
+
- **Stitch defaults moved to more robust values.** `stitchMode` now
|
|
130
|
+
defaults to `'panorama'` (was `'auto'` — the auto-resolver's SCANS
|
|
131
|
+
branch keys off double-integrated IMU translation, which is unreliable
|
|
132
|
+
during rotation); `warperType` defaults to `'spherical'` (was
|
|
133
|
+
`'plane'` — bounds both axes, fixing fragmented wide/vertical pans);
|
|
134
|
+
and the keyframe gate is denser (`maxKeyframes` → 8, a 1.5 s
|
|
135
|
+
`maxKeyframeIntervalMs` time gate re-enabled — bounding a static/slow
|
|
136
|
+
capture to ~12 s before the 8-keyframe auto-finalize, `overlapThreshold`
|
|
137
|
+
→ 0.15). **Migration:** hosts relying on the previous behaviour set the
|
|
138
|
+
values explicitly via the new `stitcher` / `frameSelection` props (or
|
|
139
|
+
the matching flat `default*` props) — e.g. `stitcher={{ stitchMode:
|
|
140
|
+
'auto', warperType: 'plane' }}`.
|
|
18
141
|
|
|
19
142
|
## [0.15.2] — 2026-06-11
|
|
20
143
|
|
package/README.md
CHANGED
|
@@ -83,6 +83,15 @@ import {
|
|
|
83
83
|
|
|
84
84
|
export function CaptureScreen() {
|
|
85
85
|
const handleCapture = (result: CameraCaptureResult) => {
|
|
86
|
+
// `onCapture` fires on success AND failure — gate on `ok` first.
|
|
87
|
+
if (!result.ok) {
|
|
88
|
+
console.warn('capture failed:', result.error.code, result.error.message);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
// Non-fatal quality signals (e.g. <70% of frames used). Always present.
|
|
92
|
+
if (result.warnings.length > 0) {
|
|
93
|
+
console.warn('warnings:', result.warnings.map((w) => w.code));
|
|
94
|
+
}
|
|
86
95
|
if (result.type === 'photo') {
|
|
87
96
|
console.log('Photo:', result.uri, result.width, result.height);
|
|
88
97
|
} else {
|
|
@@ -98,6 +107,8 @@ export function CaptureScreen() {
|
|
|
98
107
|
return (
|
|
99
108
|
<Camera
|
|
100
109
|
onCapture={handleCapture}
|
|
110
|
+
// onError still fires on failure too (an unchanged mirror of the
|
|
111
|
+
// ok:false result above).
|
|
101
112
|
onError={(err: CameraError) => console.warn(err.code, err.message)}
|
|
102
113
|
/>
|
|
103
114
|
);
|
|
@@ -135,10 +146,13 @@ export function CaptureScreen() {
|
|
|
135
146
|
// 2. Capture history (drives the built-in thumbnail strip).
|
|
136
147
|
const [thumbnails, setThumbnails] = useState<CaptureThumbnailItem[]>([]);
|
|
137
148
|
|
|
138
|
-
// 3. Post-stitch preview modal — set on
|
|
139
|
-
const [preview, setPreview] = useState<
|
|
149
|
+
// 3. Post-stitch preview modal — set on success, cleared on close.
|
|
150
|
+
const [preview, setPreview] = useState<
|
|
151
|
+
Extract<CameraCaptureResult, { ok: true }> | null
|
|
152
|
+
>(null);
|
|
140
153
|
|
|
141
154
|
const onCapture = useCallback((result: CameraCaptureResult) => {
|
|
155
|
+
if (!result.ok) return; // failures go to onError; nothing to preview
|
|
142
156
|
setPreview(result);
|
|
143
157
|
setThumbnails((prev) => [
|
|
144
158
|
...prev,
|
|
@@ -260,12 +274,12 @@ Setting `headerTitle` renders a built-in top header; the settings gear is absorb
|
|
|
260
274
|
|
|
261
275
|
| Prop | Type | Fires / purpose |
|
|
262
276
|
|---|---|---|
|
|
263
|
-
| `onCapture` | `(result: CameraCaptureResult) => void` |
|
|
277
|
+
| `onCapture` | `(result: CameraCaptureResult) => void` | Fires once per capture attempt. **Gate on `result.ok` first** (`true` = output present, discriminated further by `result.type`; `false` carries `result.error`). Both carry `result.warnings: CaptureWarning[]` (e.g. `LOW_FRAME_UTILIZATION`). |
|
|
264
278
|
| `onCaptureSourceChange` | `(source: CaptureSource) => void` | Effective source changes (AR toggle, or 0.5× forcing non-AR). |
|
|
265
279
|
| `onLensChange` | `(lens: CameraLens) => void` | User taps the 1×/0.5× chip. |
|
|
266
280
|
| `onFramesDropped` | `(info: FramesDroppedInfo) => void` | cv::Stitcher's confidence retry dropped input frame(s). |
|
|
267
281
|
| `onCaptureAbandoned` | `(reason: 'orientation-drift') => void` | SDK auto-cancelled an in-flight capture (currently only mid-capture rotation). |
|
|
268
|
-
| `onError` | `(err: CameraError) => void` | Classified error —
|
|
282
|
+
| `onError` | `(err: CameraError) => void` | Classified error — fires on failure as an unchanged mirror of the `ok:false` `onCapture` result. See codes below. |
|
|
269
283
|
| `outputDir` | `string` | Directory for saved JPEGs. The lib creates it if missing. |
|
|
270
284
|
| `engine` | `'batch-keyframe' \| …` | Stitching engine. Default `'batch-keyframe'`; most apps leave it. |
|
|
271
285
|
| `frameProcessor` | vision-camera frame processor | Host worklet composed with first-party stitching (see [`useStitcherWorklet`](docs/camera-component.md)). Advanced. |
|
|
@@ -320,7 +334,104 @@ import { Alert } from 'react-native';
|
|
|
320
334
|
```
|
|
321
335
|
|
|
322
336
|
It lives in the SDK (not per-host) so every consumer shows the same guidance for
|
|
323
|
-
the same failure. The `example/` app uses it end-to-end.
|
|
337
|
+
the same failure. The `example/` app uses it end-to-end. To localise this copy,
|
|
338
|
+
pass an `overrides` map as the second argument — see
|
|
339
|
+
[Internationalization](#internationalization-i18n) below.
|
|
340
|
+
|
|
341
|
+
## Internationalization (i18n)
|
|
342
|
+
|
|
343
|
+
Every user-facing string the SDK can show is **overridable** — there is no
|
|
344
|
+
bundled `locale` prop. By design you supply the translated strings from your
|
|
345
|
+
own i18n catalogue (i18next, FormatJS, etc.); the SDK never ships translations
|
|
346
|
+
it can't keep in sync with your wording. There are exactly **two** surfaces, and
|
|
347
|
+
together they cover **100 %** of what a user reads:
|
|
348
|
+
|
|
349
|
+
### 1. `guidanceCopy` — everything the SDK renders on screen
|
|
350
|
+
|
|
351
|
+
A single `Partial<GuidanceCopy>` prop. Pass the keys you want to translate;
|
|
352
|
+
omitted keys fall back to the English default. This covers the rotate prompt,
|
|
353
|
+
the pan hint, the live "too fast" cue, the lateral-drift popups, the crop-editor
|
|
354
|
+
buttons, the **capture-status banner**, and the **crop-editor warning banners**:
|
|
355
|
+
|
|
356
|
+
| Key | Group | English default |
|
|
357
|
+
| --- | --- | --- |
|
|
358
|
+
| `rotateToLandscape` | rotate prompt | `Rotate to landscape` |
|
|
359
|
+
| `rotateToPortrait` | rotate prompt | `Rotate to portrait` |
|
|
360
|
+
| `panHint` | pan how-to | `Pan slowly top to bottom` |
|
|
361
|
+
| `tooFast` | speed cue | `Moving too fast — slow down` |
|
|
362
|
+
| `lateralStopTitle` / `lateralStopBody` / `lateralStopDismiss` | lateral popup (stitched) | `Keep the pan straight` / … / `Got it` |
|
|
363
|
+
| `lateralWrongDirectionTitle` / `lateralWrongDirectionBody` | lateral popup (too few frames) | `Follow the arrow` / … |
|
|
364
|
+
| `cropConfirm` / `cropReset` / `cropUseOriginal` / `cropRetake` | crop buttons | `Crop` / `Reset` / `Use original` / `Retake` |
|
|
365
|
+
| `previewConfirm` | preview-only accept button (`showPreview`) | `Confirm` |
|
|
366
|
+
| `statusRecording` | status banner | `Hold steady — pan slowly` |
|
|
367
|
+
| `statusStitching` | status banner | `Stitching panorama…` |
|
|
368
|
+
| `warnLowFrameUtilization` | crop warning **(template)** | `Only {included} of {requested} captured frames ({percent}%) could be used — …` |
|
|
369
|
+
| `warnLateralDriftFinalize` | crop warning | `Capture stopped early because the phone drifted sideways — …` |
|
|
370
|
+
| `warnHighPanSpeed` | crop warning | `The capture was taken faster than the recommended pace — …` |
|
|
371
|
+
|
|
372
|
+
> **Templates:** `warnLowFrameUtilization` is interpolated at runtime — your
|
|
373
|
+
> translation must keep the `{included}`, `{requested}` and `{percent}`
|
|
374
|
+
> placeholders (an unknown placeholder is left verbatim rather than throwing).
|
|
375
|
+
> Overriding a `warn*` key re-words **both** the on-screen banner **and** the
|
|
376
|
+
> `message` carried on `onCapture(...).warnings[]`. The matching machine-readable
|
|
377
|
+
> `code` (e.g. `HIGH_PAN_SPEED`) is always present regardless of wording, so you
|
|
378
|
+
> can also branch on the code instead of the string.
|
|
379
|
+
|
|
380
|
+
### 2. `userFacingStitchError(code, overrides?)` — the host-rendered error alert
|
|
381
|
+
|
|
382
|
+
The recoverable-stitch-error copy is rendered by **you** (in `onError`), so it's
|
|
383
|
+
localised at the call site: pass an `overrides` map (keyed by the codes in the
|
|
384
|
+
exported `RECOVERABLE_STITCH_CODES`) and any match wins over the bundled English;
|
|
385
|
+
omitted codes keep the default.
|
|
386
|
+
|
|
387
|
+
```tsx
|
|
388
|
+
import {
|
|
389
|
+
Camera,
|
|
390
|
+
userFacingStitchError,
|
|
391
|
+
RECOVERABLE_STITCH_CODES,
|
|
392
|
+
DEFAULT_GUIDANCE_COPY,
|
|
393
|
+
type GuidanceCopy,
|
|
394
|
+
type UserFacingStitchErrorOverrides,
|
|
395
|
+
} from 'react-native-image-stitcher';
|
|
396
|
+
import { Alert } from 'react-native';
|
|
397
|
+
import { useTranslation } from 'react-i18next';
|
|
398
|
+
|
|
399
|
+
function CaptureScreen() {
|
|
400
|
+
const { t } = useTranslation();
|
|
401
|
+
|
|
402
|
+
// (1) SDK-rendered copy — translate the keys you care about.
|
|
403
|
+
const guidanceCopy: Partial<GuidanceCopy> = {
|
|
404
|
+
rotateToLandscape: t('pano.rotateToLandscape'),
|
|
405
|
+
statusRecording: t('pano.statusRecording'),
|
|
406
|
+
// keep the placeholders in the template translation:
|
|
407
|
+
warnLowFrameUtilization: t('pano.warnLowFrames'), // "{included}/{requested} ({percent}%) …"
|
|
408
|
+
// …any subset; the rest stay English via DEFAULT_GUIDANCE_COPY
|
|
409
|
+
};
|
|
410
|
+
|
|
411
|
+
// (2) Host-rendered error alerts — translate by code.
|
|
412
|
+
const errorCopy: UserFacingStitchErrorOverrides = Object.fromEntries(
|
|
413
|
+
RECOVERABLE_STITCH_CODES.map((code) => [
|
|
414
|
+
code,
|
|
415
|
+
{ title: t(`pano.err.${code}.title`), message: t(`pano.err.${code}.msg`) },
|
|
416
|
+
]),
|
|
417
|
+
);
|
|
418
|
+
|
|
419
|
+
return (
|
|
420
|
+
<Camera
|
|
421
|
+
guidanceCopy={guidanceCopy}
|
|
422
|
+
onError={(err) => {
|
|
423
|
+
const friendly = userFacingStitchError(err.code, errorCopy);
|
|
424
|
+
if (friendly) Alert.alert(friendly.title, friendly.message);
|
|
425
|
+
else reportGenericError(err);
|
|
426
|
+
}}
|
|
427
|
+
/>
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
`DEFAULT_GUIDANCE_COPY` and `DEFAULT_CAPTURE_WARNING_COPY` are exported so you can
|
|
433
|
+
seed your translation catalogue from the source strings, and
|
|
434
|
+
`RECOVERABLE_STITCH_GUIDANCE` exposes the built-in error copy for the same reason.
|
|
324
435
|
|
|
325
436
|
### Migration from 0.13.x
|
|
326
437
|
|
|
@@ -28,6 +28,15 @@
|
|
|
28
28
|
|
|
29
29
|
#include <string>
|
|
30
30
|
#include <vector>
|
|
31
|
+
#include <cstdio> // /proc/self/statm read for the purge diagnostic
|
|
32
|
+
#include <unistd.h> // sysconf — device RAM for the manual-pipeline budget
|
|
33
|
+
#include <dlfcn.h> // dlsym — resolve mallopt() at runtime (API-gated; see below)
|
|
34
|
+
|
|
35
|
+
// M_PURGE (release free pages back to the OS) was added to bionic at API 28;
|
|
36
|
+
// define it for our minSdk-24 build (a harmless no-op on the older allocator).
|
|
37
|
+
#ifndef M_PURGE
|
|
38
|
+
#define M_PURGE (-101)
|
|
39
|
+
#endif
|
|
31
40
|
|
|
32
41
|
|
|
33
42
|
#define LOG_TAG "BatchStitcher.JNI"
|
|
@@ -63,6 +72,53 @@ void androidLogBridge(int level, const char* tag, const char* msg) {
|
|
|
63
72
|
__android_log_print(prio, LOG_TAG, "%s %s", tag ? tag : "", msg ? msg : "");
|
|
64
73
|
}
|
|
65
74
|
|
|
75
|
+
// 2026-06-15 — last successful stitch's debugSummary (pipe/warp/route/seam/blend).
|
|
76
|
+
// The nativeStitchFramePaths return is a jintArray which can't carry a string,
|
|
77
|
+
// so we stash it here and expose it via the lightweight nativeLastDebugSummary()
|
|
78
|
+
// getter that Kotlin calls right after a successful stitch (same thread → no
|
|
79
|
+
// concurrency). Mirrors the iOS RNStitchResult.debugSummary surface so the DEV
|
|
80
|
+
// overlay shows warp/route/seam/blend on Android too, not just mode/score.
|
|
81
|
+
std::string g_lastDebugSummary;
|
|
82
|
+
|
|
83
|
+
// Return the just-finished stitch's freed native memory to the OS. cv::Mat /
|
|
84
|
+
// the OpenCV allocator keep freed blocks in a process-wide pool, so without this
|
|
85
|
+
// the native-heap RSS baseline ratchets up ~10-15 MB per capture (dumpsys showed
|
|
86
|
+
// the creep in Native Heap, not Graphics). mallopt() was exported by bionic at
|
|
87
|
+
// API 26 but our minSdk is 24, so resolve it at runtime via dlsym and call only
|
|
88
|
+
// when present (it is on every API-26+ device, including the test A35).
|
|
89
|
+
double procRssMB() {
|
|
90
|
+
FILE* f = fopen("/proc/self/statm", "r");
|
|
91
|
+
if (f == nullptr) return -1.0;
|
|
92
|
+
long sizePages = 0, residentPages = 0;
|
|
93
|
+
const int n = fscanf(f, "%ld %ld", &sizePages, &residentPages);
|
|
94
|
+
fclose(f);
|
|
95
|
+
if (n != 2) return -1.0;
|
|
96
|
+
return static_cast<double>(residentPages)
|
|
97
|
+
* static_cast<double>(sysconf(_SC_PAGE_SIZE)) / (1024.0 * 1024.0);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
void purgeNativeAllocator() {
|
|
101
|
+
using MalloptFn = int (*)(int, int);
|
|
102
|
+
// Resolve mallopt at runtime (API-26 symbol; minSdk 24). Prefer an explicit
|
|
103
|
+
// libc.so handle — RTLD_DEFAULT from a dlopen'd .so doesn't always reach
|
|
104
|
+
// libc on Android — then fall back to RTLD_DEFAULT.
|
|
105
|
+
static MalloptFn fn = []() -> MalloptFn {
|
|
106
|
+
void* h = dlopen("libc.so", RTLD_NOLOAD | RTLD_NOW);
|
|
107
|
+
void* s = (h != nullptr) ? dlsym(h, "mallopt") : nullptr;
|
|
108
|
+
if (s == nullptr) s = dlsym(RTLD_DEFAULT, "mallopt");
|
|
109
|
+
return reinterpret_cast<MalloptFn>(s);
|
|
110
|
+
}();
|
|
111
|
+
const double before = procRssMB();
|
|
112
|
+
if (fn != nullptr) fn(M_PURGE, 0);
|
|
113
|
+
const double after = procRssMB();
|
|
114
|
+
// Diagnostic: shows whether mallopt resolved and how much RSS the purge
|
|
115
|
+
// actually returned to the OS. If mallopt=MISSING → dlsym failed; if
|
|
116
|
+
// resolved but before≈after → the residual isn't allocator-retained (real
|
|
117
|
+
// leak) and M_PURGE can't help.
|
|
118
|
+
LOGI("[memstat] purge: mallopt=%s rss %.1f -> %.1f MB",
|
|
119
|
+
(fn != nullptr) ? "ok" : "MISSING", before, after);
|
|
120
|
+
}
|
|
121
|
+
|
|
66
122
|
} // namespace
|
|
67
123
|
|
|
68
124
|
|
|
@@ -81,7 +137,8 @@ Java_io_imagestitcher_rn_BatchStitcher_nativeStitchFramePaths(
|
|
|
81
137
|
jdouble registrationResolMP,
|
|
82
138
|
jdouble seamEstimationResolMP,
|
|
83
139
|
jdouble compositingResolMP,
|
|
84
|
-
jstring stitchModeStr
|
|
140
|
+
jstring stitchModeStr,
|
|
141
|
+
jboolean useManualPipeline) {
|
|
85
142
|
|
|
86
143
|
if (framePaths == nullptr) {
|
|
87
144
|
throw_runtime(env, "framePaths is null");
|
|
@@ -116,25 +173,51 @@ Java_io_imagestitcher_rn_BatchStitcher_nativeStitchFramePaths(
|
|
|
116
173
|
? retailens::StitchMode::Panorama
|
|
117
174
|
: retailens::StitchMode::Scans;
|
|
118
175
|
|
|
119
|
-
// 2026-06-
|
|
120
|
-
//
|
|
121
|
-
//
|
|
122
|
-
//
|
|
123
|
-
// high-level path
|
|
124
|
-
//
|
|
125
|
-
|
|
126
|
-
//
|
|
127
|
-
//
|
|
128
|
-
//
|
|
176
|
+
// 2026-06-15 — pipeline is caller-selectable (mirrors iOS). The batch
|
|
177
|
+
// finalize passes useManualPipeline=true: ALL the memory/OOM hardening
|
|
178
|
+
// lives on the manual path (PreStitchMemoryAbort, RAM-aware canvas-budget
|
|
179
|
+
// downscale, STREAM/BATCH held-set routing, the black-canvas utilization
|
|
180
|
+
// guard); the high-level cv::Stitcher path calls NONE of it — so manual is
|
|
181
|
+
// both the preferred output AND the memory-safe one. The on-demand
|
|
182
|
+
// HIGH-LEVEL preview tab calls refinePanorama with useManualPipeline=false
|
|
183
|
+
// to re-stitch the captured keyframes via stock cv::Stitcher.
|
|
184
|
+
//
|
|
185
|
+
// WARPER: NOT hardcoded — cfg.warperType carries the caller's choice (set
|
|
186
|
+
// above from the JS `warperType`, which defaults to "spherical" and is
|
|
187
|
+
// settable via the ⚙️ panel / the host's `defaultWarper` prop). The JS
|
|
188
|
+
// default is the single source of truth now (mirrors iOS). Choosing "plane"
|
|
189
|
+
// re-arms the manual pipeline's dynamic plane→spherical fallback/divergence
|
|
190
|
+
// switch (they only fire when warperType != "spherical").
|
|
191
|
+
cfg.useManualPipeline = (useManualPipeline == JNI_TRUE);
|
|
192
|
+
if (cfg.warperType.empty()) cfg.warperType = "spherical";
|
|
129
193
|
if (cfg.registrationResolMP <= 0.0) {
|
|
130
194
|
cfg.registrationResolMP = 0.6;
|
|
131
195
|
}
|
|
196
|
+
// Plumb the device's physical RAM so the manual pipeline's memory budget
|
|
197
|
+
// (perProcessMemoryBudgetMB = RAM × 0.42, floored at 900 MB) scales to the
|
|
198
|
+
// ACTUAL device instead of the assumed-4GB fallback (which over-throttles a
|
|
199
|
+
// 6–8 GB phone into STREAM+feather → blurrier). iOS passes physicalMemory;
|
|
200
|
+
// on Android we read it from sysconf here (no JNI signature change needed).
|
|
201
|
+
if (cfg.availableRamMB <= 0.0) {
|
|
202
|
+
const long pages = sysconf(_SC_PHYS_PAGES);
|
|
203
|
+
const long pageSize = sysconf(_SC_PAGE_SIZE);
|
|
204
|
+
if (pages > 0 && pageSize > 0) {
|
|
205
|
+
cfg.availableRamMB =
|
|
206
|
+
static_cast<double>(pages) * static_cast<double>(pageSize)
|
|
207
|
+
/ (1024.0 * 1024.0);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
132
210
|
|
|
133
211
|
const std::string outPath = jstring_to_string(env, outputPath);
|
|
134
212
|
|
|
135
213
|
retailens::StitchResult result = retailens::stitchFramePaths(
|
|
136
214
|
paths, outPath, cfg, &androidLogBridge);
|
|
137
215
|
|
|
216
|
+
// Return the stitch's freed native memory to the OS so the native-heap RSS
|
|
217
|
+
// baseline doesn't ratchet up ~10-15 MB per capture (see purgeNativeAllocator).
|
|
218
|
+
// Applies to BOTH pipelines (they share the OpenCV/bionic allocator).
|
|
219
|
+
purgeNativeAllocator();
|
|
220
|
+
|
|
138
221
|
if (!result.success) {
|
|
139
222
|
const std::string msg = "Stitch failed: " + result.errorMessage +
|
|
140
223
|
" (code=" + std::to_string(static_cast<int>(result.errorCode)) + ")";
|
|
@@ -142,6 +225,10 @@ Java_io_imagestitcher_rn_BatchStitcher_nativeStitchFramePaths(
|
|
|
142
225
|
return nullptr;
|
|
143
226
|
}
|
|
144
227
|
|
|
228
|
+
// Stash the run's debugSummary for nativeLastDebugSummary() (jintArray
|
|
229
|
+
// can't carry a string). Read by Kotlin right after this returns.
|
|
230
|
+
g_lastDebugSummary = result.debugSummary;
|
|
231
|
+
|
|
145
232
|
// Return [width, height, framesRequested, framesIncluded, finalThresholdMilli]
|
|
146
233
|
// — same JNI return layout as the previous file (Kotlin already
|
|
147
234
|
// parses indices 0-4). The threshold is multiplied by 1000 +
|
|
@@ -157,3 +244,12 @@ Java_io_imagestitcher_rn_BatchStitcher_nativeStitchFramePaths(
|
|
|
157
244
|
env->SetIntArrayRegion(dims, 0, 5, values);
|
|
158
245
|
return dims;
|
|
159
246
|
}
|
|
247
|
+
|
|
248
|
+
// Returns the debugSummary of the most recent successful stitch (pipe/warp/
|
|
249
|
+
// route/seam/blend). Kotlin calls this right after nativeStitchFramePaths so
|
|
250
|
+
// the value is fresh (stitches are serialized on one background thread).
|
|
251
|
+
extern "C" JNIEXPORT jstring JNICALL
|
|
252
|
+
Java_io_imagestitcher_rn_BatchStitcher_nativeLastDebugSummary(
|
|
253
|
+
JNIEnv* env, jobject /*thiz*/) {
|
|
254
|
+
return env->NewStringUTF(g_lastDebugSummary.c_str());
|
|
255
|
+
}
|