react-native-image-stitcher 0.16.0 → 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.
Files changed (37) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/README.md +25 -10
  3. package/android/src/main/cpp/image_stitcher_jni.cpp +52 -7
  4. package/android/src/main/cpp/keyframe_gate_jni.cpp +15 -8
  5. package/android/src/main/java/io/imagestitcher/rn/IncrementalStitcher.kt +135 -59
  6. package/android/src/main/java/io/imagestitcher/rn/RNSARCameraView.kt +7 -36
  7. package/cpp/keyframe_gate.cpp +54 -15
  8. package/cpp/keyframe_gate.hpp +33 -0
  9. package/cpp/stitcher.cpp +481 -87
  10. package/cpp/stitcher.hpp +52 -0
  11. package/dist/camera/Camera.d.ts +13 -0
  12. package/dist/camera/Camera.js +9 -64
  13. package/dist/camera/CaptureFrameCounterOverlay.js +12 -1
  14. package/dist/camera/CaptureMemoryPill.d.ts +15 -7
  15. package/dist/camera/CaptureMemoryPill.js +34 -9
  16. package/dist/camera/PanoramaBandOverlay.d.ts +2 -1
  17. package/dist/camera/PanoramaBandOverlay.js +9 -3
  18. package/dist/camera/PanoramaSettings.js +22 -25
  19. package/dist/camera/RectCropPreview.d.ts +3 -29
  20. package/dist/camera/RectCropPreview.js +20 -130
  21. package/dist/stitching/incremental.d.ts +29 -0
  22. package/dist/stitching/useIncrementalStitcher.d.ts +7 -1
  23. package/dist/stitching/useIncrementalStitcher.js +7 -1
  24. package/ios/Sources/RNImageStitcher/IncrementalStitcher.swift +103 -26
  25. package/ios/Sources/RNImageStitcher/IncrementalStitcherBridge.m +4 -0
  26. package/ios/Sources/RNImageStitcher/IncrementalStitcherBridge.swift +15 -0
  27. package/ios/Sources/RNImageStitcher/OpenCVStitcher.mm +21 -1
  28. package/package.json +1 -1
  29. package/src/camera/Camera.tsx +21 -70
  30. package/src/camera/CaptureFrameCounterOverlay.tsx +15 -1
  31. package/src/camera/CaptureMemoryPill.tsx +33 -9
  32. package/src/camera/PanoramaBandOverlay.tsx +9 -1
  33. package/src/camera/PanoramaSettings.ts +22 -25
  34. package/src/camera/RectCropPreview.tsx +38 -220
  35. package/src/stitching/incremental.ts +29 -0
  36. package/src/stitching/useIncrementalStitcher.ts +13 -0
  37. package/android/src/main/java/io/imagestitcher/rn/TransferredNV21.kt +0 -100
@@ -356,6 +356,11 @@ struct KeyframeGate::Impl {
356
356
  // the downscale factor. Re-set whenever prevFrameGrayWork is.
357
357
  int32_t prevFrameOrigWidth = 0;
358
358
  int32_t prevFrameOrigHeight = 0;
359
+ // 2026-06-16 (audit #4) — the CURRENT frame's downscaled working image,
360
+ // produced by ingestWorkingFrame() (under the JNI pin) and consumed by
361
+ // evaluateWithWorkingMat() (after the pin is released). Empty unless an
362
+ // ingest is pending. Owned (downscaleToWorking clones / resizes).
363
+ cv::Mat pendingWorkMat;
359
364
  };
360
365
 
361
366
  // Compile-time layout check on the shared POD struct — ensures iOS
@@ -750,6 +755,45 @@ KeyframeGateDecision KeyframeGate::evaluateWithFrame(
750
755
  int32_t height,
751
756
  int32_t stride,
752
757
  int64_t monotonicNowMs)
