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/CHANGELOG.md
CHANGED
|
@@ -14,7 +14,177 @@ 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.1] — 2026-06-16
|
|
18
|
+
|
|
19
|
+
### Changed — high-level `cv::Stitcher` is now the default pipeline
|
|
20
|
+
|
|
21
|
+
The batch finalize now drives OpenCV's high-level `cv::Stitcher`
|
|
22
|
+
(PANORAMA) on both platforms instead of the hand-rolled `cv::detail`
|
|
23
|
+
("manual") path. In testing it produced consistently better seams and
|
|
24
|
+
lower, more stable peak memory. This is a **behaviour change, not an
|
|
25
|
+
API change** — the public surface (`<Camera>`, the hooks, the finalize
|
|
26
|
+
options) is unchanged; only the stitched output and memory profile
|
|
27
|
+
differ.
|
|
28
|
+
|
|
29
|
+
The warper is chosen per-capture (pure function of the selected lens +
|
|
30
|
+
pan direction), always `PANORAMA`:
|
|
31
|
+
|
|
32
|
+
| Lens | Mode A (vertical pan) | Mode B (horizontal pan) |
|
|
33
|
+
| ----- | --------------------- | ----------------------- |
|
|
34
|
+
| 1× | plane | cylindrical |
|
|
35
|
+
| 0.5× | spherical | spherical |
|
|
36
|
+
|
|
37
|
+
The lens comes from the explicit `1x` / `0.5x` the user selected
|
|
38
|
+
(plumbed through the finalize options); the previous FOV-from-intrinsics
|
|
39
|
+
heuristic was unreliable on multi-camera devices and is gone, along with
|
|
40
|
+
the now-redundant rotation-vs-translation (ex-SCANS) branch.
|
|
41
|
+
|
|
42
|
+
### Added — production memory hardening on the high-level path
|
|
43
|
+
|
|
44
|
+
The OOM guards that previously only covered the manual path were ported
|
|
45
|
+
across, so the new default is memory-safe under pressure:
|
|
46
|
+
|
|
47
|
+
- pre-stitch RSS headroom abort (also works on iOS now via the
|
|
48
|
+
`phys_footprint` probe, which revives the runtime-pressure router);
|
|
49
|
+
- RAM-aware compositing resolution;
|
|
50
|
+
- two-phase `estimateTransform` → project the warp canvas → abort if
|
|
51
|
+
degenerate, downscale or route to the bounded spherical warper if
|
|
52
|
+
over budget;
|
|
53
|
+
- a full C++ catch ladder + a JNI backstop so an allocation failure can
|
|
54
|
+
no longer cross the C-ABI and abort the process;
|
|
55
|
+
- a warper→spherical rescue (high-level) with the manual `PANORAMA` ↔
|
|
56
|
+
`SCANS` mode-fallback preserved for the iOS manual callers.
|
|
57
|
+
|
|
58
|
+
### Fixed
|
|
59
|
+
|
|
60
|
+
- The native allocator is purged after each stitch, and on Android the
|
|
61
|
+
OpenCV worker pool is pinned to one thread, eliminating the per-stitch
|
|
62
|
+
RSS creep observed on the manual path.
|
|
63
|
+
|
|
64
|
+
## [0.16.0] — 2026-06-15
|
|
65
|
+
|
|
66
|
+
### Added — first-time-user panorama capture GUIDANCE
|
|
67
|
+
|
|
68
|
+
A set of opt-in-by-default guidance surfaces that coach the operator
|
|
69
|
+
through a non-AR hold-and-pan panorama. All seven are wired into
|
|
70
|
+
`<Camera>` automatically and read directly from new props (none are
|
|
71
|
+
threaded through `PanoramaSettings`):
|
|
72
|
+
|
|
73
|
+
1. **Mode gate + 2. rotate-to-landscape prompt.** Starting a panorama
|
|
74
|
+
while the phone is held portrait under Mode A is blocked behind a
|
|
75
|
+
"Rotate to landscape" caption; the capture starts the instant the
|
|
76
|
+
user rotates to landscape (either way up). Releasing the shutter
|
|
77
|
+
before rotating cancels the pending start.
|
|
78
|
+
3. **Pan how-to overlay.** A brief code-drawn looping graphic (phone +
|
|
79
|
+
sweeping band) + bouncing direction arrow (down for landscape Mode A,
|
|
80
|
+
right for portrait Mode B) shown for ~2.5 s at the start of each
|
|
81
|
+
recording.
|
|
82
|
+
4. **"Moving too fast" pill.** A transient amber pill while the gyro
|
|
83
|
+
pan rate exceeds the warn threshold.
|
|
84
|
+
5. **Blinking countdown + auto-finalize.** A blinking whole-seconds
|
|
85
|
+
countdown; at 0 the capture auto-finalizes (stitches what was
|
|
86
|
+
captured — same path as releasing the shutter).
|
|
87
|
+
6. **Lateral-drift stop.** If the operator drifts sideways out of the
|
|
88
|
+
pan plane beyond the budget, the capture FINALIZES what was captured
|
|
89
|
+
and a one-button popup explains why.
|
|
90
|
+
7. **Post-stitch review surface.** Optional. `rectCrop` shows a
|
|
91
|
+
draggable-quad crop editor (drag four corners; confirm perspective-
|
|
92
|
+
rectifies in place via `cv::warpPerspective` when the quad isn't
|
|
93
|
+
axis-aligned, "Use original" emits un-cropped, "Retake" discards).
|
|
94
|
+
`showPreview` shows the same screen with NO crop box — just the
|
|
95
|
+
stitched image with [Retake]/[Confirm]. With both off, `onCapture`
|
|
96
|
+
fires immediately.
|
|
97
|
+
|
|
98
|
+
New `<Camera>` props (all optional): `panMode`, `panGuidance`
|
|
99
|
+
(default `true`), `maxPanDurationMs` (default `9000`; `0` disables the
|
|
100
|
+
countdown + auto-finalize), `panTooFastThreshold`, `lateralBudgetCm`
|
|
101
|
+
(default `5`; `0` disables the lateral stop), `rectCrop`
|
|
102
|
+
(default `false`), `showPreview` (default `false`), and
|
|
103
|
+
`guidanceCopy` (partial override of every guidance string). A skewed
|
|
104
|
+
crop quad is always perspective-rectified (there is no opt-out flag).
|
|
105
|
+
|
|
106
|
+
New public exports: the `PanMode` type, `GuidanceCopy` +
|
|
107
|
+
`DEFAULT_GUIDANCE_COPY`, the `usePanMotion` hook, the five guidance
|
|
108
|
+
components (`RotateToLandscapePrompt`, `PanHowToOverlay`,
|
|
109
|
+
`CaptureCountdownOverlay`, `LateralMotionModal`, `RectCropPreview`) with
|
|
110
|
+
their prop types, and the `cropQuad` perspective-rectify helper.
|
|
111
|
+
|
|
112
|
+
### Added — capture hardening
|
|
113
|
+
|
|
114
|
+
Follow-up hardening on top of the guidance set, driven by on-device
|
|
115
|
+
testing:
|
|
116
|
+
|
|
117
|
+
- **Guidance graphics are now code-drawn, not GIFs.** The rotate-to-
|
|
118
|
+
landscape and pan-capture animations are rendered with pure RN
|
|
119
|
+
`View` + `Animated` (`guidanceGraphics.tsx`) — resolution-independent
|
|
120
|
+
(no pixelation on high-density screens) and themeable via
|
|
121
|
+
`GUIDANCE_TOKENS`. Removes the bundled GIF assets AND the Android
|
|
122
|
+
host's previous need to add Fresco's `animated-gif` module.
|
|
123
|
+
- **Crop editor seeds from the max-inscribed rectangle.** With
|
|
124
|
+
`rectCrop`, the draggable quad now opens on the tightest clean
|
|
125
|
+
rectangle (native `computeInscribedRect`) instead of a blind 8 %
|
|
126
|
+
inset, and the editor gains an explicit **"Use original"** button
|
|
127
|
+
(emit the stitch un-cropped) plus a warning banner. When the editor is
|
|
128
|
+
on, the native auto-crop is forced off so the full bordered panorama
|
|
129
|
+
is available to drag.
|
|
130
|
+
- **`onCapture` carries `warnings`.** Both success and failure results
|
|
131
|
+
include `warnings: CaptureWarning[]` — `LOW_FRAME_UTILIZATION` (<70 %
|
|
132
|
+
of captured frames used) and `LATERAL_DRIFT_FINALIZE`. New exports:
|
|
133
|
+
`CaptureWarning`, `CaptureWarningCode`, `PanoramaCaptureResult`.
|
|
134
|
+
- **Post-stitch validation.** A disjoint / fragmented stitch (frames
|
|
135
|
+
that survived confidence but didn't fuse into one panorama) is now
|
|
136
|
+
rejected with the new `STITCH_LOW_QUALITY` error code + "try again"
|
|
137
|
+
copy, instead of emitting a broken image.
|
|
138
|
+
- **Quality-driven warper.** Wide pans switch from plane to the bounded
|
|
139
|
+
cylindrical projection based on the estimated sweep angle (not only on
|
|
140
|
+
an OOM-divergence fallback), reducing end-of-pan perspective stretch.
|
|
141
|
+
- **Headroom-based memory gating.** The flat process-RSS pre-stitch
|
|
142
|
+
abort is replaced by a per-process headroom model: under memory
|
|
143
|
+
pressure the pipeline routes to the lighter STREAM+feather path rather
|
|
144
|
+
than hard-aborting, and the pre-stitch abort fires only when there's
|
|
145
|
+
no room for even a minimal stitch on top of the current footprint —
|
|
146
|
+
so a memory-heavy host app no longer trips it spuriously.
|
|
147
|
+
|
|
148
|
+
### Added — `stitcher` / `frameSelection` config as JSON-object props
|
|
149
|
+
|
|
150
|
+
`<Camera>` now accepts the full stitcher and frame-gate config as JSON
|
|
151
|
+
objects — `stitcher={{ warperType, blenderType, seamFinderType,
|
|
152
|
+
stitchMode, enableMaxInscribedRectCrop }}` and
|
|
153
|
+
`frameSelection={{ mode, maxKeyframes, overlapThreshold,
|
|
154
|
+
maxKeyframeIntervalMs, flow }}` (both partial; `flow` is deep-merged).
|
|
155
|
+
Object fields win over the matching flat `default*` props, which remain
|
|
156
|
+
supported. This is the recommended way to configure the pipeline.
|
|
157
|
+
|
|
158
|
+
### Changed (BREAKING)
|
|
159
|
+
|
|
160
|
+
- **`onCapture` is now a discriminated union keyed on `ok`.** It fires
|
|
161
|
+
once per capture attempt — on success (`ok:true`, discriminated
|
|
162
|
+
further by `type`) AND on failure (`ok:false`, carrying `error:
|
|
163
|
+
CameraError`); previously it fired only on success and failures went
|
|
164
|
+
solely to `onError`. `onError` STILL fires on failure as an unchanged
|
|
165
|
+
mirror. **Migration:** gate on `result.ok` before reading
|
|
166
|
+
`uri`/`width`/`height` — `if (!result.ok) { handle(result.error);
|
|
167
|
+
return; }`. Both branches also carry the new `warnings` array.
|
|
168
|
+
- **`<Camera>` now defaults to `panMode='vertical'` (landscape-only,
|
|
169
|
+
top→bottom panorama).** Previously the component accepted both
|
|
170
|
+
landscape and portrait holds with no gate. `panMode` options are now
|
|
171
|
+
`'vertical'` (landscape-only; portrait holds gated behind the
|
|
172
|
+
rotate-to-landscape prompt), `'horizontal'` (portrait-only, left→right;
|
|
173
|
+
landscape holds gated behind the rotate-to-portrait prompt), and
|
|
174
|
+
`'both'` (either, ungated). **Hosts that want portrait/left→right
|
|
175
|
+
panoramas pass `panMode='horizontal'` or `'both'`.**
|
|
176
|
+
- **Stitch defaults moved to more robust values.** `stitchMode` now
|
|
177
|
+
defaults to `'panorama'` (was `'auto'` — the auto-resolver's SCANS
|
|
178
|
+
branch keys off double-integrated IMU translation, which is unreliable
|
|
179
|
+
during rotation); `warperType` defaults to `'spherical'` (was
|
|
180
|
+
`'plane'` — bounds both axes, fixing fragmented wide/vertical pans);
|
|
181
|
+
and the keyframe gate is denser (`maxKeyframes` → 8, a 1.5 s
|
|
182
|
+
`maxKeyframeIntervalMs` time gate re-enabled — bounding a static/slow
|
|
183
|
+
capture to ~12 s before the 8-keyframe auto-finalize, `overlapThreshold`
|
|
184
|
+
→ 0.15). **Migration:** hosts relying on the previous behaviour set the
|
|
185
|
+
values explicitly via the new `stitcher` / `frameSelection` props (or
|
|
186
|
+
the matching flat `default*` props) — e.g. `stitcher={{ stitchMode:
|
|
187
|
+
'auto', warperType: 'plane' }}`.
|
|
18
188
|
|
|
19
189
|
## [0.15.2] — 2026-06-11
|
|
20
190
|
|
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,119 @@ 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
|
+
Each default is the **exact, complete** source string — translate it verbatim
|
|
357
|
+
(keep the `{included}` / `{requested}` / `{percent}` placeholders in
|
|
358
|
+
`warnLowFrameUtilization`), or `import { DEFAULT_GUIDANCE_COPY }` to seed your
|
|
359
|
+
catalogue programmatically.
|
|
360
|
+
|
|
361
|
+
| Key | Where it appears | English default (translate verbatim) |
|
|
362
|
+
| --- | --- | --- |
|
|
363
|
+
| `rotateToLandscape` | rotate prompt | `Rotate to landscape` |
|
|
364
|
+
| `rotateToPortrait` | rotate prompt | `Rotate to portrait` |
|
|
365
|
+
| `panHint` | pan how-to overlay | `Pan slowly top to bottom` |
|
|
366
|
+
| `tooFast` | speed-cue pill | `Moving too fast — slow down` |
|
|
367
|
+
| `lateralStopTitle` | lateral-drift popup (stitched) | `Keep the pan straight` |
|
|
368
|
+
| `lateralStopBody` | lateral-drift popup (stitched) | `You moved sideways. Pan in one direction only — we stitched what you captured.` |
|
|
369
|
+
| `lateralStopDismiss` | lateral-drift popup button | `Got it` |
|
|
370
|
+
| `lateralWrongDirectionTitle` | lateral-drift popup (too few frames) | `Follow the arrow` |
|
|
371
|
+
| `lateralWrongDirectionBody` | lateral-drift popup (too few frames) | `You moved the phone the wrong way. Pan slowly in the direction the arrow shows, in one straight line.` |
|
|
372
|
+
| `cropConfirm` | crop-editor button | `Crop` |
|
|
373
|
+
| `cropReset` | crop-editor button | `Reset` |
|
|
374
|
+
| `cropUseOriginal` | crop-editor button | `Use original` |
|
|
375
|
+
| `cropRetake` | crop-editor button | `Retake` |
|
|
376
|
+
| `previewConfirm` | preview accept button (`showPreview`) | `Confirm` |
|
|
377
|
+
| `statusRecording` | status banner | `Hold steady — pan slowly` |
|
|
378
|
+
| `statusStitching` | status banner | `Stitching panorama…` |
|
|
379
|
+
| `warnLowFrameUtilization` | crop warning **(template)** | `Only {included} of {requested} captured frames ({percent}%) could be used — the panorama may be incomplete. Pan more slowly and steadily next time.` |
|
|
380
|
+
| `warnLateralDriftFinalize` | crop warning | `Capture stopped early because the phone drifted sideways — only the part captured before the drift was stitched.` |
|
|
381
|
+
| `warnHighPanSpeed` | crop warning | `The capture was taken faster than the recommended pace — the result may not be the best. Pan more slowly next time.` |
|
|
382
|
+
|
|
383
|
+
> **Templates:** `warnLowFrameUtilization` is interpolated at runtime — your
|
|
384
|
+
> translation must keep the `{included}`, `{requested}` and `{percent}`
|
|
385
|
+
> placeholders (an unknown placeholder is left verbatim rather than throwing).
|
|
386
|
+
> Overriding a `warn*` key re-words **both** the on-screen banner **and** the
|
|
387
|
+
> `message` carried on `onCapture(...).warnings[]`. The matching machine-readable
|
|
388
|
+
> `code` (e.g. `HIGH_PAN_SPEED`) is always present regardless of wording, so you
|
|
389
|
+
> can also branch on the code instead of the string.
|
|
390
|
+
|
|
391
|
+
### 2. `userFacingStitchError(code, overrides?)` — the host-rendered error alert
|
|
392
|
+
|
|
393
|
+
The recoverable-stitch-error copy is rendered by **you** (in `onError`), so it's
|
|
394
|
+
localised at the call site: pass an `overrides` map (keyed by the codes in the
|
|
395
|
+
exported `RECOVERABLE_STITCH_CODES`) and any match wins over the bundled English;
|
|
396
|
+
omitted codes keep the default.
|
|
397
|
+
|
|
398
|
+
```tsx
|
|
399
|
+
import {
|
|
400
|
+
Camera,
|
|
401
|
+
userFacingStitchError,
|
|
402
|
+
RECOVERABLE_STITCH_CODES,
|
|
403
|
+
DEFAULT_GUIDANCE_COPY,
|
|
404
|
+
type GuidanceCopy,
|
|
405
|
+
type UserFacingStitchErrorOverrides,
|
|
406
|
+
} from 'react-native-image-stitcher';
|
|
407
|
+
import { Alert } from 'react-native';
|
|
408
|
+
import { useTranslation } from 'react-i18next';
|
|
409
|
+
|
|
410
|
+
function CaptureScreen() {
|
|
411
|
+
const { t } = useTranslation();
|
|
412
|
+
|
|
413
|
+
// (1) SDK-rendered copy — translate the keys you care about.
|
|
414
|
+
const guidanceCopy: Partial<GuidanceCopy> = {
|
|
415
|
+
rotateToLandscape: t('pano.rotateToLandscape'),
|
|
416
|
+
statusRecording: t('pano.statusRecording'),
|
|
417
|
+
// keep the placeholders in the template translation:
|
|
418
|
+
warnLowFrameUtilization: t('pano.warnLowFrames'), // "{included}/{requested} ({percent}%) …"
|
|
419
|
+
// …any subset; the rest stay English via DEFAULT_GUIDANCE_COPY
|
|
420
|
+
};
|
|
421
|
+
|
|
422
|
+
// (2) Host-rendered error alerts — translate by code.
|
|
423
|
+
const errorCopy: UserFacingStitchErrorOverrides = Object.fromEntries(
|
|
424
|
+
RECOVERABLE_STITCH_CODES.map((code) => [
|
|
425
|
+
code,
|
|
426
|
+
{ title: t(`pano.err.${code}.title`), message: t(`pano.err.${code}.msg`) },
|
|
427
|
+
]),
|
|
428
|
+
);
|
|
429
|
+
|
|
430
|
+
return (
|
|
431
|
+
<Camera
|
|
432
|
+
guidanceCopy={guidanceCopy}
|
|
433
|
+
onError={(err) => {
|
|
434
|
+
const friendly = userFacingStitchError(err.code, errorCopy);
|
|
435
|
+
if (friendly) Alert.alert(friendly.title, friendly.message);
|
|
436
|
+
else reportGenericError(err);
|
|
437
|
+
}}
|
|
438
|
+
/>
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
`DEFAULT_GUIDANCE_COPY` and `DEFAULT_CAPTURE_WARNING_COPY` are exported so you can
|
|
444
|
+
seed your translation catalogue from the source strings, and
|
|
445
|
+
`RECOVERABLE_STITCH_GUIDANCE` exposes the built-in error copy for the same reason.
|
|
446
|
+
|
|
447
|
+
> **Full worked example** — a Spanish `es.json` catalogue (both surfaces) plus a
|
|
448
|
+
> host language-setting that switches the copy at runtime: see the
|
|
449
|
+
> [Internationalization guide](https://bhargavkanda.github.io/react-native-image-stitcher/docs/i18n#worked-example-spanish-with-a-dynamic-language-setting).
|
|
324
450
|
|
|
325
451
|
### Migration from 0.13.x
|
|
326
452
|
|
|
@@ -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,59 @@ 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
|
+
// Returns the POST-purge RSS in MB (the leak-plateau "floor"), or -1 when the
|
|
101
|
+
// diagnostic reads are gated off. The mallopt(M_PURGE) CALL is UNCONDITIONAL —
|
|
102
|
+
// it's the leak fix; only its before/after READS + the log are gated (3A), so a
|
|
103
|
+
// release build pays nothing while debug builds surface memFloor.
|
|
104
|
+
double purgeNativeAllocator(bool profiling) {
|
|
105
|
+
using MalloptFn = int (*)(int, int);
|
|
106
|
+
// Resolve mallopt at runtime (API-26 symbol; minSdk 24). Prefer an explicit
|
|
107
|
+
// libc.so handle — RTLD_DEFAULT from a dlopen'd .so doesn't always reach
|
|
108
|
+
// libc on Android — then fall back to RTLD_DEFAULT.
|
|
109
|
+
static MalloptFn fn = []() -> MalloptFn {
|
|
110
|
+
void* h = dlopen("libc.so", RTLD_NOLOAD | RTLD_NOW);
|
|
111
|
+
void* s = (h != nullptr) ? dlsym(h, "mallopt") : nullptr;
|
|
112
|
+
if (s == nullptr) s = dlsym(RTLD_DEFAULT, "mallopt");
|
|
113
|
+
return reinterpret_cast<MalloptFn>(s);
|
|
114
|
+
}();
|
|
115
|
+
const double before = profiling ? procRssMB() : -1.0;
|
|
116
|
+
if (fn != nullptr) fn(M_PURGE, 0); // the fix — always runs
|
|
117
|
+
if (!profiling) return -1.0;
|
|
118
|
+
const double after = procRssMB();
|
|
119
|
+
// Diagnostic: shows whether mallopt resolved and how much RSS the purge
|
|
120
|
+
// actually returned to the OS. If mallopt=MISSING → dlsym failed; if
|
|
121
|
+
// resolved but before≈after → the residual isn't allocator-retained (real
|
|
122
|
+
// leak) and M_PURGE can't help.
|
|
123
|
+
LOGI("[memstat] purge: mallopt=%s rss %.1f -> %.1f MB",
|
|
124
|
+
(fn != nullptr) ? "ok" : "MISSING", before, after);
|
|
125
|
+
return after; // memFloor — the post-purge plateau metric
|
|
126
|
+
}
|
|
127
|
+
|
|
66
128
|
} // namespace
|
|
67
129
|
|
|
68
130
|
|
|
@@ -81,7 +143,8 @@ Java_io_imagestitcher_rn_BatchStitcher_nativeStitchFramePaths(
|
|
|
81
143
|
jdouble registrationResolMP,
|
|
82
144
|
jdouble seamEstimationResolMP,
|
|
83
145
|
jdouble compositingResolMP,
|
|
84
|
-
jstring stitchModeStr
|
|
146
|
+
jstring stitchModeStr,
|
|
147
|
+
jboolean useManualPipeline) {
|
|
85
148
|
|
|
86
149
|
if (framePaths == nullptr) {
|
|
87
150
|
throw_runtime(env, "framePaths is null");
|
|
@@ -116,24 +179,89 @@ Java_io_imagestitcher_rn_BatchStitcher_nativeStitchFramePaths(
|
|
|
116
179
|
? retailens::StitchMode::Panorama
|
|
117
180
|
: retailens::StitchMode::Scans;
|
|
118
181
|
|
|
119
|
-
// 2026-06-
|
|
120
|
-
//
|
|
121
|
-
//
|
|
122
|
-
//
|
|
123
|
-
// high-level path
|
|
124
|
-
//
|
|
125
|
-
|
|
126
|
-
//
|
|
127
|
-
//
|
|
128
|
-
//
|
|
182
|
+
// 2026-06-15 — pipeline is caller-selectable (mirrors iOS). The batch
|
|
183
|
+
// finalize passes useManualPipeline=true: ALL the memory/OOM hardening
|
|
184
|
+
// lives on the manual path (PreStitchMemoryAbort, RAM-aware canvas-budget
|
|
185
|
+
// downscale, STREAM/BATCH held-set routing, the black-canvas utilization
|
|
186
|
+
// guard); the high-level cv::Stitcher path calls NONE of it — so manual is
|
|
187
|
+
// both the preferred output AND the memory-safe one. The on-demand
|
|
188
|
+
// HIGH-LEVEL preview tab calls refinePanorama with useManualPipeline=false
|
|
189
|
+
// to re-stitch the captured keyframes via stock cv::Stitcher.
|
|
190
|
+
//
|
|
191
|
+
// WARPER: NOT hardcoded — cfg.warperType carries the caller's choice (set
|
|
192
|
+
// above from the JS `warperType`, which defaults to "spherical" and is
|
|
193
|
+
// settable via the ⚙️ panel / the host's `defaultWarper` prop). The JS
|
|
194
|
+
// default is the single source of truth now (mirrors iOS). Choosing "plane"
|
|
195
|
+
// re-arms the manual pipeline's dynamic plane→spherical fallback/divergence
|
|
196
|
+
// switch (they only fire when warperType != "spherical").
|
|
197
|
+
cfg.useManualPipeline = (useManualPipeline == JNI_TRUE);
|
|
198
|
+
// 2026-06-16 — memory profiling (DEV). Gated by the compile flag (debug-on,
|
|
199
|
+
// release-off); Android leaves memProbeFn null so rss_mb() uses /proc.
|
|
200
|
+
cfg.enableMemoryProfiling = (RNIS_MEMORY_PROFILING != 0);
|
|
201
|
+
if (cfg.warperType.empty()) cfg.warperType = "spherical";
|
|
129
202
|
if (cfg.registrationResolMP <= 0.0) {
|
|
130
203
|
cfg.registrationResolMP = 0.6;
|
|
131
204
|
}
|
|
205
|
+
// Plumb the device's physical RAM so the manual pipeline's memory budget
|
|
206
|
+
// (perProcessMemoryBudgetMB = RAM × 0.42, floored at 900 MB) scales to the
|
|
207
|
+
// ACTUAL device instead of the assumed-4GB fallback (which over-throttles a
|
|
208
|
+
// 6–8 GB phone into STREAM+feather → blurrier). iOS passes physicalMemory;
|
|
209
|
+
// on Android we read it from sysconf here (no JNI signature change needed).
|
|
210
|
+
if (cfg.availableRamMB <= 0.0) {
|
|
211
|
+
const long pages = sysconf(_SC_PHYS_PAGES);
|
|
212
|
+
const long pageSize = sysconf(_SC_PAGE_SIZE);
|
|
213
|
+
if (pages > 0 && pageSize > 0) {
|
|
214
|
+
cfg.availableRamMB =
|
|
215
|
+
static_cast<double>(pages) * static_cast<double>(pageSize)
|
|
216
|
+
/ (1024.0 * 1024.0);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
132
219
|
|
|
133
220
|
const std::string outPath = jstring_to_string(env, outputPath);
|
|
134
221
|
|
|
135
|
-
|
|
136
|
-
|
|
222
|
+
// 2026-06-16 (review #1) — backstop try/catch at the JNI C-ABI boundary.
|
|
223
|
+
// stitchFramePaths now has its own catch ladders (high-level + manual), so
|
|
224
|
+
// this should never fire — but a C++ exception crossing into JNI is UB
|
|
225
|
+
// (std::terminate/SIGABRT), so we NEVER let one through: convert any escape
|
|
226
|
+
// into a Java exception the Kotlin layer can catch.
|
|
227
|
+
retailens::StitchResult result;
|
|
228
|
+
try {
|
|
229
|
+
result = retailens::stitchFramePaths(
|
|
230
|
+
paths, outPath, cfg, &androidLogBridge);
|
|
231
|
+
} catch (const std::exception& e) {
|
|
232
|
+
throw_runtime(env, std::string("native stitch crashed: ") + e.what());
|
|
233
|
+
return nullptr;
|
|
234
|
+
} catch (...) {
|
|
235
|
+
throw_runtime(env, "native stitch crashed (unknown exception)");
|
|
236
|
+
return nullptr;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Return the stitch's freed native memory to the OS so the native-heap RSS
|
|
240
|
+
// baseline doesn't ratchet up ~10-15 MB per capture (see purgeNativeAllocator).
|
|
241
|
+
// Applies to BOTH pipelines (they share the OpenCV/bionic allocator). The
|
|
242
|
+
// post-purge RSS is the leak-plateau "floor" — append it to debugSummary so
|
|
243
|
+
// it rides the existing nativeLastDebugSummary() path to JS (no new bridge).
|
|
244
|
+
const double memFloor = purgeNativeAllocator(RNIS_MEMORY_PROFILING != 0);
|
|
245
|
+
if ((RNIS_MEMORY_PROFILING != 0) && memFloor >= 0.0) {
|
|
246
|
+
char fbuf[40];
|
|
247
|
+
std::snprintf(fbuf, sizeof(fbuf), ";memFloor=%.1f", memFloor);
|
|
248
|
+
if (!result.debugSummary.empty()) result.debugSummary += fbuf;
|
|
249
|
+
// 2026-06-16 — one authoritative per-stitch memory line to logcat (the
|
|
250
|
+
// sampler peak otherwise only rides debugSummary to the on-screen
|
|
251
|
+
// overlay). pipe/warp/mode lets each line be attributed to a preview
|
|
252
|
+
// tab: pipe=manual warp=plane mode=panorama = "As captured" primary;
|
|
253
|
+
// pipe=highlevel warp=plane = HL·Plane; warp=spherical = HL·Sph;
|
|
254
|
+
// mode=scans = SCANS. Grep `[memstat] record:` to harvest all of them.
|
|
255
|
+
LOGI("[memstat] record: pipe=%s warp=%s mode=%s before=%.1f peak=%.1f "
|
|
256
|
+
"after=%.1f floor=%.1f src=%s frames=%d/%d",
|
|
257
|
+
cfg.useManualPipeline ? "manual" : "highlevel",
|
|
258
|
+
cfg.warperType.c_str(),
|
|
259
|
+
(result.stitchModeUsed == retailens::StitchMode::Scans)
|
|
260
|
+
? "scans" : "panorama",
|
|
261
|
+
result.memBeforeMB, result.memPeakMB, result.memAfterMB, memFloor,
|
|
262
|
+
result.memSource.c_str(),
|
|
263
|
+
result.framesIncluded, result.framesRequested);
|
|
264
|
+
}
|
|
137
265
|
|
|
138
266
|
if (!result.success) {
|
|
139
267
|
const std::string msg = "Stitch failed: " + result.errorMessage +
|
|
@@ -142,6 +270,10 @@ Java_io_imagestitcher_rn_BatchStitcher_nativeStitchFramePaths(
|
|
|
142
270
|
return nullptr;
|
|
143
271
|
}
|
|
144
272
|
|
|
273
|
+
// Stash the run's debugSummary for nativeLastDebugSummary() (jintArray
|
|
274
|
+
// can't carry a string). Read by Kotlin right after this returns.
|
|
275
|
+
g_lastDebugSummary = result.debugSummary;
|
|
276
|
+
|
|
145
277
|
// Return [width, height, framesRequested, framesIncluded, finalThresholdMilli]
|
|
146
278
|
// — same JNI return layout as the previous file (Kotlin already
|
|
147
279
|
// parses indices 0-4). The threshold is multiplied by 1000 +
|
|
@@ -157,3 +289,12 @@ Java_io_imagestitcher_rn_BatchStitcher_nativeStitchFramePaths(
|
|
|
157
289
|
env->SetIntArrayRegion(dims, 0, 5, values);
|
|
158
290
|
return dims;
|
|
159
291
|
}
|
|
292
|
+
|
|
293
|
+
// Returns the debugSummary of the most recent successful stitch (pipe/warp/
|
|
294
|
+
// route/seam/blend). Kotlin calls this right after nativeStitchFramePaths so
|
|
295
|
+
// the value is fresh (stitches are serialized on one background thread).
|
|
296
|
+
extern "C" JNIEXPORT jstring JNICALL
|
|
297
|
+
Java_io_imagestitcher_rn_BatchStitcher_nativeLastDebugSummary(
|
|
298
|
+
JNIEnv* env, jobject /*thiz*/) {
|
|
299
|
+
return env->NewStringUTF(g_lastDebugSummary.c_str());
|
|
300
|
+
}
|
|
@@ -311,23 +311,30 @@ Java_io_imagestitcher_rn_KeyframeGate_nativeEvaluateWithFrame(
|
|
|
311
311
|
}
|
|
312
312
|
}
|
|
313
313
|
|
|
314
|
-
//
|
|
315
|
-
// GetPrimitiveArrayCritical
|
|
316
|
-
// GetByteArrayElements (may copy
|
|
317
|
-
//
|
|
318
|
-
//
|
|
319
|
-
//
|
|
314
|
+
// 2026-06-16 (audit #4) — pin the byte[] ONLY to INGEST it.
|
|
315
|
+
// GetPrimitiveArrayCritical is zero-copy (the JVM pins the GC) — preferred
|
|
316
|
+
// over GetByteArrayElements (which may copy a 2 MB Y-plane at 30-60 Hz) —
|
|
317
|
+
// but it blocks the GC for the pin's whole life. So we keep that life to a
|
|
318
|
+
// single downscale: ingestWorkingFrame() reads the pinned bytes into an
|
|
319
|
+
// OWNED working frame, we Release IMMEDIATELY, then evaluateWithWorkingMat()
|
|
320
|
+
// runs the heavy OpenCV (goodFeaturesToTrack / optical flow) with the pin
|
|
321
|
+
// already gone — no longer stalling the GC or the frame-rate producer
|
|
322
|
+
// thread. Always paired with ReleasePrimitiveArrayCritical, even on errors.
|
|
320
323
|
retailens::KeyframeGateDecision d;
|
|
321
324
|
if (grayBytes && grayWidth > 0 && grayHeight > 0 && grayStride >= grayWidth) {
|
|
322
325
|
void* raw = env->GetPrimitiveArrayCritical(grayBytes, nullptr);
|
|
323
326
|
if (raw) {
|
|
324
|
-
|
|
325
|
-
pose, planePtr,
|
|
327
|
+
gate(handle)->ingestWorkingFrame(
|
|
326
328
|
static_cast<const uint8_t*>(raw),
|
|
327
329
|
static_cast<int32_t>(grayWidth),
|
|
328
330
|
static_cast<int32_t>(grayHeight),
|
|
329
331
|
static_cast<int32_t>(grayStride));
|
|
330
332
|
env->ReleasePrimitiveArrayCritical(grayBytes, raw, JNI_ABORT);
|
|
333
|
+
// Pin released — heavy OpenCV now runs outside the critical section.
|
|
334
|
+
d = gate(handle)->evaluateWithWorkingMat(
|
|
335
|
+
pose, planePtr,
|
|
336
|
+
static_cast<int32_t>(grayWidth),
|
|
337
|
+
static_cast<int32_t>(grayHeight));
|
|
331
338
|
} else {
|
|
332
339
|
// GetPrimitiveArrayCritical failed (rare, but defensive).
|
|
333
340
|
// Fall back to pose-only path so we degrade gracefully
|