react-native-image-stitcher 0.16.0 → 0.16.2
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 +80 -0
- package/README.md +41 -44
- package/android/build.gradle +34 -0
- package/android/src/main/cpp/image_stitcher_jni.cpp +52 -7
- package/android/src/main/cpp/keyframe_gate_jni.cpp +15 -8
- package/android/src/main/java/io/imagestitcher/rn/IncrementalStitcher.kt +135 -59
- package/android/src/main/java/io/imagestitcher/rn/RNSARCameraView.kt +7 -36
- package/cpp/keyframe_gate.cpp +54 -15
- package/cpp/keyframe_gate.hpp +33 -0
- package/cpp/stitcher.cpp +481 -87
- package/cpp/stitcher.hpp +52 -0
- package/dist/camera/Camera.d.ts +13 -0
- package/dist/camera/Camera.js +9 -64
- package/dist/camera/CaptureFrameCounterOverlay.js +12 -1
- package/dist/camera/CaptureMemoryPill.d.ts +15 -7
- package/dist/camera/CaptureMemoryPill.js +34 -9
- package/dist/camera/PanoramaBandOverlay.d.ts +2 -1
- package/dist/camera/PanoramaBandOverlay.js +9 -3
- package/dist/camera/PanoramaSettings.js +22 -25
- package/dist/camera/RectCropPreview.d.ts +3 -29
- package/dist/camera/RectCropPreview.js +20 -130
- package/dist/stitching/incremental.d.ts +29 -0
- package/dist/stitching/useIncrementalStitcher.d.ts +7 -1
- package/dist/stitching/useIncrementalStitcher.js +7 -1
- package/ios/Sources/RNImageStitcher/IncrementalStitcher.swift +103 -26
- package/ios/Sources/RNImageStitcher/IncrementalStitcherBridge.m +4 -0
- package/ios/Sources/RNImageStitcher/IncrementalStitcherBridge.swift +15 -0
- package/ios/Sources/RNImageStitcher/OpenCVStitcher.mm +21 -1
- package/package.json +1 -1
- package/src/camera/Camera.tsx +21 -70
- package/src/camera/CaptureFrameCounterOverlay.tsx +15 -1
- package/src/camera/CaptureMemoryPill.tsx +33 -9
- package/src/camera/PanoramaBandOverlay.tsx +9 -1
- package/src/camera/PanoramaSettings.ts +22 -25
- package/src/camera/RectCropPreview.tsx +38 -220
- package/src/stitching/incremental.ts +29 -0
- package/src/stitching/useIncrementalStitcher.ts +13 -0
- package/android/src/main/java/io/imagestitcher/rn/TransferredNV21.kt +0 -100
package/CHANGELOG.md
CHANGED
|
@@ -14,6 +14,86 @@ 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
|
+
## [0.16.2] — 2026-06-17
|
|
18
|
+
|
|
19
|
+
### Added — reuse the bundled OpenCV from your host app's native code (Android)
|
|
20
|
+
|
|
21
|
+
A host app's own native (C++/NDK) code can now reuse the **same** custom
|
|
22
|
+
OpenCV this library bundles (4.10.0, arm64-v8a) — **including `cv::Stitcher`**
|
|
23
|
+
— with no second copy of `libopencv_java4.so` in the APK.
|
|
24
|
+
|
|
25
|
+
The Android build now publishes the location of its vendored OpenCV SDK via
|
|
26
|
+
`rootProject.ext.rnisOpenCVDir` (and `rnisOpenCVAndroidSdkDir`). A consumer
|
|
27
|
+
points its `externalNativeBuild` at `-DOpenCV_DIR=${rootProject.ext.rnisOpenCVDir}`,
|
|
28
|
+
calls `find_package(OpenCV)`, and links the shared `opencv_java` (core /
|
|
29
|
+
imgproc / calib3d / … resolved at runtime from the already-shipped `.so`)
|
|
30
|
+
plus the whole-archived static `opencv_stitching` (`cv::Stitcher`). A
|
|
31
|
+
build-verified consumer ships in the example app
|
|
32
|
+
(`example/android/app/src/main/cpp/`).
|
|
33
|
+
|
|
34
|
+
This is additive — no public API or runtime-behaviour change. AGP
|
|
35
|
+
`prefabPublishing` was evaluated and is unworkable for prebuilt OpenCV
|
|
36
|
+
(prefab only exports libraries the module itself builds), so OpenCV's own
|
|
37
|
+
first-class CMake package is used instead. iOS reuse (the vendored
|
|
38
|
+
`opencv2.xcframework`) is unchanged.
|
|
39
|
+
|
|
40
|
+
### Docs
|
|
41
|
+
|
|
42
|
+
Documentation site refreshed: an easier **Getting started**, a complete
|
|
43
|
+
**`<Camera>` API** reference (every prop, the v0.16 guidance params —
|
|
44
|
+
`rectCrop` / `showPreview` / `panMode` / `panGuidance` / `maxPanDurationMs` /
|
|
45
|
+
`panTooFastThreshold` / `lateralBudgetCm` / `guidanceCopy` — and the
|
|
46
|
+
`stitcher` / `frameSelection` settings-JSON tables), a fully-loaded
|
|
47
|
+
**Complete example**, the v0.16 **Capture result & errors** union, and new
|
|
48
|
+
**Sharing OpenCV** / **Bring your own OpenCV** guides.
|
|
49
|
+
|
|
50
|
+
## [0.16.1] — 2026-06-16
|
|
51
|
+
|
|
52
|
+
### Changed — high-level `cv::Stitcher` is now the default pipeline
|
|
53
|
+
|
|
54
|
+
The batch finalize now drives OpenCV's high-level `cv::Stitcher`
|
|
55
|
+
(PANORAMA) on both platforms instead of the hand-rolled `cv::detail`
|
|
56
|
+
("manual") path. In testing it produced consistently better seams and
|
|
57
|
+
lower, more stable peak memory. This is a **behaviour change, not an
|
|
58
|
+
API change** — the public surface (`<Camera>`, the hooks, the finalize
|
|
59
|
+
options) is unchanged; only the stitched output and memory profile
|
|
60
|
+
differ.
|
|
61
|
+
|
|
62
|
+
The warper is chosen per-capture (pure function of the selected lens +
|
|
63
|
+
pan direction), always `PANORAMA`:
|
|
64
|
+
|
|
65
|
+
| Lens | Mode A (vertical pan) | Mode B (horizontal pan) |
|
|
66
|
+
| ----- | --------------------- | ----------------------- |
|
|
67
|
+
| 1× | plane | cylindrical |
|
|
68
|
+
| 0.5× | spherical | spherical |
|
|
69
|
+
|
|
70
|
+
The lens comes from the explicit `1x` / `0.5x` the user selected
|
|
71
|
+
(plumbed through the finalize options); the previous FOV-from-intrinsics
|
|
72
|
+
heuristic was unreliable on multi-camera devices and is gone, along with
|
|
73
|
+
the now-redundant rotation-vs-translation (ex-SCANS) branch.
|
|
74
|
+
|
|
75
|
+
### Added — production memory hardening on the high-level path
|
|
76
|
+
|
|
77
|
+
The OOM guards that previously only covered the manual path were ported
|
|
78
|
+
across, so the new default is memory-safe under pressure:
|
|
79
|
+
|
|
80
|
+
- pre-stitch RSS headroom abort (also works on iOS now via the
|
|
81
|
+
`phys_footprint` probe, which revives the runtime-pressure router);
|
|
82
|
+
- RAM-aware compositing resolution;
|
|
83
|
+
- two-phase `estimateTransform` → project the warp canvas → abort if
|
|
84
|
+
degenerate, downscale or route to the bounded spherical warper if
|
|
85
|
+
over budget;
|
|
86
|
+
- a full C++ catch ladder + a JNI backstop so an allocation failure can
|
|
87
|
+
no longer cross the C-ABI and abort the process;
|
|
88
|
+
- a warper→spherical rescue (high-level) with the manual `PANORAMA` ↔
|
|
89
|
+
`SCANS` mode-fallback preserved for the iOS manual callers.
|
|
90
|
+
|
|
91
|
+
### Fixed
|
|
92
|
+
|
|
93
|
+
- The native allocator is purged after each stitch, and on Android the
|
|
94
|
+
OpenCV worker pool is pinned to one thread, eliminating the per-stitch
|
|
95
|
+
RSS creep observed on the manual path.
|
|
96
|
+
|
|
17
97
|
## [0.16.0] — 2026-06-15
|
|
18
98
|
|
|
19
99
|
### Added — first-time-user panorama capture GUIDANCE
|
package/README.md
CHANGED
|
@@ -29,8 +29,8 @@ Peer dependencies (the host app provides these):
|
|
|
29
29
|
"react": ">=18.0.0",
|
|
30
30
|
"react-native": ">=0.72.0",
|
|
31
31
|
"react-native-vision-camera": ">=4.7.0",
|
|
32
|
+
"react-native-worklets-core": ">=1.3.0",
|
|
32
33
|
"react-native-sensors": ">=7.0.0",
|
|
33
|
-
"expo-sensors": ">=14.0.0",
|
|
34
34
|
"react-native-safe-area-context": ">=4.0.0"
|
|
35
35
|
}
|
|
36
36
|
```
|
|
@@ -71,50 +71,32 @@ cd android && ./gradlew :app:assembleDebug # Android
|
|
|
71
71
|
> See [Orientation support](#orientation-support) for the full story
|
|
72
72
|
> (landscape *is* supported on iOS if you need it).
|
|
73
73
|
|
|
74
|
-
The minimum:
|
|
75
|
-
`
|
|
74
|
+
The minimum: mount `<Camera>` with an `onCapture` handler. It fires once
|
|
75
|
+
per capture attempt — gate on `result.ok` before reading the output.
|
|
76
76
|
|
|
77
77
|
```tsx
|
|
78
|
-
import {
|
|
79
|
-
Camera,
|
|
80
|
-
type CameraCaptureResult,
|
|
81
|
-
type CameraError,
|
|
82
|
-
} from 'react-native-image-stitcher';
|
|
78
|
+
import { Camera, type CameraCaptureResult } from 'react-native-image-stitcher';
|
|
83
79
|
|
|
84
80
|
export function CaptureScreen() {
|
|
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
|
-
}
|
|
95
|
-
if (result.type === 'photo') {
|
|
96
|
-
console.log('Photo:', result.uri, result.width, result.height);
|
|
97
|
-
} else {
|
|
98
|
-
console.log(
|
|
99
|
-
'Panorama:',
|
|
100
|
-
result.uri,
|
|
101
|
-
`${result.framesIncluded}/${result.framesRequested} frames`,
|
|
102
|
-
`stitched as ${result.stitchModeResolved ?? 'n/a'}`,
|
|
103
|
-
);
|
|
104
|
-
}
|
|
105
|
-
};
|
|
106
|
-
|
|
107
81
|
return (
|
|
108
82
|
<Camera
|
|
109
|
-
onCapture={
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
83
|
+
onCapture={(result: CameraCaptureResult) => {
|
|
84
|
+
if (!result.ok) {
|
|
85
|
+
console.warn('capture failed:', result.error.code);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
// result.type is 'photo' or 'panorama'; both carry uri/width/height.
|
|
89
|
+
console.log(result.type, result.uri, result.width, result.height);
|
|
90
|
+
}}
|
|
113
91
|
/>
|
|
114
92
|
);
|
|
115
93
|
}
|
|
116
94
|
```
|
|
117
95
|
|
|
96
|
+
> **Camera permission is the host's job.** The SDK never requests it for
|
|
97
|
+
> you — resolve it (e.g. with vision-camera's `useCameraPermission`)
|
|
98
|
+
> before mounting `<Camera>`.
|
|
99
|
+
|
|
118
100
|
### A complete capture screen
|
|
119
101
|
|
|
120
102
|
A realistic screen: requests permission up front, shows a capture
|
|
@@ -353,21 +335,32 @@ omitted keys fall back to the English default. This covers the rotate prompt,
|
|
|
353
335
|
the pan hint, the live "too fast" cue, the lateral-drift popups, the crop-editor
|
|
354
336
|
buttons, the **capture-status banner**, and the **crop-editor warning banners**:
|
|
355
337
|
|
|
356
|
-
|
|
338
|
+
Each default is the **exact, complete** source string — translate it verbatim
|
|
339
|
+
(keep the `{included}` / `{requested}` / `{percent}` placeholders in
|
|
340
|
+
`warnLowFrameUtilization`), or `import { DEFAULT_GUIDANCE_COPY }` to seed your
|
|
341
|
+
catalogue programmatically.
|
|
342
|
+
|
|
343
|
+
| Key | Where it appears | English default (translate verbatim) |
|
|
357
344
|
| --- | --- | --- |
|
|
358
345
|
| `rotateToLandscape` | rotate prompt | `Rotate to landscape` |
|
|
359
346
|
| `rotateToPortrait` | rotate prompt | `Rotate to portrait` |
|
|
360
|
-
| `panHint` | pan how-to | `Pan slowly top to bottom` |
|
|
361
|
-
| `tooFast` | speed
|
|
362
|
-
| `lateralStopTitle`
|
|
363
|
-
| `
|
|
364
|
-
| `
|
|
365
|
-
| `
|
|
347
|
+
| `panHint` | pan how-to overlay | `Pan slowly top to bottom` |
|
|
348
|
+
| `tooFast` | speed-cue pill | `Moving too fast — slow down` |
|
|
349
|
+
| `lateralStopTitle` | lateral-drift popup (stitched) | `Keep the pan straight` |
|
|
350
|
+
| `lateralStopBody` | lateral-drift popup (stitched) | `You moved sideways. Pan in one direction only — we stitched what you captured.` |
|
|
351
|
+
| `lateralStopDismiss` | lateral-drift popup button | `Got it` |
|
|
352
|
+
| `lateralWrongDirectionTitle` | lateral-drift popup (too few frames) | `Follow the arrow` |
|
|
353
|
+
| `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.` |
|
|
354
|
+
| `cropConfirm` | crop-editor button | `Crop` |
|
|
355
|
+
| `cropReset` | crop-editor button | `Reset` |
|
|
356
|
+
| `cropUseOriginal` | crop-editor button | `Use original` |
|
|
357
|
+
| `cropRetake` | crop-editor button | `Retake` |
|
|
358
|
+
| `previewConfirm` | preview accept button (`showPreview`) | `Confirm` |
|
|
366
359
|
| `statusRecording` | status banner | `Hold steady — pan slowly` |
|
|
367
360
|
| `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 —
|
|
361
|
+
| `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.` |
|
|
362
|
+
| `warnLateralDriftFinalize` | crop warning | `Capture stopped early because the phone drifted sideways — only the part captured before the drift was stitched.` |
|
|
363
|
+
| `warnHighPanSpeed` | crop warning | `The capture was taken faster than the recommended pace — the result may not be the best. Pan more slowly next time.` |
|
|
371
364
|
|
|
372
365
|
> **Templates:** `warnLowFrameUtilization` is interpolated at runtime — your
|
|
373
366
|
> translation must keep the `{included}`, `{requested}` and `{percent}`
|
|
@@ -433,6 +426,10 @@ function CaptureScreen() {
|
|
|
433
426
|
seed your translation catalogue from the source strings, and
|
|
434
427
|
`RECOVERABLE_STITCH_GUIDANCE` exposes the built-in error copy for the same reason.
|
|
435
428
|
|
|
429
|
+
> **Full worked example** — a Spanish `es.json` catalogue (both surfaces) plus a
|
|
430
|
+
> host language-setting that switches the copy at runtime: see the
|
|
431
|
+
> [Internationalization guide](https://bhargavkanda.github.io/react-native-image-stitcher/docs/i18n#worked-example-spanish-with-a-dynamic-language-setting).
|
|
432
|
+
|
|
436
433
|
### Migration from 0.13.x
|
|
437
434
|
|
|
438
435
|
- **Removed:** the `panGuide` and `panoramaGuidance` props (the
|
package/android/build.gradle
CHANGED
|
@@ -112,6 +112,40 @@ android {
|
|
|
112
112
|
prefab true
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
+
// ── Host OpenCV reuse: why NOT prefab publishing ─────────────────
|
|
116
|
+
//
|
|
117
|
+
// Goal: let a HOST app's own native (C++/NDK) code reuse the SAME
|
|
118
|
+
// custom OpenCV (4.10.0, arm64-v8a) this AAR already bundles — both
|
|
119
|
+
// libopencv_java4.so (cv::Mat, imgproc, features2d, calib3d, flann,
|
|
120
|
+
// photo, video …) AND cv::Stitcher (which lives in the static
|
|
121
|
+
// archive libopencv_stitching.a, NOT in the fat .so).
|
|
122
|
+
//
|
|
123
|
+
// We evaluated AGP `prefabPublishing` first (the idiomatic AAR way)
|
|
124
|
+
// and it CANNOT carry this OpenCV. Empirically verified: AGP's
|
|
125
|
+
// prefab modules only export libraries the module's own
|
|
126
|
+
// externalNativeBuild PRODUCES (here: just `image_stitcher`). A
|
|
127
|
+
// prebuilt jniLib (.so) or a prebuilt static archive (.a) is not an
|
|
128
|
+
// accepted prefab `libraryName` — the configure fails with
|
|
129
|
+
// `[CXX1404] did not find implicitly required targets`. So neither
|
|
130
|
+
// libopencv_java4.so nor libopencv_stitching.a can ride a prefab.
|
|
131
|
+
//
|
|
132
|
+
// Instead we expose the bundled OpenCV's OWN first-class CMake
|
|
133
|
+
// package — which already defines every module (incl. opencv_stitching
|
|
134
|
+
// as a STATIC IMPORTED target and opencv_java as a SHARED IMPORTED
|
|
135
|
+
// target) — to consumers via a rootProject ext property. A host
|
|
136
|
+
// points its externalNativeBuild at `-DOpenCV_DIR=<that dir>`, does
|
|
137
|
+
// `find_package(OpenCV REQUIRED)`, then links the SHARED `opencv_java`
|
|
138
|
+
// (cv::Mat & friends resolved at runtime from the AAR's already-shipped
|
|
139
|
+
// libopencv_java4.so — NO second copy) plus the whole-archived STATIC
|
|
140
|
+
// `opencv_stitching` (cv::Stitcher, a small private copy since it isn't
|
|
141
|
+
// in the fat .so). This is the idiomatic Android OpenCV-consumption
|
|
142
|
+
// path. See example/android/app for the working consumer.
|
|
143
|
+
//
|
|
144
|
+
// Set unconditionally at configure time so it's readable from the
|
|
145
|
+
// host app module regardless of project evaluation order.
|
|
146
|
+
rootProject.ext.rnisOpenCVAndroidSdkDir = "$opencvSdkDir/native"
|
|
147
|
+
rootProject.ext.rnisOpenCVDir = "$opencvSdkDir/native/jni"
|
|
148
|
+
|
|
115
149
|
// ── JNI shim build path ─────────────────────────────────────────
|
|
116
150
|
// Gradle compiles cpp/image_stitcher_jni.cpp into
|
|
117
151
|
// libimage_stitcher.so for the ABIs filtered above. The shim
|
|
@@ -97,7 +97,11 @@ double procRssMB() {
|
|
|
97
97
|
* static_cast<double>(sysconf(_SC_PAGE_SIZE)) / (1024.0 * 1024.0);
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
-
|
|
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) {
|
|
101
105
|
using MalloptFn = int (*)(int, int);
|
|
102
106
|
// Resolve mallopt at runtime (API-26 symbol; minSdk 24). Prefer an explicit
|
|
103
107
|
// libc.so handle — RTLD_DEFAULT from a dlopen'd .so doesn't always reach
|
|
@@ -108,8 +112,9 @@ void purgeNativeAllocator() {
|
|
|
108
112
|
if (s == nullptr) s = dlsym(RTLD_DEFAULT, "mallopt");
|
|
109
113
|
return reinterpret_cast<MalloptFn>(s);
|
|
110
114
|
}();
|
|
111
|
-
const double before = procRssMB();
|
|
112
|
-
if (fn != nullptr) fn(M_PURGE, 0);
|
|
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;
|
|
113
118
|
const double after = procRssMB();
|
|
114
119
|
// Diagnostic: shows whether mallopt resolved and how much RSS the purge
|
|
115
120
|
// actually returned to the OS. If mallopt=MISSING → dlsym failed; if
|
|
@@ -117,6 +122,7 @@ void purgeNativeAllocator() {
|
|
|
117
122
|
// leak) and M_PURGE can't help.
|
|
118
123
|
LOGI("[memstat] purge: mallopt=%s rss %.1f -> %.1f MB",
|
|
119
124
|
(fn != nullptr) ? "ok" : "MISSING", before, after);
|
|
125
|
+
return after; // memFloor — the post-purge plateau metric
|
|
120
126
|
}
|
|
121
127
|
|
|
122
128
|
} // namespace
|
|
@@ -189,6 +195,9 @@ Java_io_imagestitcher_rn_BatchStitcher_nativeStitchFramePaths(
|
|
|
189
195
|
// re-arms the manual pipeline's dynamic plane→spherical fallback/divergence
|
|
190
196
|
// switch (they only fire when warperType != "spherical").
|
|
191
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);
|
|
192
201
|
if (cfg.warperType.empty()) cfg.warperType = "spherical";
|
|
193
202
|
if (cfg.registrationResolMP <= 0.0) {
|
|
194
203
|
cfg.registrationResolMP = 0.6;
|
|
@@ -210,13 +219,49 @@ Java_io_imagestitcher_rn_BatchStitcher_nativeStitchFramePaths(
|
|
|
210
219
|
|
|
211
220
|
const std::string outPath = jstring_to_string(env, outputPath);
|
|
212
221
|
|
|
213
|
-
|
|
214
|
-
|
|
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
|
+
}
|
|
215
238
|
|
|
216
239
|
// Return the stitch's freed native memory to the OS so the native-heap RSS
|
|
217
240
|
// baseline doesn't ratchet up ~10-15 MB per capture (see purgeNativeAllocator).
|
|
218
|
-
// Applies to BOTH pipelines (they share the OpenCV/bionic allocator).
|
|
219
|
-
|
|
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
|
+
}
|
|
220
265
|
|
|
221
266
|
if (!result.success) {
|
|
222
267
|
const std::string msg = "Stitch failed: " + result.errorMessage +
|
|
@@ -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
|