react-native-image-stitcher 0.14.2 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +131 -0
- package/README.md +35 -0
- package/RNImageStitcher.podspec +8 -7
- package/android/build.gradle +0 -16
- package/android/src/main/cpp/CMakeLists.txt +2 -63
- package/android/src/main/cpp/image_stitcher_jni.cpp +14 -0
- package/android/src/main/cpp/keyframe_gate_jni.cpp +13 -0
- package/android/src/main/java/io/imagestitcher/rn/BatchStitcher.kt +285 -3
- package/android/src/main/java/io/imagestitcher/rn/IncrementalStitcher.kt +180 -1162
- package/android/src/main/java/io/imagestitcher/rn/KeyframeGate.kt +29 -0
- package/android/src/main/java/io/imagestitcher/rn/RNImageStitcherPackage.kt +0 -4
- package/android/src/main/java/io/imagestitcher/rn/RNSARCameraView.kt +13 -64
- package/cpp/keyframe_gate.cpp +82 -23
- package/cpp/keyframe_gate.hpp +31 -2
- package/cpp/stitcher.cpp +208 -28
- package/cpp/tests/CMakeLists.txt +18 -12
- package/cpp/tests/keyframe_timebudget_test.cpp +65 -0
- package/cpp/tests/warp_guard_test.cpp +48 -0
- package/cpp/warp_guard.hpp +41 -0
- package/dist/camera/Camera.d.ts +31 -16
- package/dist/camera/Camera.js +10 -2
- package/dist/camera/CaptureStitchStatsToast.d.ts +15 -2
- package/dist/camera/CaptureStitchStatsToast.js +27 -7
- package/dist/camera/PanoramaSettings.d.ts +10 -223
- package/dist/camera/PanoramaSettings.js +6 -28
- package/dist/camera/PanoramaSettingsBridge.d.ts +1 -24
- package/dist/camera/PanoramaSettingsBridge.js +3 -102
- package/dist/camera/PanoramaSettingsModal.js +7 -1
- package/dist/camera/buildPanoramaInitialSettings.d.ts +11 -0
- package/dist/camera/buildPanoramaInitialSettings.js +4 -0
- package/dist/camera/cameraErrorMessages.d.ts +32 -0
- package/dist/camera/cameraErrorMessages.js +53 -0
- package/dist/camera/selectCaptureDevice.d.ts +5 -1
- package/dist/camera/selectCaptureDevice.js +22 -2
- package/dist/camera/useCapture.js +38 -0
- package/dist/index.d.ts +5 -8
- package/dist/index.js +11 -34
- package/dist/stitching/incremental.d.ts +1 -117
- package/dist/stitching/stitchVideo.d.ts +0 -35
- package/dist/types.d.ts +0 -87
- package/ios/Sources/RNImageStitcher/IncrementalStitcher.swift +96 -674
- package/ios/Sources/RNImageStitcher/IncrementalStitcherBridge.swift +9 -12
- package/ios/Sources/RNImageStitcher/KeyframeGate.swift +14 -0
- package/ios/Sources/RNImageStitcher/KeyframeGateBridge.h +7 -0
- package/ios/Sources/RNImageStitcher/KeyframeGateBridge.mm +6 -0
- package/ios/Sources/RNImageStitcher/OpenCVKeyframeCollector.h +2 -2
- package/ios/Sources/RNImageStitcher/OpenCVKeyframeCollector.mm +3 -3
- package/ios/Sources/RNImageStitcher/OpenCVStitcher.h +28 -60
- package/ios/Sources/RNImageStitcher/OpenCVStitcher.mm +180 -921
- package/ios/Sources/RNImageStitcher/RNSARSession.swift +10 -35
- package/ios/Sources/RNImageStitcher/Stitcher.swift +84 -35
- package/ios/Sources/RNImageStitcher/StitcherBridge.m +13 -0
- package/ios/Sources/RNImageStitcher/StitcherBridge.swift +132 -5
- package/package.json +3 -2
- package/src/camera/Camera.tsx +43 -22
- package/src/camera/CaptureStitchStatsToast.tsx +58 -14
- package/src/camera/PanoramaSettings.ts +16 -289
- package/src/camera/PanoramaSettingsBridge.ts +3 -114
- package/src/camera/PanoramaSettingsModal.tsx +14 -1
- package/src/camera/__tests__/PanoramaSettingsBridge.test.ts +3 -188
- package/src/camera/__tests__/buildPanoramaInitialSettings.test.ts +41 -0
- package/src/camera/__tests__/cameraErrorMessages.test.ts +76 -0
- package/src/camera/__tests__/selectCaptureDevice.test.ts +33 -0
- package/src/camera/buildPanoramaInitialSettings.ts +17 -0
- package/src/camera/cameraErrorMessages.ts +84 -0
- package/src/camera/selectCaptureDevice.ts +28 -3
- package/src/camera/useCapture.ts +44 -1
- package/src/index.ts +11 -40
- package/src/stitching/incremental.ts +3 -140
- package/src/stitching/stitchVideo.ts +0 -26
- package/src/types.ts +0 -95
- package/android/src/main/cpp/stitcher_jsi_install_jni.cpp +0 -227
- package/android/src/main/java/io/imagestitcher/rn/IncrementalFirstwinsEngine.kt +0 -1081
- package/android/src/main/java/io/imagestitcher/rn/StitcherJsiInstallerModule.kt +0 -103
- package/android/src/main/java/io/imagestitcher/rn/StitcherWorkletRuntime.kt +0 -256
- package/cpp/stitcher_frame_jsi.cpp +0 -214
- package/cpp/stitcher_frame_jsi.hpp +0 -108
- package/cpp/stitcher_proxy_jsi.cpp +0 -109
- package/cpp/stitcher_proxy_jsi.hpp +0 -46
- package/cpp/stitcher_worklet_dispatch.cpp +0 -103
- package/cpp/stitcher_worklet_dispatch.hpp +0 -71
- package/cpp/stitcher_worklet_registry.cpp +0 -91
- package/cpp/stitcher_worklet_registry.hpp +0 -146
- package/cpp/tests/stitcher_worklet_registry_test.cpp +0 -195
- package/dist/stitching/IncrementalStitcherView.d.ts +0 -41
- package/dist/stitching/IncrementalStitcherView.js +0 -157
- package/dist/stitching/StitcherWorkletRegistry.d.ts +0 -117
- package/dist/stitching/StitcherWorkletRegistry.js +0 -78
- package/dist/stitching/ensureStitcherProxyInstalled.d.ts +0 -8
- package/dist/stitching/ensureStitcherProxyInstalled.js +0 -81
- package/dist/stitching/useFrameProcessor.d.ts +0 -119
- package/dist/stitching/useFrameProcessor.js +0 -196
- package/dist/stitching/useFrameStream.d.ts +0 -34
- package/dist/stitching/useFrameStream.js +0 -234
- package/dist/stitching/useThrottledFrameProcessor.d.ts +0 -33
- package/dist/stitching/useThrottledFrameProcessor.js +0 -132
- package/ios/Sources/RNImageStitcher/OpenCVIncrementalStitcher.h +0 -474
- package/ios/Sources/RNImageStitcher/OpenCVIncrementalStitcher.mm +0 -1328
- package/ios/Sources/RNImageStitcher/OpenCVSlitScanStitcher.h +0 -103
- package/ios/Sources/RNImageStitcher/OpenCVSlitScanStitcher.mm +0 -3285
- package/ios/Sources/RNImageStitcher/RNSARWorkletRuntime.h +0 -128
- package/ios/Sources/RNImageStitcher/RNSARWorkletRuntime.mm +0 -313
- package/ios/Sources/RNImageStitcher/SaveFrameAsJpegPlugin.mm +0 -185
- package/ios/Sources/RNImageStitcher/StitcherFrameHostObject.h +0 -60
- package/ios/Sources/RNImageStitcher/StitcherFrameHostObject.mm +0 -214
- package/ios/Sources/RNImageStitcher/StitcherJsiInstaller.h +0 -42
- package/ios/Sources/RNImageStitcher/StitcherJsiInstaller.mm +0 -160
- package/src/stitching/IncrementalStitcherView.tsx +0 -198
- package/src/stitching/StitcherWorkletRegistry.ts +0 -156
- package/src/stitching/__tests__/StitcherWorkletRegistry.test.ts +0 -176
- package/src/stitching/__tests__/ensureStitcherProxyInstalled.test.ts +0 -94
- package/src/stitching/__tests__/useThrottledFrameProcessor.test.ts +0 -178
- package/src/stitching/ensureStitcherProxyInstalled.ts +0 -141
- package/src/stitching/useFrameProcessor.ts +0 -226
- package/src/stitching/useFrameStream.ts +0 -271
- package/src/stitching/useThrottledFrameProcessor.ts +0 -145
|
@@ -10,9 +10,16 @@ import com.facebook.react.bridge.WritableNativeMap
|
|
|
10
10
|
import kotlinx.coroutines.CoroutineScope
|
|
11
11
|
import kotlinx.coroutines.Dispatchers
|
|
12
12
|
import kotlinx.coroutines.launch
|
|
13
|
+
import org.opencv.core.Core
|
|
14
|
+
import org.opencv.core.CvType
|
|
13
15
|
import org.opencv.core.Mat
|
|
14
16
|
import org.opencv.core.MatOfInt
|
|
17
|
+
import org.opencv.core.Point
|
|
18
|
+
import org.opencv.core.Rect
|
|
19
|
+
import org.opencv.core.Scalar
|
|
20
|
+
import org.opencv.core.Size
|
|
15
21
|
import org.opencv.imgcodecs.Imgcodecs
|
|
22
|
+
import org.opencv.imgproc.Imgproc
|
|
16
23
|
import java.io.File
|
|
17
24
|
import java.util.concurrent.atomic.AtomicBoolean
|
|
18
25
|
|
|
@@ -62,9 +69,11 @@ class BatchStitcher(reactContext: ReactApplicationContext)
|
|
|
62
69
|
* | "landscape-left" | "landscape-right"
|
|
63
70
|
* (drives output bake-rotation table,
|
|
64
71
|
* mirrors iOS)
|
|
65
|
-
* @param useInscribedRectCrop
|
|
66
|
-
*
|
|
67
|
-
*
|
|
72
|
+
* @param useInscribedRectCrop when true, the cv::Stitcher path
|
|
73
|
+
* crops to the maximum inscribed
|
|
74
|
+
* rectangle of the coverage mask
|
|
75
|
+
* (choose_crop_rect in stitcher.cpp);
|
|
76
|
+
* false = looser cv::boundingRect
|
|
68
77
|
* @return [width, height] of the written JPEG
|
|
69
78
|
* @throws RuntimeException on stitch failure
|
|
70
79
|
*/
|
|
@@ -264,6 +273,279 @@ class BatchStitcher(reactContext: ReactApplicationContext)
|
|
|
264
273
|
}
|
|
265
274
|
}
|
|
266
275
|
|
|
276
|
+
// ── v0.15 inscribed-rect debug harness (iOS parity) ──────────
|
|
277
|
+
//
|
|
278
|
+
// Pure-Kotlin / OpenCV-Java twins of iOS' OpenCVStitcher
|
|
279
|
+
// computeInscribedRect / cropToRect / debugMaskOverlay. Exposing
|
|
280
|
+
// them as @ReactMethods is what makes the example app's
|
|
281
|
+
// `inscribedRectDebugAvailable()` return true so the __DEV__
|
|
282
|
+
// top-left rect-debug toggle shows on Android too. The
|
|
283
|
+
// inscribed-rect ALGORITHM is a direct port of cpp/stitcher.cpp's
|
|
284
|
+
// maxInscribedRectFromMask, duplicated in Kotlin per the same
|
|
285
|
+
// "duplicate stage code, DRY when proven" convention the sibling
|
|
286
|
+
// normaliseImage follows. Returns/params
|
|
287
|
+
// match the iOS StitcherBridge contract exactly so the shared JS
|
|
288
|
+
// (InscribedRectDebug.tsx) is platform-agnostic.
|
|
289
|
+
|
|
290
|
+
/** Resolves `{ x, y, width, height, imageWidth, imageHeight }`. */
|
|
291
|
+
@ReactMethod
|
|
292
|
+
fun computeInscribedRect(options: ReadableMap, promise: Promise) {
|
|
293
|
+
val imagePath = options.getString("imagePath")
|
|
294
|
+
?: return promise.reject("invalid-options", "imagePath required")
|
|
295
|
+
CoroutineScope(Dispatchers.Default).launch {
|
|
296
|
+
val toRelease = mutableListOf<Mat>()
|
|
297
|
+
try {
|
|
298
|
+
ensureOpenCv()
|
|
299
|
+
val cleaned = stripFileScheme(imagePath)
|
|
300
|
+
if (!File(cleaned).exists()) {
|
|
301
|
+
promise.reject("read-failed", "Image not found: $imagePath")
|
|
302
|
+
return@launch
|
|
303
|
+
}
|
|
304
|
+
val img = Imgcodecs.imread(cleaned, Imgcodecs.IMREAD_COLOR)
|
|
305
|
+
toRelease += img
|
|
306
|
+
if (img.empty()) {
|
|
307
|
+
promise.reject("read-failed", "Could not decode $imagePath")
|
|
308
|
+
return@launch
|
|
309
|
+
}
|
|
310
|
+
val mask = coverageOrBrightnessMask(img, cleaned, 1, toRelease)
|
|
311
|
+
val r = maxInscribedRect(mask) // [x, y, w, h]
|
|
312
|
+
promise.resolve(WritableNativeMap().apply {
|
|
313
|
+
putInt("x", r[0])
|
|
314
|
+
putInt("y", r[1])
|
|
315
|
+
putInt("width", r[2])
|
|
316
|
+
putInt("height", r[3])
|
|
317
|
+
putInt("imageWidth", img.cols())
|
|
318
|
+
putInt("imageHeight", img.rows())
|
|
319
|
+
})
|
|
320
|
+
} catch (t: Throwable) {
|
|
321
|
+
promise.reject("compute-inscribed-rect-failed", t.message, t)
|
|
322
|
+
} finally {
|
|
323
|
+
toRelease.forEach { it.release() }
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/** Crop `imagePath` to `{ x, y, width, height }` in place; resolves `{ width, height }`. */
|
|
329
|
+
@ReactMethod
|
|
330
|
+
fun cropToRect(options: ReadableMap, promise: Promise) {
|
|
331
|
+
val imagePath = options.getString("imagePath")
|
|
332
|
+
?: return promise.reject("invalid-options", "imagePath required")
|
|
333
|
+
fun optInt(key: String, default: Int): Int =
|
|
334
|
+
if (options.hasKey(key) && !options.isNull(key)) options.getInt(key) else default
|
|
335
|
+
val x = optInt("x", 0)
|
|
336
|
+
val y = optInt("y", 0)
|
|
337
|
+
val width = optInt("width", 0)
|
|
338
|
+
val height = optInt("height", 0)
|
|
339
|
+
val quality = optInt("quality", 90).coerceIn(1, 100)
|
|
340
|
+
CoroutineScope(Dispatchers.Default).launch {
|
|
341
|
+
val toRelease = mutableListOf<Mat>()
|
|
342
|
+
try {
|
|
343
|
+
ensureOpenCv()
|
|
344
|
+
val cleaned = stripFileScheme(imagePath)
|
|
345
|
+
if (!File(cleaned).exists()) {
|
|
346
|
+
promise.reject("read-failed", "Image not found: $imagePath")
|
|
347
|
+
return@launch
|
|
348
|
+
}
|
|
349
|
+
val img = Imgcodecs.imread(cleaned, Imgcodecs.IMREAD_COLOR)
|
|
350
|
+
toRelease += img
|
|
351
|
+
if (img.empty()) {
|
|
352
|
+
promise.reject("read-failed", "Could not decode $imagePath")
|
|
353
|
+
return@launch
|
|
354
|
+
}
|
|
355
|
+
// Clamp the rect to image bounds (never trust JS input).
|
|
356
|
+
val rx = x.coerceIn(0, img.cols() - 1)
|
|
357
|
+
val ry = y.coerceIn(0, img.rows() - 1)
|
|
358
|
+
var rw = width.coerceAtLeast(1)
|
|
359
|
+
var rh = height.coerceAtLeast(1)
|
|
360
|
+
if (rx + rw > img.cols()) rw = img.cols() - rx
|
|
361
|
+
if (ry + rh > img.rows()) rh = img.rows() - ry
|
|
362
|
+
val cropped = Mat(img, Rect(rx, ry, rw, rh)).clone()
|
|
363
|
+
toRelease += cropped
|
|
364
|
+
val params = MatOfInt(Imgcodecs.IMWRITE_JPEG_QUALITY, quality)
|
|
365
|
+
if (!Imgcodecs.imwrite(cleaned, cropped, params)) {
|
|
366
|
+
promise.reject("write-failed", "Could not rewrite $imagePath")
|
|
367
|
+
return@launch
|
|
368
|
+
}
|
|
369
|
+
promise.resolve(WritableNativeMap().apply {
|
|
370
|
+
putInt("width", cropped.cols())
|
|
371
|
+
putInt("height", cropped.rows())
|
|
372
|
+
})
|
|
373
|
+
} catch (t: Throwable) {
|
|
374
|
+
promise.reject("crop-to-rect-failed", t.message, t)
|
|
375
|
+
} finally {
|
|
376
|
+
toRelease.forEach { it.release() }
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/** Red-tint the dropped pixels; writes `<path>.mask.jpg`. Resolves `{ maskPath, width, height, excludedPercent }`. */
|
|
382
|
+
@ReactMethod
|
|
383
|
+
fun debugMaskOverlay(options: ReadableMap, promise: Promise) {
|
|
384
|
+
val imagePath = options.getString("imagePath")
|
|
385
|
+
?: return promise.reject("invalid-options", "imagePath required")
|
|
386
|
+
val threshold = if (options.hasKey("threshold") && !options.isNull("threshold")) {
|
|
387
|
+
options.getInt("threshold")
|
|
388
|
+
} else {
|
|
389
|
+
1
|
|
390
|
+
}
|
|
391
|
+
CoroutineScope(Dispatchers.Default).launch {
|
|
392
|
+
val toRelease = mutableListOf<Mat>()
|
|
393
|
+
try {
|
|
394
|
+
ensureOpenCv()
|
|
395
|
+
val cleaned = stripFileScheme(imagePath)
|
|
396
|
+
if (!File(cleaned).exists()) {
|
|
397
|
+
promise.reject("read-failed", "Image not found: $imagePath")
|
|
398
|
+
return@launch
|
|
399
|
+
}
|
|
400
|
+
val img = Imgcodecs.imread(cleaned, Imgcodecs.IMREAD_COLOR)
|
|
401
|
+
toRelease += img
|
|
402
|
+
if (img.empty()) {
|
|
403
|
+
promise.reject("read-failed", "Could not decode $imagePath")
|
|
404
|
+
return@launch
|
|
405
|
+
}
|
|
406
|
+
val mask = coverageOrBrightnessMask(img, cleaned, threshold.coerceAtLeast(0), toRelease)
|
|
407
|
+
val excluded = Mat()
|
|
408
|
+
toRelease += excluded
|
|
409
|
+
Core.bitwise_not(mask, excluded) // 255 = dropped pixels
|
|
410
|
+
// Blend red (BGR 0,0,255) over the dropped pixels so they stand out.
|
|
411
|
+
val red = Mat(img.size(), img.type(), Scalar(0.0, 0.0, 255.0))
|
|
412
|
+
toRelease += red
|
|
413
|
+
val blended = Mat()
|
|
414
|
+
toRelease += blended
|
|
415
|
+
Core.addWeighted(img, 0.35, red, 0.65, 0.0, blended)
|
|
416
|
+
val overlay = img.clone()
|
|
417
|
+
toRelease += overlay
|
|
418
|
+
blended.copyTo(overlay, excluded)
|
|
419
|
+
val maskPath = "$cleaned.mask.jpg"
|
|
420
|
+
val params = MatOfInt(Imgcodecs.IMWRITE_JPEG_QUALITY, 90)
|
|
421
|
+
if (!Imgcodecs.imwrite(maskPath, overlay, params)) {
|
|
422
|
+
promise.reject("write-failed", "Could not write mask overlay for $imagePath")
|
|
423
|
+
return@launch
|
|
424
|
+
}
|
|
425
|
+
val total = img.rows() * img.cols()
|
|
426
|
+
val excludedPercent = if (total > 0) Core.countNonZero(excluded) * 100 / total else 0
|
|
427
|
+
promise.resolve(WritableNativeMap().apply {
|
|
428
|
+
putString("maskPath", maskPath)
|
|
429
|
+
putInt("width", img.cols())
|
|
430
|
+
putInt("height", img.rows())
|
|
431
|
+
putInt("excludedPercent", excludedPercent)
|
|
432
|
+
})
|
|
433
|
+
} catch (t: Throwable) {
|
|
434
|
+
promise.reject("debug-mask-overlay-failed", t.message, t)
|
|
435
|
+
} finally {
|
|
436
|
+
toRelease.forEach { it.release() }
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Coverage mask for an on-disk image: prefer the TRUE coverage
|
|
443
|
+
* sidecar (`<path>.coverage.png`) the stitch writes next to the
|
|
444
|
+
* panorama; else a brightness proxy (gray > threshold, with
|
|
445
|
+
* border-connected holes filled). Mirrors the iOS debug methods.
|
|
446
|
+
* The returned Mat (and any intermediates) are registered in
|
|
447
|
+
* `toRelease`.
|
|
448
|
+
*/
|
|
449
|
+
private fun coverageOrBrightnessMask(
|
|
450
|
+
img: Mat,
|
|
451
|
+
cleaned: String,
|
|
452
|
+
threshold: Int,
|
|
453
|
+
toRelease: MutableList<Mat>,
|
|
454
|
+
): Mat {
|
|
455
|
+
val coveragePath = "$cleaned.coverage.png"
|
|
456
|
+
if (File(coveragePath).exists()) {
|
|
457
|
+
val cov = Imgcodecs.imread(coveragePath, Imgcodecs.IMREAD_GRAYSCALE)
|
|
458
|
+
toRelease += cov
|
|
459
|
+
if (!cov.empty() && cov.cols() == img.cols() && cov.rows() == img.rows()) {
|
|
460
|
+
val mask = Mat()
|
|
461
|
+
toRelease += mask
|
|
462
|
+
Imgproc.threshold(cov, mask, 0.0, 255.0, Imgproc.THRESH_BINARY)
|
|
463
|
+
return mask
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
val gray = Mat()
|
|
467
|
+
toRelease += gray
|
|
468
|
+
Imgproc.cvtColor(img, gray, Imgproc.COLOR_BGR2GRAY)
|
|
469
|
+
val raw = Mat()
|
|
470
|
+
toRelease += raw
|
|
471
|
+
Imgproc.threshold(gray, raw, threshold.toDouble(), 255.0, Imgproc.THRESH_BINARY)
|
|
472
|
+
val filled = fillBorderConnectedHoles(raw)
|
|
473
|
+
toRelease += filled
|
|
474
|
+
return filled
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Fill mask holes NOT connected to the border (interior content the
|
|
479
|
+
* brightness threshold dropped). Mirrors iOS' FillBorderConnectedHoles:
|
|
480
|
+
* pad a black border, flood the border-connected black to white, then
|
|
481
|
+
* OR the surviving interior holes back in.
|
|
482
|
+
*/
|
|
483
|
+
private fun fillBorderConnectedHoles(mask: Mat): Mat {
|
|
484
|
+
val padded = Mat()
|
|
485
|
+
Core.copyMakeBorder(mask, padded, 1, 1, 1, 1, Core.BORDER_CONSTANT, Scalar(0.0))
|
|
486
|
+
// floodFill needs a mask 2px larger than the image; a zero mask
|
|
487
|
+
// makes it behave like the no-mask overload iOS uses.
|
|
488
|
+
val ffMask = Mat.zeros(padded.rows() + 2, padded.cols() + 2, CvType.CV_8UC1)
|
|
489
|
+
Imgproc.floodFill(padded, ffMask, Point(0.0, 0.0), Scalar(255.0))
|
|
490
|
+
val exterior = Mat(padded, Rect(1, 1, mask.cols(), mask.rows()))
|
|
491
|
+
val holes = Mat()
|
|
492
|
+
Core.bitwise_not(exterior, holes)
|
|
493
|
+
val filled = Mat()
|
|
494
|
+
Core.bitwise_or(mask, holes, filled)
|
|
495
|
+
padded.release()
|
|
496
|
+
ffMask.release()
|
|
497
|
+
exterior.release()
|
|
498
|
+
holes.release()
|
|
499
|
+
return filled
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* Largest axis-aligned rectangle entirely inside the non-zero region
|
|
504
|
+
* of a CV_8UC1 `mask`. Returns `[x, y, width, height]` (all 0 if
|
|
505
|
+
* empty). Direct port of cpp/stitcher.cpp's maxInscribedRectFromMask
|
|
506
|
+
* (max-rectangle-in-histogram, row-swept, O(W*H)); operates on a
|
|
507
|
+
* bulk-extracted ByteArray so there is no per-pixel JNI Mat access.
|
|
508
|
+
*/
|
|
509
|
+
private fun maxInscribedRect(mask: Mat): IntArray {
|
|
510
|
+
if (mask.empty() || mask.type() != CvType.CV_8UC1) return intArrayOf(0, 0, 0, 0)
|
|
511
|
+
val h = mask.rows()
|
|
512
|
+
val w = mask.cols()
|
|
513
|
+
val data = ByteArray(h * w)
|
|
514
|
+
mask.get(0, 0, data)
|
|
515
|
+
val heights = IntArray(w)
|
|
516
|
+
var bestArea = 0L
|
|
517
|
+
var bx = 0
|
|
518
|
+
var by = 0
|
|
519
|
+
var bw = 0
|
|
520
|
+
var bh = 0
|
|
521
|
+
val stack = IntArray(w + 1)
|
|
522
|
+
for (row in 0 until h) {
|
|
523
|
+
val base = row * w
|
|
524
|
+
for (col in 0 until w) {
|
|
525
|
+
heights[col] = if (data[base + col].toInt() != 0) heights[col] + 1 else 0
|
|
526
|
+
}
|
|
527
|
+
var sp = 0
|
|
528
|
+
for (col in 0..w) {
|
|
529
|
+
val hh = if (col == w) 0 else heights[col]
|
|
530
|
+
while (sp > 0 && heights[stack[sp - 1]] > hh) {
|
|
531
|
+
val topIdx = stack[--sp]
|
|
532
|
+
val leftIdx = if (sp == 0) -1 else stack[sp - 1]
|
|
533
|
+
val width = col - leftIdx - 1
|
|
534
|
+
val area = heights[topIdx].toLong() * width.toLong()
|
|
535
|
+
if (area > bestArea) {
|
|
536
|
+
bestArea = area
|
|
537
|
+
bx = leftIdx + 1
|
|
538
|
+
by = row - heights[topIdx] + 1
|
|
539
|
+
bw = width
|
|
540
|
+
bh = heights[topIdx]
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
stack[sp++] = col
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
return intArrayOf(bx, by, bw, bh)
|
|
547
|
+
}
|
|
548
|
+
|
|
267
549
|
// ── Internals ────────────────────────────────────────────────
|
|
268
550
|
|
|
269
551
|
/**
|