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
|
@@ -14,6 +14,7 @@ import org.opencv.core.Core
|
|
|
14
14
|
import org.opencv.core.CvType
|
|
15
15
|
import org.opencv.core.Mat
|
|
16
16
|
import org.opencv.core.MatOfInt
|
|
17
|
+
import org.opencv.core.MatOfPoint2f
|
|
17
18
|
import org.opencv.core.Point
|
|
18
19
|
import org.opencv.core.Rect
|
|
19
20
|
import org.opencv.core.Scalar
|
|
@@ -108,8 +109,27 @@ class BatchStitcher(reactContext: ReactApplicationContext)
|
|
|
108
109
|
// SCANS canvas size is bounded by sum-of-frames; PANORAMA
|
|
109
110
|
// can diverge to multi-GB on translation-heavy input).
|
|
110
111
|
stitchMode: String,
|
|
112
|
+
// 2026-06-15 — pipeline picker (mirrors iOS' OpenCVStitcher
|
|
113
|
+
// `useManualPipeline:` param). true → the MANUAL cv::detail
|
|
114
|
+
// pipeline (graphcut + multiband + the full memory-guard set;
|
|
115
|
+
// the default for batch capture). false → stock high-level
|
|
116
|
+
// cv::Stitcher (the on-demand HIGH-LEVEL preview tab driven by
|
|
117
|
+
// refinePanorama). Appended LAST to match the JNI C signature
|
|
118
|
+
// in image_stitcher_jni.cpp — order/count/type must line up
|
|
119
|
+
// exactly or it's an UnsatisfiedLinkError at runtime.
|
|
120
|
+
useManualPipeline: Boolean,
|
|
111
121
|
): IntArray
|
|
112
122
|
|
|
123
|
+
// 2026-06-15 — getter for the last successful stitch's debugSummary
|
|
124
|
+
// (pipe/warp/route/seam/blend). The jintArray return of
|
|
125
|
+
// nativeStitchFramePaths can't carry a string; this fetches the value the
|
|
126
|
+
// JNI stashed. Called by stitchSync right after a successful stitch.
|
|
127
|
+
private external fun nativeLastDebugSummary(): String
|
|
128
|
+
|
|
129
|
+
/** debugSummary of the most recent stitchSync() (empty if none/failed). */
|
|
130
|
+
internal var lastDebugSummary: String = ""
|
|
131
|
+
private set
|
|
132
|
+
|
|
113
133
|
// ── Stitch frames → panorama ─────────────────────────────────
|
|
114
134
|
|
|
115
135
|
@ReactMethod
|
|
@@ -180,6 +200,9 @@ class BatchStitcher(reactContext: ReactApplicationContext)
|
|
|
180
200
|
seamEstimationResolMP,
|
|
181
201
|
compositingResolMP,
|
|
182
202
|
stitchMode,
|
|
203
|
+
// Direct @ReactMethod batch stitch → MANUAL pipeline
|
|
204
|
+
// (the memory-safe default; mirrors iOS).
|
|
205
|
+
true,
|
|
183
206
|
)
|
|
184
207
|
val duration = System.currentTimeMillis() - start
|
|
185
208
|
// 2026-05-15 (D) — dims layout from native JNI:
|
|
@@ -378,6 +401,121 @@ class BatchStitcher(reactContext: ReactApplicationContext)
|
|
|
378
401
|
}
|
|
379
402
|
}
|
|
380
403
|
|
|
404
|
+
/**
|
|
405
|
+
* item-7 — free-quad perspective crop (iOS `cropToQuadAtPath` parity).
|
|
406
|
+
*
|
|
407
|
+
* `options` carries `imagePath` + the 4 IMAGE-PIXEL corners as a flat
|
|
408
|
+
* `quad` array of 8 numbers `[tlX,tlY,trX,trY,brX,brY,blX,blY]`
|
|
409
|
+
* (ordered TL→TR→BR→BL by the JS editor's `orderQuadCorners`) +
|
|
410
|
+
* optional `quality` (default 90). Rectifies the quadrilateral to an
|
|
411
|
+
* upright rectangle (Imgproc.getPerspectiveTransform +
|
|
412
|
+
* Imgproc.warpPerspective), overwrites in place, resolves
|
|
413
|
+
* `{ width, height }`. Mirrors `cropToRect`; the JS editor chooses
|
|
414
|
+
* this when the dragged quad isn't ~axis-aligned.
|
|
415
|
+
*
|
|
416
|
+
* The destination size (averaged opposite edges) + the convex /
|
|
417
|
+
* min-area / in-bounds gate + the output-canvas OOM guard are
|
|
418
|
+
* Kotlin ports of cpp/crop_quad.hpp (quadDstRect / isQuadAcceptable)
|
|
419
|
+
* and cpp/warp_guard.hpp (canvasExceedsGuard), kept in sync with iOS
|
|
420
|
+
* per the same "duplicate stage code" convention `cropToRect` follows.
|
|
421
|
+
*/
|
|
422
|
+
@ReactMethod
|
|
423
|
+
fun cropToQuad(options: ReadableMap, promise: Promise) {
|
|
424
|
+
val imagePath = options.getString("imagePath")
|
|
425
|
+
?: return promise.reject("invalid-options", "imagePath required")
|
|
426
|
+
val quadArr = options.getArray("quad")
|
|
427
|
+
if (quadArr == null || quadArr.size() != 8) {
|
|
428
|
+
return promise.reject(
|
|
429
|
+
"invalid-options",
|
|
430
|
+
"quad must be an array of 8 numbers [tlX,tlY,trX,trY,brX,brY,blX,blY]",
|
|
431
|
+
)
|
|
432
|
+
}
|
|
433
|
+
val p = DoubleArray(8) { quadArr.getDouble(it) }
|
|
434
|
+
val quality =
|
|
435
|
+
(if (options.hasKey("quality") && !options.isNull("quality")) {
|
|
436
|
+
options.getInt("quality")
|
|
437
|
+
} else {
|
|
438
|
+
90
|
|
439
|
+
}).coerceIn(1, 100)
|
|
440
|
+
CoroutineScope(Dispatchers.Default).launch {
|
|
441
|
+
val toRelease = mutableListOf<Mat>()
|
|
442
|
+
try {
|
|
443
|
+
ensureOpenCv()
|
|
444
|
+
val cleaned = stripFileScheme(imagePath)
|
|
445
|
+
if (!File(cleaned).exists()) {
|
|
446
|
+
promise.reject("read-failed", "Image not found: $imagePath")
|
|
447
|
+
return@launch
|
|
448
|
+
}
|
|
449
|
+
val img = Imgcodecs.imread(cleaned, Imgcodecs.IMREAD_COLOR)
|
|
450
|
+
toRelease += img
|
|
451
|
+
if (img.empty()) {
|
|
452
|
+
promise.reject("read-failed", "Could not decode $imagePath")
|
|
453
|
+
return@launch
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// Geometry gate — convex, non-degenerate, inside the image.
|
|
457
|
+
if (!isQuadAcceptableForCrop(p, img.cols().toDouble(), img.rows().toDouble())) {
|
|
458
|
+
promise.reject(
|
|
459
|
+
"crop-to-quad-failed",
|
|
460
|
+
"Crop quad is degenerate (non-convex, zero-area, or out of bounds)",
|
|
461
|
+
)
|
|
462
|
+
return@launch
|
|
463
|
+
}
|
|
464
|
+
// Destination size = avg of opposite edge lengths (rounded).
|
|
465
|
+
val dstW = Math.round((quadEdge(p, 0, 1) + quadEdge(p, 3, 2)) / 2.0).toInt()
|
|
466
|
+
val dstH = Math.round((quadEdge(p, 0, 3) + quadEdge(p, 1, 2)) / 2.0).toInt()
|
|
467
|
+
// Output-canvas OOM net — same 50 MP guard the stitch uses.
|
|
468
|
+
if (dstW <= 0 || dstH <= 0 || canvasExceedsGuard(dstW.toLong(), dstH.toLong())) {
|
|
469
|
+
promise.reject(
|
|
470
|
+
"crop-to-quad-failed",
|
|
471
|
+
"Crop quad output canvas is degenerate or exceeds the size guard (${dstW}x${dstH})",
|
|
472
|
+
)
|
|
473
|
+
return@launch
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
val src = MatOfPoint2f(
|
|
477
|
+
Point(p[0], p[1]), // TL
|
|
478
|
+
Point(p[2], p[3]), // TR
|
|
479
|
+
Point(p[4], p[5]), // BR
|
|
480
|
+
Point(p[6], p[7]), // BL
|
|
481
|
+
)
|
|
482
|
+
toRelease += src
|
|
483
|
+
val dst = MatOfPoint2f(
|
|
484
|
+
Point(0.0, 0.0),
|
|
485
|
+
Point(dstW.toDouble(), 0.0),
|
|
486
|
+
Point(dstW.toDouble(), dstH.toDouble()),
|
|
487
|
+
Point(0.0, dstH.toDouble()),
|
|
488
|
+
)
|
|
489
|
+
toRelease += dst
|
|
490
|
+
val transform = Imgproc.getPerspectiveTransform(src, dst)
|
|
491
|
+
toRelease += transform
|
|
492
|
+
val warped = Mat()
|
|
493
|
+
toRelease += warped
|
|
494
|
+
Imgproc.warpPerspective(
|
|
495
|
+
img, warped, transform, Size(dstW.toDouble(), dstH.toDouble()),
|
|
496
|
+
Imgproc.INTER_LINEAR,
|
|
497
|
+
)
|
|
498
|
+
if (warped.empty()) {
|
|
499
|
+
promise.reject("crop-to-quad-failed", "Perspective warp produced an empty image")
|
|
500
|
+
return@launch
|
|
501
|
+
}
|
|
502
|
+
val params = MatOfInt(Imgcodecs.IMWRITE_JPEG_QUALITY, quality)
|
|
503
|
+
if (!Imgcodecs.imwrite(cleaned, warped, params)) {
|
|
504
|
+
promise.reject("write-failed", "Could not rewrite $imagePath")
|
|
505
|
+
return@launch
|
|
506
|
+
}
|
|
507
|
+
promise.resolve(WritableNativeMap().apply {
|
|
508
|
+
putInt("width", warped.cols())
|
|
509
|
+
putInt("height", warped.rows())
|
|
510
|
+
})
|
|
511
|
+
} catch (t: Throwable) {
|
|
512
|
+
promise.reject("crop-to-quad-failed", t.message, t)
|
|
513
|
+
} finally {
|
|
514
|
+
toRelease.forEach { it.release() }
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
381
519
|
/** Red-tint the dropped pixels; writes `<path>.mask.jpg`. Resolves `{ maskPath, width, height, excludedPercent }`. */
|
|
382
520
|
@ReactMethod
|
|
383
521
|
fun debugMaskOverlay(options: ReadableMap, promise: Promise) {
|
|
@@ -546,6 +684,79 @@ class BatchStitcher(reactContext: ReactApplicationContext)
|
|
|
546
684
|
return intArrayOf(bx, by, bw, bh)
|
|
547
685
|
}
|
|
548
686
|
|
|
687
|
+
// ── item-7 free-quad crop geometry (ports of cpp/crop_quad.hpp +
|
|
688
|
+
// cpp/warp_guard.hpp; kept in sync with iOS cropToQuad) ───────
|
|
689
|
+
|
|
690
|
+
/**
|
|
691
|
+
* Euclidean length of the edge between corners `i` and `j` in the flat
|
|
692
|
+
* `[x0,y0,x1,y1,...]` quad array `p` (corner k = (p[2k], p[2k+1])).
|
|
693
|
+
*/
|
|
694
|
+
private fun quadEdge(p: DoubleArray, i: Int, j: Int): Double {
|
|
695
|
+
val dx = p[2 * i] - p[2 * j]
|
|
696
|
+
val dy = p[2 * i + 1] - p[2 * j + 1]
|
|
697
|
+
return Math.hypot(dx, dy)
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
/**
|
|
701
|
+
* Port of cpp/crop_quad.hpp:isQuadAcceptable for the flat 8-number
|
|
702
|
+
* quad `p` ([tlX,tlY,trX,trY,brX,brY,blX,blY]). True when the quad is
|
|
703
|
+
* convex, has |area| ≥ `minArea` px², and every corner lies inside
|
|
704
|
+
* `[0..imageW]×[0..imageH]` (½-px epsilon). Mirrors the iOS gate so
|
|
705
|
+
* both platforms reject the same degenerate quads.
|
|
706
|
+
*/
|
|
707
|
+
private fun isQuadAcceptableForCrop(
|
|
708
|
+
p: DoubleArray,
|
|
709
|
+
imageW: Double,
|
|
710
|
+
imageH: Double,
|
|
711
|
+
minArea: Double = 1.0,
|
|
712
|
+
): Boolean {
|
|
713
|
+
// Convexity — all consecutive edge cross-products share one sign.
|
|
714
|
+
var sign = 0
|
|
715
|
+
for (i in 0 until 4) {
|
|
716
|
+
val ax = p[2 * i]; val ay = p[2 * i + 1]
|
|
717
|
+
val bx = p[2 * ((i + 1) % 4)]; val by = p[2 * ((i + 1) % 4) + 1]
|
|
718
|
+
val cx = p[2 * ((i + 2) % 4)]; val cy = p[2 * ((i + 2) % 4) + 1]
|
|
719
|
+
val cross = (bx - ax) * (cy - by) - (by - ay) * (cx - bx)
|
|
720
|
+
if (cross != 0.0) {
|
|
721
|
+
val s = if (cross > 0.0) 1 else -1
|
|
722
|
+
if (sign == 0) sign = s else if (s != sign) return false
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
// Min-area via the shoelace formula (|2A| ≥ 2·minArea).
|
|
726
|
+
var area2 = 0.0
|
|
727
|
+
for (i in 0 until 4) {
|
|
728
|
+
val ax = p[2 * i]; val ay = p[2 * i + 1]
|
|
729
|
+
val bx = p[2 * ((i + 1) % 4)]; val by = p[2 * ((i + 1) % 4) + 1]
|
|
730
|
+
area2 += ax * by - bx * ay
|
|
731
|
+
}
|
|
732
|
+
if (Math.abs(area2) < minArea * 2.0) return false
|
|
733
|
+
// In-bounds — every corner inside the decoded image (½-px slop).
|
|
734
|
+
if (imageW > 0.0 && imageH > 0.0) {
|
|
735
|
+
val eps = 0.5
|
|
736
|
+
for (i in 0 until 4) {
|
|
737
|
+
val x = p[2 * i]; val y = p[2 * i + 1]
|
|
738
|
+
if (x < -eps || x > imageW + eps || y < -eps || y > imageH + eps) return false
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
return true
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
/**
|
|
745
|
+
* Port of cpp/warp_guard.hpp:canvasExceedsGuard — true when a
|
|
746
|
+
* `width`×`height` output canvas is degenerate (non-positive) or
|
|
747
|
+
* strictly larger than `maxPixels` (default 50 MP, boundary inclusive).
|
|
748
|
+
* int64 area math matches the C++ so the same quads are rejected.
|
|
749
|
+
*/
|
|
750
|
+
private fun canvasExceedsGuard(
|
|
751
|
+
width: Long,
|
|
752
|
+
height: Long,
|
|
753
|
+
maxPixels: Long = 50L * 1000L * 1000L,
|
|
754
|
+
): Boolean {
|
|
755
|
+
if (width <= 0 || height <= 0) return true
|
|
756
|
+
if (width > 3_000_000_000L || height > 3_000_000_000L) return true
|
|
757
|
+
return width * height > maxPixels
|
|
758
|
+
}
|
|
759
|
+
|
|
549
760
|
// ── Internals ────────────────────────────────────────────────
|
|
550
761
|
|
|
551
762
|
/**
|
|
@@ -623,9 +834,15 @@ class BatchStitcher(reactContext: ReactApplicationContext)
|
|
|
623
834
|
// translation captures safely (PANORAMA on translation can
|
|
624
835
|
// diverge → multi-GB canvas → lmkd kill).
|
|
625
836
|
stitchMode: String = "scans",
|
|
837
|
+
// 2026-06-15 — pipeline picker (mirrors iOS' OpenCVStitcher
|
|
838
|
+
// `useManualPipeline:`). Defaults to true (MANUAL) so the
|
|
839
|
+
// batch-keyframe finalize orchestrator gets the memory-safe
|
|
840
|
+
// manual path without re-stating it. The refine/high-level
|
|
841
|
+
// path passes false to drive the stock cv::Stitcher pipeline.
|
|
842
|
+
useManualPipeline: Boolean = true,
|
|
626
843
|
): IntArray {
|
|
627
844
|
ensureNativeStitcher()
|
|
628
|
-
|
|
845
|
+
val dims = nativeStitchFramePaths(
|
|
629
846
|
framePaths,
|
|
630
847
|
outputPath,
|
|
631
848
|
jpegQuality,
|
|
@@ -638,7 +855,12 @@ class BatchStitcher(reactContext: ReactApplicationContext)
|
|
|
638
855
|
seamEstimationResolMP,
|
|
639
856
|
compositingResolMP,
|
|
640
857
|
stitchMode,
|
|
858
|
+
useManualPipeline,
|
|
641
859
|
)
|
|
860
|
+
// Capture the run's debugSummary (pipe/warp/route/seam/blend) for the
|
|
861
|
+
// DEV overlay; best-effort so a getter hiccup never fails the stitch.
|
|
862
|
+
lastDebugSummary = try { nativeLastDebugSummary() } catch (_: Throwable) { "" }
|
|
863
|
+
return dims
|
|
642
864
|
}
|
|
643
865
|
|
|
644
866
|
/**
|