758
+ {
759
+ // 2026-06-16 (audit #4) — thin wrapper = ingest (reads pixels) + evaluate.
760
+ // Kept for iOS / non-pinned callers; the Android JNI calls the two halves
761
+ // separately so the heavy OpenCV runs OUTSIDE the GC-pinned critical region.
762
+ ingestWorkingFrame(grayData, width, height, stride);
763
+ return evaluateWithWorkingMat(pose, latchedPlane, width, height, monotonicNowMs);
764
+ }
765
+
766
+ void KeyframeGate::ingestWorkingFrame(
767
+ const uint8_t* grayData,
768
+ int32_t width,
769
+ int32_t height,
770
+ int32_t stride)
771
+ {
772
+ Impl& s = *pImpl_;
773
+ s.pendingWorkMat.release();
774
+ // Only the Flow strategy reads pixels; disabled / force-accept / Pose ignore
775
+ // them (the Pose path is deliberately OpenCV-free). For those — and for
776
+ // invalid input — stash nothing; evaluateWithWorkingMat short-circuits
777
+ // without a frame. This is the ONLY step that touches `grayData`, so the
778
+ // JNI can ReleasePrimitiveArrayCritical immediately after it returns.
779
+ if (!s.enabled || s.forceAcceptNext || s.strategy != GateStrategy::Flow
780
+ || grayData == nullptr || width <= 0 || height <= 0 || stride < width) {
781
+ return;
782
+ }
783
+ // Non-owning view over the (pinned) bytes; downscaleToWorking returns an
784
+ // OWNED Mat (clone or resize output), so it stays valid after the pin drops.
785
+ cv::Mat currGrayFull(height, width, CV_8UC1,
786
+ const_cast<uint8_t*>(grayData),
787
+ static_cast<size_t>(stride));
788
+ s.pendingWorkMat = downscaleToWorking(currGrayFull);
789
+ }
790
+
791
+ KeyframeGateDecision KeyframeGate::evaluateWithWorkingMat(
792
+ const Pose& pose,
793
+ const PlaneTransform* latchedPlane,
794
+ int32_t origWidth,
795
+ int32_t origHeight,
796
+ int64_t monotonicNowMs)
753
797
  {
754
798
  Impl& s = *pImpl_;
755
799
  // Stash the monotonic stamp (see evaluate()). The Pose-strategy
@@ -785,19 +829,14 @@ KeyframeGateDecision KeyframeGate::evaluateWithFrame(
785
829
  return evaluate(pose, latchedPlane, s.currentNowMs);
786
830
  }
787
831
 
788
- // Flow path — wrap incoming pixel data as a non-owning cv::Mat
789
- // and downscale to working resolution. The non-owning view is
790
- // SAFE because we deep-copy (via clone) before storing on Impl.
791
- if (grayData == nullptr || width <= 0 || height <= 0 || stride < width) {
792
- // Defensive: caller forgot to supply image data despite
793
- // strategy=Flow. Fall back to pose path so we degrade
794
- // gracefully rather than crashing on a null deref.
832
+ // Flow path — operate on the working frame ingestWorkingFrame() stashed.
833
+ // Empty ingest declined (null/invalid grayData under Flow); fall back to
834
+ // the pose path so we degrade gracefully rather than deref a null frame.
835
+ cv::Mat currGrayWork = std::move(s.pendingWorkMat);
836
+ s.pendingWorkMat.release();
837
+ if (currGrayWork.empty()) {
795
838
  return evaluate(pose, latchedPlane, s.currentNowMs);
796
839
  }
797
- cv::Mat currGrayFull(height, width, CV_8UC1,
798
- const_cast<uint8_t*>(grayData),
799
- static_cast<size_t>(stride));
800
- cv::Mat currGrayWork = downscaleToWorking(currGrayFull);
801
840
 
802
841
  // §4 — first-frame accept under Flow. No prev to track against;
803
842
  // we anchor here and detect features so subsequent frames have
@@ -811,8 +850,8 @@ KeyframeGateDecision KeyframeGate::evaluateWithFrame(
811
850
  s.flowMinDistance);
812
851
  s.prevFrameGrayWork = currGrayWork; // clone-owned via downscale path
813
852
  s.prevFeatures = std::move(features);
814
- s.prevFrameOrigWidth = width;
815
- s.prevFrameOrigHeight = height;
853
+ s.prevFrameOrigWidth = origWidth;
854
+ s.prevFrameOrigHeight = origHeight;
816
855
  s.lastAcceptedPose = pose;
817
856
  s.lastAcceptSteadyMs = s.currentNowMs;
818
857
  s.acceptedCount = 1;
@@ -970,8 +1009,8 @@ KeyframeGateDecision KeyframeGate::evaluateWithFrame(
970
1009
  s.flowMinDistance);
971
1010
  s.prevFrameGrayWork = currGrayWork; // owned via downscale's clone
972
1011
  s.prevFeatures = std::move(nextFeatures);
973
- s.prevFrameOrigWidth = width;
974
- s.prevFrameOrigHeight = height;
1012
+ s.prevFrameOrigWidth = origWidth;
1013
+ s.prevFrameOrigHeight = origHeight;
975
1014
  s.lastAcceptedPose = pose;
976
1015
  s.lastAcceptSteadyMs = s.currentNowMs;
977
1016
  s.acceptedCount += 1;
@@ -248,6 +248,39 @@ public:
248
248
  int32_t stride,
249
249
  int64_t monotonicNowMs = -1);
250
250
 
251
+ // ── Split entry point (2026-06-16, audit #4) ──────────────────
252
+ //
253
+ // Android JNI pins the gray byte[] with GetPrimitiveArrayCritical, which
254
+ // blocks the GC for the pin's whole duration. Running the heavy OpenCV
255
+ // (goodFeaturesToTrack / optical flow, multi-ms) inside that window stalls
256
+ // the frame-rate producer thread and violates the JNI no-blocking-between-
257
+ // critical rule. This pair splits the work so ONLY the pinned-buffer read
258
+ // happens under the pin:
259
+ //
260
+ // ingestWorkingFrame(grayData, w, h, stride) // <- under the pin
261
+ // Builds the downscaled working frame (the only step that reads the
262
+ // pinned pixels) and stashes it INSIDE the gate. Does nothing (no
263
+ // pixel read) for Pose/disabled/force-accept/invalid input — those
264
+ // don't need the frame. cv::Mat is kept out of this header so the
265
+ // JNI needn't link OpenCV; the frame lives on the gate's Impl.
266
+ //
267
+ // evaluateWithWorkingMat(pose, plane, origW, origH) // <- pin released
268
+ // Runs the rest of the gate (the heavy OpenCV) on the stashed working
269
+ // frame, OUTSIDE the critical section. origW/origH are the ORIGINAL
270
+ // full-res dims (the working frame is downscaled).
271
+ //
272
+ // evaluateWithFrame() above is exactly ingest+evaluate in sequence — kept
273
+ // for iOS and any non-pinned caller (DRY: it delegates to these two).
274
+ void ingestWorkingFrame(const uint8_t* grayData,
275
+ int32_t width,
276
+ int32_t height,
277
+ int32_t stride);
278
+ KeyframeGateDecision evaluateWithWorkingMat(const Pose& pose,
279
+ const PlaneTransform* latchedPlane,
280
+ int32_t origWidth,
281
+ int32_t origHeight,
282
+ int64_t monotonicNowMs = -1);
283
+
251
284
  // ── State accessors (read-only, post-evaluate) ────────────────
252
285
  int32_t getAcceptedCount() const;
253
286
  int32_t getMaxCount() const;