This commit is contained in:
coco
2026-07-14 16:50:37 +08:00
parent 40a4c70223
commit 51ed82cddf
4 changed files with 764 additions and 32 deletions
@@ -45,6 +45,15 @@ fun ScanPage(onBack: () -> Unit) {
val scanState by viewModel.state.collectAsStateWithLifecycle()
val cameraExecutor = remember { Executors.newSingleThreadExecutor() }
// Unbind camera when leaving the page (prevents ImageReader queue overflow)
DisposableEffect(Unit) {
onDispose {
try {
ProcessCameraProvider.getInstance(context).get().unbindAll()
} catch (_: Exception) { }
cameraExecutor.shutdown()
}
}
val analyzer = remember { OvImageAnalyzer(viewModel) }
// Track whether camera should be bound
@@ -51,7 +51,8 @@ object ContourSelector {
contourGf.rightBottom = Point((rect.x + rect.width).toDouble(), (rect.y + rect.height).toDouble())
val roi = Rect(roteRect.center.x.toInt(), roteRect.center.y.toInt(), MIN_VALUE, MIN_VALUE)
if ((roi.x + MIN_VALUE) > (src.width() - 1)
if (roi.x < 0 || roi.y < 0
|| (roi.x + MIN_VALUE) > (src.width() - 1)
|| (roi.y + MIN_VALUE) > (src.height() - 1)
|| width < MIN_VALUE
|| height < MIN_VALUE
@@ -158,12 +158,11 @@ class ScanViewModel : ViewModel() {
roiRect = Rect(0, 0, procW, procH)
}
// Step 1: Locate the strip — skip locate() for camera (always fails with center distance ratio)
// go directly to locateCroppedStrip which works better on full camera frames
val rgbMat = Mat()
Imgproc.cvtColor(srcRgba, rgbMat, Imgproc.COLOR_RGBA2RGB)
var stripResult = StripLocator.locateCroppedStrip(rgbMat)
rgbMat.release()
// Step 1: Locate the strip using C++ pipeline (find green blocks → crop → detect C/T lines)
var stripResult = StripLocator.locateAndDetectCT(srcRgba, roiRect!!)
if (stripResult.errorCode != 0) {
Log.d(TAG, "locateAndDetectCT failed (errorCode=${stripResult.errorCode}), skipping frame")
}
if (stripResult.errorCode != 0) {
// Update preview bitmap even on failure (shows the source image)
@@ -202,6 +201,12 @@ class ScanViewModel : ViewModel() {
// Step 3: Classify
val result = OvClassifier.classify(features)
// Log first 3 frames' features for diagnostics
val frameIdx = synchronized(results) { results.size + 1 }
if (frameIdx <= 3) {
Log.d(TAG, "Frame $frameIdx features: ${features.joinToString(",") { "%.4f".format(it) }}")
}
synchronized(results) {
results.add(result)
val count = results.size
@@ -105,7 +105,7 @@ object StripLocator {
val srcHei = roiRect.height
// Extract ROI
val roiSrc = srcRgba.submat(roiRect)
val roiSrcRaw = srcRgba.submat(roiRect)
if (drawAnnotations) {
Imgproc.rectangle(srcRgba,
@@ -137,7 +137,9 @@ object StripLocator {
// Problem: grayscale is sensitive to brightness/reflections. Green blocks
// may not contrast well with background under glare.
val s = Mat()
Imgproc.cvtColor(roiSrc, roiSrc, Imgproc.COLOR_RGBA2RGB) // RGBA → BGR
val roiSrc = Mat()
Imgproc.cvtColor(roiSrcRaw, roiSrc, Imgproc.COLOR_RGBA2RGB)
roiSrcRaw.release()
val hsv = Mat()
Imgproc.cvtColor(roiSrc, hsv, Imgproc.COLOR_RGB2HSV) // BGR → HSV
val hsvMats = mutableListOf<Mat>()
@@ -163,7 +165,7 @@ object StripLocator {
Imgproc.findContours(sThresoldImg, contours0, Mat(), Imgproc.RETR_EXTERNAL, Imgproc.CHAIN_APPROX_SIMPLE)
if (contours0.isEmpty()) {
Log.d(TAG, "Step 3 fail: no contours found after OTSU+morph (S-channel)")
sChannel.release(); sCpy.release(); sThresoldImg.release(); kernel5x5.release()
sChannel?.release(); sCpy?.release(); sThresoldImg?.release(); kernel5x5?.release()
roiSrc.release()
return StripResult(errorCode = -9, paperColor = PaperColor.Unknown)
}
@@ -228,7 +230,7 @@ object StripLocator {
if (effectiveGreenContours.size < 2) {
Log.d(TAG, "Step 4 fail: need >=2 green contours, got ${effectiveGreenContours.size}")
sChannel.release(); sCpy.release(); sThresoldImg.release(); kernel5x5.release()
sChannel?.release(); sCpy?.release(); sThresoldImg?.release(); kernel5x5?.release()
roiSrc.release()
return StripResult(errorCode = -9, paperColor = PaperColor.Green)
}
@@ -243,7 +245,7 @@ object StripLocator {
if (gfsFilteredH.size < 2) {
Log.d(TAG, "Step 4b fail: height filter left ${gfsFilteredH.size} contours (maxH=$maxHeight)")
sChannel.release(); sCpy.release(); sThresoldImg.release(); kernel5x5.release()
sChannel?.release(); sCpy?.release(); sThresoldImg?.release(); kernel5x5?.release()
roiSrc.release()
return StripResult(errorCode = -9, paperColor = PaperColor.Green)
}
@@ -258,7 +260,7 @@ object StripLocator {
else lastArea.toDouble() / firstArea
if (areaRatio < 1.1 || areaRatio > 8.0) {
Log.d(TAG, "Step 5 fail: area ratio $areaRatio not in [1.1, 8.0]")
sChannel.release(); sCpy.release(); sThresoldImg.release(); kernel5x5.release()
sChannel?.release(); sCpy?.release(); sThresoldImg?.release(); kernel5x5?.release()
roiSrc.release()
return StripResult(errorCode = -8, paperColor = PaperColor.Green)
}
@@ -288,7 +290,7 @@ object StripLocator {
|| bottomY + MIN_PIX_VAL >= srcHei
) {
Log.d(TAG, "Step 6 fail: halfH=$realHalfHBig, bounds=($leftX,$topY)-($rightX,$bottomY) src=${srcWid}x${srcHei}")
sChannel.release(); sCpy.release(); sThresoldImg.release(); kernel5x5.release(); roiSrc.release()
sChannel?.release(); sCpy?.release(); sThresoldImg?.release(); kernel5x5?.release(); roiSrc.release()
return StripResult(errorCode = -10, paperColor = PaperColor.Unknown)
}
@@ -311,7 +313,7 @@ object StripLocator {
|| rcColor1.x <= 0 || rcColor1.x + rcColor1.width >= srcWid
|| rcColor1.y <= 0 || rcColor1.y + rcColor1.height >= srcHei
) {
sChannel.release(); sCpy.release(); sThresoldImg.release(); kernel5x5.release()
sChannel?.release(); sCpy?.release(); sThresoldImg?.release(); kernel5x5?.release()
wbRoiSrc.release(); roiSrc.release()
return StripResult(errorCode = -11, paperColor = PaperColor.Unknown)
}
@@ -335,7 +337,7 @@ object StripLocator {
mc1.release()
if (paperColor == PaperColor.Unknown || paperColor != PaperColor.Green) {
Log.d(TAG, "Step 9 fail: paperColor=$paperColor")
sChannel.release(); sCpy.release(); sThresoldImg.release(); kernel5x5.release()
sChannel?.release(); sCpy?.release(); sThresoldImg?.release(); kernel5x5?.release()
wbRoiSrc.release(); roiSrc.release()
return StripResult(errorCode = -1, paperColor = paperColor)
}
@@ -351,7 +353,7 @@ object StripLocator {
realAngle = -angle - 90
} else {
Log.d(TAG, "Step 10 fail: rotRcH == rotRcW (square block, cannot determine orientation)")
sChannel.release(); sCpy.release(); sThresoldImg.release(); kernel5x5.release()
sChannel?.release(); sCpy?.release(); sThresoldImg?.release(); kernel5x5?.release()
wbRoiSrc.release(); roiSrc.release()
return StripResult(errorCode = -1, paperColor = paperColor)
}
@@ -373,7 +375,7 @@ object StripLocator {
mc2.release()
if (paperColor != color2) {
Log.d(TAG, "Step 10 fail: second block color mismatch: first=$paperColor second=$color2")
sChannel.release(); sCpy.release(); sThresoldImg.release(); kernel5x5.release()
sChannel?.release(); sCpy?.release(); sThresoldImg?.release(); kernel5x5?.release()
wbRoiSrc.release(); roiSrc.release()
return StripResult(errorCode = -1, paperColor = paperColor)
}
@@ -384,7 +386,7 @@ object StripLocator {
val whRadioSmall = realHalfWSmall.toDouble() / (realHalfHSmall + 0.01)
if (whRadioSmall < 1.3 || whRadioSmall > 8.0) {
Log.d(TAG, "Small block whRatio fail: $whRadioSmall not in [1.3, 8.0]")
sChannel.release(); sCpy.release(); sThresoldImg.release(); kernel5x5.release()
sChannel?.release(); sCpy?.release(); sThresoldImg?.release(); kernel5x5?.release()
wbRoiSrc.release(); roiSrc.release()
return StripResult(errorCode = -1, paperColor = paperColor)
}
@@ -394,7 +396,7 @@ object StripLocator {
val heightDiff = Math.abs(realHalfHBig - realHalfHSmall).toDouble() / (realHalfHSmall + 0.01)
if (heightDiff > 0.30) {
Log.d(TAG, "Block height mismatch: bigH=$realHalfHBig smallH=$realHalfHSmall diff=$heightDiff")
sChannel.release(); sCpy.release(); sThresoldImg.release(); kernel5x5.release()
sChannel?.release(); sCpy?.release(); sThresoldImg?.release(); kernel5x5?.release()
wbRoiSrc.release(); roiSrc.release()
return StripResult(errorCode = -1, paperColor = paperColor)
}
@@ -407,7 +409,7 @@ object StripLocator {
val distRadio = centerPointDist / (realHalfWBig * 2.0)
if (distRadio < 1 || distRadio > 1.65) {
Log.d(TAG, "Step 10c fail: center distance ratio $distRadio not in [1, 1.65]")
sChannel.release(); sCpy.release(); sThresoldImg.release(); kernel5x5.release()
sChannel?.release(); sCpy?.release(); sThresoldImg?.release(); kernel5x5?.release()
wbRoiSrc.release(); roiSrc.release()
return StripResult(errorCode = -1, paperColor = paperColor)
}
@@ -471,7 +473,7 @@ object StripLocator {
|| (autoRoi.x + autoRoi.width) >= (maskSrc.width() - 1)
|| (autoRoi.y + autoRoi.height) >= (maskSrc.height() - 1)
) {
sChannel.release(); sCpy.release(); sThresoldImg.release(); kernel5x5.release()
sChannel?.release(); sCpy?.release(); sThresoldImg?.release(); kernel5x5?.release()
wbRoiSrc.release(); roiSrc.release()
mask.release(); maskSrc.release()
return StripResult(errorCode = -11, paperColor = paperColor)
@@ -718,20 +720,23 @@ object StripLocator {
val hChannel = hsvChannels[0] // Hue
val sChannel = hsvChannels[1] // Saturation
// Create green mask: H in [30, 85] AND S > 30
// Create green mask: H in [30, 85] AND S > 20 (lowered from 30 for reflections)
val hMaskLow = Mat()
val hMaskHigh = Mat()
val sMask = Mat()
Imgproc.threshold(hChannel, hMaskLow, H_GREEN_MIN, 255.0, Imgproc.THRESH_BINARY)
Imgproc.threshold(hChannel, hMaskHigh, H_GREEN_MAX, 255.0, Imgproc.THRESH_BINARY_INV)
Imgproc.threshold(sChannel, sMask, 30.0, 255.0, Imgproc.THRESH_BINARY)
Imgproc.threshold(sChannel, sMask, 20.0, 255.0, Imgproc.THRESH_BINARY)
val hMask = Mat()
Core.bitwise_and(hMaskLow, hMaskHigh, hMask)
val greenMask = Mat()
Core.bitwise_and(hMask, sMask, greenMask)
// Morphological open to clean noise
// Morphological close + open: close merges regions split by reflections,
// open cleans up noise. Use 5x5 kernel for close (stronger merge).
val kernel5x5 = Imgproc.getStructuringElement(Imgproc.MORPH_RECT, Size(5.0, 5.0))
Imgproc.morphologyEx(greenMask, greenMask, Imgproc.MORPH_CLOSE, kernel5x5)
val kernel3x3 = Imgproc.getStructuringElement(Imgproc.MORPH_RECT, Size(3.0, 3.0))
Imgproc.morphologyEx(greenMask, greenMask, Imgproc.MORPH_OPEN, kernel3x3)
@@ -740,9 +745,9 @@ object StripLocator {
Imgproc.findContours(greenMask, contours, Mat(), Imgproc.RETR_EXTERNAL, Imgproc.CHAIN_APPROX_SIMPLE)
if (contours.isEmpty()) {
hsv.release(); hChannel.release(); sChannel.release()
hsv.release(); hChannel.release(); sChannel?.release()
hMaskLow.release(); hMaskHigh.release(); sMask.release()
hMask.release(); greenMask.release(); kernel3x3.release()
hMask.release(); greenMask.release(); kernel5x5.release(); kernel3x3.release()
hsvChannels[2].release()
return result
}
@@ -758,10 +763,10 @@ object StripLocator {
val gfs2 = mutableListOf<Pair<MatOfPoint, ContourGf>>()
ContourSelector.selectContour(gfs1, gfs2,
roiSrc.cols() * 0.05, roiSrc.cols() * 0.95, GfFlag.LeftTopX)
roiSrc.cols() * 0.02, roiSrc.cols() * 0.98, GfFlag.LeftTopX)
val gfs3 = mutableListOf<Pair<MatOfPoint, ContourGf>>()
ContourSelector.selectContour(gfs2, gfs3, 30.0, 100000.0, GfFlag.Area)
ContourSelector.selectContour(gfs2, gfs3, 15.0, 100000.0, GfFlag.Area)
val gfs4 = mutableListOf<Pair<MatOfPoint, ContourGf>>()
ContourSelector.selectContour(gfs3, gfs4, 0.40, 1.1, GfFlag.Rectangularity)
@@ -774,9 +779,9 @@ object StripLocator {
// so no need for color pre-filtering. Just return them.
result.addAll(gfs4)
hsv.release(); hChannel.release(); sChannel.release()
hsv.release(); hChannel.release(); sChannel?.release()
hMaskLow.release(); hMaskHigh.release(); sMask.release()
hMask.release(); greenMask.release(); kernel3x3.release()
hMask.release(); greenMask.release(); kernel5x5.release(); kernel3x3.release()
hsvChannels[2].release()
return result
@@ -968,6 +973,718 @@ object StripLocator {
)
}
/**
* Full C++ pipeline: locate green blocks → crop strip → detect C/T lines.
*
* Aligns with ImageLocationColloidalGold.cpp GetPositions():
* 1. Find green locator blocks via S-channel OTSU + color pre-filtering
* 2. Compute rotation, crop and warp the strip region
* 3. Detect C/T lines on the cropped strip with C++ filter thresholds
*
* C++ C/T filter order: RightBottomX → LeftTopX → Rectangularity → Height(0.7) → Width(0.045)
* C++ dedup: keep one contour per side (left/right of center)
* C++ single-line validation: stddev check on artificial T line region
*
* @param srcRgba Full RGBA camera frame (will be modified internally)
* @param roiRect Region of interest (typically full frame)
* @return StripResult with extracted ROIs, or errorCode != 0 on failure
*/
fun locateAndDetectCT(srcRgba: Mat, roiRect: Rect): StripResult {
val srcWid = roiRect.width
val srcHei = roiRect.height
val roiSubmat = srcRgba.submat(roiRect)
val roiSrc = Mat()
Imgproc.cvtColor(roiSubmat, roiSrc, Imgproc.COLOR_RGBA2RGB)
roiSubmat.release()
// =====================================================================
// PHASE 1: GREEN BLOCK DETECTION (aligned with C++ _filterAndJudgeVertical)
// =====================================================================
// Try HSV first (more reliable on camera), OTSU as fallback
var sChannel: Mat? = null
var sCpy: Mat? = null
var sThresoldImg: Mat? = null
var kernel5x5: Mat? = null
var effectiveContours = tryDirectHsvThresholding(roiSrc)
if (effectiveContours.size >= 2) {
Log.d(TAG, "locateAndDetectCT: HSV primary found ${effectiveContours.size} green contours")
} else {
Log.d(TAG, "locateAndDetectCT: HSV found <2 green, trying OTSU fallback")
// Convert to HSV, extract S channel for OTSU
val hsv = Mat()
Imgproc.cvtColor(roiSrc, hsv, Imgproc.COLOR_RGB2HSV)
val hsvMats = mutableListOf<Mat>()
Core.split(hsv, hsvMats)
sChannel = hsvMats[1]
hsvMats[0].release(); hsvMats[2].release(); hsv.release()
sCpy = Mat()
sChannel!!.copyTo(sCpy)
// OTSU on S channel (C++: threshold(s, sThresoldImg, 128, 255, THRESH_BINARY | THRESH_OTSU))
sThresoldImg = Mat()
Imgproc.threshold(sChannel!!, sThresoldImg, 128.0, 255.0,
Imgproc.THRESH_BINARY or Imgproc.THRESH_OTSU)
// Morphology open with 5×5 kernel (C++: morphologyEx(sThresoldImg, sThresoldImg, MORPH_OPEN, 5x5))
kernel5x5 = Imgproc.getStructuringElement(Imgproc.MORPH_RECT, Size(5.0, 5.0))
Imgproc.morphologyEx(sThresoldImg, sThresoldImg, Imgproc.MORPH_OPEN, kernel5x5)
// Find external contours
val contours0 = mutableListOf<MatOfPoint>()
Imgproc.findContours(sThresoldImg, contours0, Mat(), Imgproc.RETR_EXTERNAL, Imgproc.CHAIN_APPROX_SIMPLE)
if (contours0.isEmpty()) {
Log.d(TAG, "locateAndDetectCT: no contours after OTSU")
sChannel?.release(); sCpy?.release(); sThresoldImg?.release(); kernel5x5?.release(); roiSrc.release()
return StripResult(errorCode = -9, paperColor = PaperColor.Unknown)
}
// Calculate geometric features
val gfsAll = mutableListOf<Pair<MatOfPoint, ContourGf>>()
ContourSelector.calContoursGf(contours0, gfsAll, sChannel)
// Filter pipeline (C++ thresholds)
val gfsRbX = mutableListOf<Pair<MatOfPoint, ContourGf>>()
ContourSelector.selectContour(gfsAll, gfsRbX,
sChannel.cols() * 0.05, sChannel.cols() * 0.95, GfFlag.RightBottomX)
val gfsLtX = mutableListOf<Pair<MatOfPoint, ContourGf>>()
ContourSelector.selectContour(gfsRbX, gfsLtX,
sChannel.cols() * 0.05, sChannel.cols() * 0.95, GfFlag.LeftTopX)
// C++: Area 50100000 (lowered from 150 for small blocks in camera)
val gfsArea = mutableListOf<Pair<MatOfPoint, ContourGf>>()
ContourSelector.selectContour(gfsLtX, gfsArea, 50.0, 100000.0, GfFlag.Area)
// C++: Rectangularity 0.551.1
val gfsRect = mutableListOf<Pair<MatOfPoint, ContourGf>>()
ContourSelector.selectContour(gfsArea, gfsRect, 0.55, 1.1, GfFlag.Rectangularity)
// Color pre-filtering: keep only green contours (C++: _readPaperColor at contour center)
val gfsGreen = mutableListOf<Pair<MatOfPoint, ContourGf>>()
for ((contour, gf) in gfsRect) {
val cx = gf.contourCenter.x.toInt()
val cy = gf.contourCenter.y.toInt()
if (cx > 10 && cy > 10 && cx + 5 < roiSrc.cols() && cy + 5 < roiSrc.rows()) {
val sampleRect = Rect(cx - 5, cy - 5, 10, 10)
val sample = roiSrc.submat(sampleRect)
val color = judgePaperColorAtPoint(sample, hsvMats)
sample.release()
if (color == PaperColor.Green) {
gfsGreen.add(Pair(contour, gf))
}
}
}
Log.d(TAG, "locateAndDetectCT green blocks: raw=${gfsAll.size}" +
"rbX=${gfsRbX.size} → ltX=${gfsLtX.size}" +
"area=${gfsArea.size} → rect=${gfsRect.size} → green=${gfsGreen.size}")
effectiveContours = gfsGreen
}
if (effectiveContours.size < 2) {
Log.d(TAG, "locateAndDetectCT fail: need >=2 green contours, got ${effectiveContours.size}")
sChannel?.release(); sCpy?.release(); sThresoldImg?.release(); kernel5x5?.release(); roiSrc.release()
return StripResult(errorCode = -9, paperColor = PaperColor.Green)
}
// Safety: real strips have 2-5 green contours; excessive = noise, reject early
if (effectiveContours.size > 8) {
Log.d(TAG, "locateAndDetectCT fail: too many green contours (${effectiveContours.size}), likely noise")
sChannel?.release(); sCpy?.release(); sThresoldImg?.release(); kernel5x5?.release(); roiSrc.release()
return StripResult(errorCode = -9, paperColor = PaperColor.Unknown)
}
// --- Height filtering: keep contours with height >= 30% of max (C++: 72.5%, relaxed for camera) ---
ContourSelector.sortContoursByGf(effectiveContours, GfFlag.Height, true)
val maxHeight = effectiveContours[0].second.size.height
val gfsH = mutableListOf<Pair<MatOfPoint, ContourGf>>()
ContourSelector.selectContour(effectiveContours, gfsH,
maxHeight * 0.30, maxHeight * 1.0, GfFlag.Height)
if (gfsH.size < 2) {
Log.d(TAG, "locateAndDetectCT fail: height filter left ${gfsH.size} contours (maxH=$maxHeight)")
sChannel?.release(); sCpy?.release(); sThresoldImg?.release(); kernel5x5?.release(); roiSrc.release()
return StripResult(errorCode = -9, paperColor = PaperColor.Green)
}
// Sort by center X for left/right ordering
ContourSelector.sortContoursByGf(gfsH, GfFlag.CenterX, false)
// --- Area ratio validation (C++: 2.04.0, relaxed to 1.030.0 for camera) ---
val firstArea = gfsH[0].second.contourArea
val lastArea = gfsH[gfsH.size - 1].second.contourArea
val areaRatio = if (firstArea > lastArea) firstArea.toDouble() / lastArea
else lastArea.toDouble() / firstArea
if (areaRatio < 1.0 || areaRatio > 30.0) {
Log.d(TAG, "locateAndDetectCT fail: area ratio $areaRatio not in [1.0, 30.0]")
sChannel?.release(); sCpy?.release(); sThresoldImg?.release(); kernel5x5?.release(); roiSrc.release()
return StripResult(errorCode = -8, paperColor = PaperColor.Green)
}
// Keep only the leftmost and rightmost contours (C++ approach)
val gfsPair = mutableListOf(gfsH[0], gfsH[gfsH.size - 1])
// =====================================================================
// PHASE 2: VALIDATE AND COMPUTE STRIP GEOMETRY
// =====================================================================
// C++: Determine big/small by contour area, NOT by position
// Physical specs: big block 30mm wide, small block 10mm wide, strip height 3-5mm
// Big block whRatio: 30/5 ~ 30/3 = 6~10, tolerance 4.5~12.5
// Small block whRatio: 10/5 ~ 10/3 = 2~3.3, tolerance 1.5~4.5
val gfp0 = gfsPair[0]
val gfp1 = gfsPair[1]
val rotRect0 = gfp0.second.rotRect
val rotRect1 = gfp1.second.rotRect
val halfW0 = (Math.max(rotRect0.size.width, rotRect0.size.height) * 0.5).toInt()
val halfH0 = (Math.min(rotRect0.size.width, rotRect0.size.height) * 0.5).toInt()
val halfW1 = (Math.max(rotRect1.size.width, rotRect1.size.height) * 0.5).toInt()
val halfH1 = (Math.min(rotRect1.size.width, rotRect1.size.height) * 0.5).toInt()
// C++: if left area > right area → left is big, right is small (bPaperRotated=true)
val bigGfp: Pair<MatOfPoint, ContourGf>
val smallGfp: Pair<MatOfPoint, ContourGf>
val realHalfWBig: Int; val realHalfHBig: Int
val realHalfWSmall: Int; val realHalfHSmall: Int
val bigRotRect: RotatedRect; val smallRotRect: RotatedRect
if (gfp0.second.contourArea > gfp1.second.contourArea) {
bigGfp = gfp0; smallGfp = gfp1
realHalfWBig = halfW0; realHalfHBig = halfH0
realHalfWSmall = halfW1; realHalfHSmall = halfH1
bigRotRect = rotRect0; smallRotRect = rotRect1
} else {
bigGfp = gfp1; smallGfp = gfp0
realHalfWBig = halfW1; realHalfHBig = halfH1
realHalfWSmall = halfW0; realHalfHSmall = halfH0
bigRotRect = rotRect1; smallRotRect = rotRect0
}
val leftX = bigGfp.second.leftTop.x.toInt()
val topY = bigGfp.second.leftTop.y.toInt()
val rightX = bigGfp.second.rightBottom.x.toInt()
val bottomY = bigGfp.second.rightBottom.y.toInt()
if (realHalfHBig < MIN_PIX_VAL
|| leftX - MIN_PIX_VAL <= 0
|| rightX + MIN_PIX_VAL >= srcWid
|| topY - MIN_PIX_VAL <= 0
|| bottomY + MIN_PIX_VAL >= srcHei
) {
Log.d(TAG, "locateAndDetectCT fail: boundary check")
sChannel?.release(); sCpy?.release(); sThresoldImg?.release(); kernel5x5?.release(); roiSrc.release()
return StripResult(errorCode = -10, paperColor = PaperColor.Unknown)
}
// Auto white balance + paper color check (C++: _filterByLocatorColor)
val wbRoiSrc = Mat()
autoWhiteBalance(roiSrc, wbRoiSrc)
val rcColor1 = Rect(
(bigRotRect.center.x - realHalfHBig / 2).toInt(),
(bigRotRect.center.y - realHalfHBig / 2).toInt(),
realHalfHBig, realHalfHBig
)
if (rcColor1.x < 0 || rcColor1.y < 0
|| rcColor1.x + rcColor1.width > wbRoiSrc.cols()
|| rcColor1.y + rcColor1.height > wbRoiSrc.rows()
) {
Log.d(TAG, "locateAndDetectCT fail: rcColor1 out of bounds")
sChannel?.release(); sCpy?.release(); sThresoldImg?.release(); kernel5x5?.release()
wbRoiSrc.release(); roiSrc.release()
return StripResult(errorCode = -11, paperColor = PaperColor.Unknown)
}
val mc1 = wbRoiSrc.submat(rcColor1)
val paperColor = judgePaperColor(mc1)
mc1.release()
if (paperColor != PaperColor.Green) {
Log.d(TAG, "locateAndDetectCT fail: paperColor=$paperColor")
sChannel?.release(); sCpy?.release(); sThresoldImg?.release(); kernel5x5?.release()
wbRoiSrc.release(); roiSrc.release()
return StripResult(errorCode = -1, paperColor = paperColor)
}
// --- Rotation correction (C++: _retrieveCtAreaInfo step 1, uses big block) ---
val bigRotRcW = bigRotRect.size.width.toInt()
val bigRotRcH = bigRotRect.size.height.toInt()
val angle = bigRotRect.angle
val bWgtH: Boolean
val realAngle: Double
if (bigRotRcH < bigRotRcW) {
bWgtH = true
realAngle = -angle
} else if (bigRotRcH > bigRotRcW) {
bWgtH = false
realAngle = -angle - 90
} else {
Log.d(TAG, "locateAndDetectCT fail: square block")
sChannel?.release(); sCpy?.release(); sThresoldImg?.release(); kernel5x5?.release()
wbRoiSrc.release(); roiSrc.release()
return StripResult(errorCode = -1, paperColor = paperColor)
}
// Verify second (small) block is also green
val rcColor2 = Rect(
(smallRotRect.center.x - realHalfHSmall / 2).toInt(),
(smallRotRect.center.y - realHalfHSmall / 2).toInt(),
realHalfHSmall, realHalfHSmall
)
if (rcColor2.x < 0 || rcColor2.y < 0
|| rcColor2.x + rcColor2.width > wbRoiSrc.cols()
|| rcColor2.y + rcColor2.height > wbRoiSrc.rows()
) {
Log.d(TAG, "locateAndDetectCT fail: rcColor2 out of bounds")
sChannel?.release(); sCpy?.release(); sThresoldImg?.release(); kernel5x5?.release()
wbRoiSrc.release(); roiSrc.release()
return StripResult(errorCode = -11, paperColor = PaperColor.Unknown)
}
val mc2 = wbRoiSrc.submat(rcColor2)
val color2 = judgePaperColor(mc2)
mc2.release()
if (paperColor != color2) {
Log.d(TAG, "locateAndDetectCT fail: second block color mismatch")
sChannel?.release(); sCpy?.release(); sThresoldImg?.release(); kernel5x5?.release()
wbRoiSrc.release(); roiSrc.release()
return StripResult(errorCode = -1, paperColor = paperColor)
}
// --- Big block whRatio: 30mm / (3~5mm) = 6~10, relaxed to 3.015.0 for camera ---
val whRadioBig = realHalfWBig.toDouble() / (realHalfHBig + 0.01)
if (whRadioBig < 3.0 || whRadioBig > 15.0) {
Log.d(TAG, "locateAndDetectCT fail: big whRatio=$whRadioBig not in [3.0, 15.0]")
sChannel?.release(); sCpy?.release(); sThresoldImg?.release(); kernel5x5?.release()
wbRoiSrc.release(); roiSrc.release()
return StripResult(errorCode = -1, paperColor = paperColor)
}
// --- Small block whRatio: 10mm / (3~5mm) = 2~3.3, relaxed to 1.35.5 for camera ---
val whRadioSmall = realHalfWSmall.toDouble() / (realHalfHSmall + 0.01)
if (whRadioSmall < 1.3 || whRadioSmall > 5.5) {
Log.d(TAG, "locateAndDetectCT fail: small whRatio=$whRadioSmall not in [1.3, 5.5]")
sChannel?.release(); sCpy?.release(); sThresoldImg?.release(); kernel5x5?.release()
wbRoiSrc.release(); roiSrc.release()
return StripResult(errorCode = -1, paperColor = paperColor)
}
// --- Height similarity (C++: 12%, relaxed to 40% for camera) ---
val heightDiff = Math.abs(realHalfHBig - realHalfHSmall).toDouble() / (realHalfHSmall + 0.01)
if (heightDiff > 0.40) {
Log.d(TAG, "locateAndDetectCT fail: height diff=$heightDiff > 0.40")
sChannel?.release(); sCpy?.release(); sThresoldImg?.release(); kernel5x5?.release()
wbRoiSrc.release(); roiSrc.release()
return StripResult(errorCode = -1, paperColor = paperColor)
}
// --- Center distance ratio (C++: 1.01.7, relaxed to 0.81.8 for camera) ---
val centerPointDist = Math.sqrt(
(bigRotRect.center.x - smallRotRect.center.x) * (bigRotRect.center.x - smallRotRect.center.x)
+ (bigRotRect.center.y - smallRotRect.center.y) * (bigRotRect.center.y - smallRotRect.center.y)
)
val distRadio = centerPointDist / (realHalfWBig * 2.0)
if (distRadio < 0.8 || distRadio > 1.8) {
Log.d(TAG, "locateAndDetectCT fail: distRadio=$distRadio not in [0.8, 1.8]")
sChannel?.release(); sCpy?.release(); sThresoldImg?.release(); kernel5x5?.release()
wbRoiSrc.release(); roiSrc.release()
return StripResult(errorCode = -13, paperColor = paperColor)
}
// --- Draw green block bounding rectangles on srcRgba for visual feedback ---
// These are drawn on the full camera frame (srcRgba) so the user can see
// that positioning succeeded. Uses the boundingRect() of each RotatedRect.
val bigBlockRect = bigRotRect.boundingRect()
val smallBlockRect = smallRotRect.boundingRect()
Imgproc.rectangle(srcRgba,
Point((roiRect.x + bigBlockRect.x).toDouble(), (roiRect.y + bigBlockRect.y).toDouble()),
Point((roiRect.x + bigBlockRect.x + bigBlockRect.width).toDouble(), (roiRect.y + bigBlockRect.y + bigBlockRect.height).toDouble()),
Scalar(255.0, 0.0, 0.0, 255.0), 3)
Imgproc.rectangle(srcRgba,
Point((roiRect.x + smallBlockRect.x).toDouble(), (roiRect.y + smallBlockRect.y).toDouble()),
Point((roiRect.x + smallBlockRect.x + smallBlockRect.width).toDouble(), (roiRect.y + smallBlockRect.y + smallBlockRect.height).toDouble()),
Scalar(255.0, 0.0, 0.0, 255.0), 3)
// =====================================================================
// PHASE 3: CROP AND WARP THE STRIP REGION (C++: _retrieveCtAreaInfo)
// =====================================================================
var x0: Double; var y0: Double; var x1: Double; var y1: Double
val phi = realAngle * Math.PI / 180
val sin = Math.sin(phi); val cos = Math.cos(phi)
var shiftBig = 1.1; val shiftSmall = 1.65
when {
distRadio > 1.4 -> shiftBig = 1.25
distRadio > 1.3 -> shiftBig = 1.2
distRadio > 1.2 -> shiftBig = 1.15
}
val bRot: Boolean
if (bigRotRect.center.x > smallRotRect.center.x) {
// Big block on the right, small block on the left
bRot = false
x0 = smallRotRect.center.x + (realHalfWSmall * shiftSmall) * cos
y0 = smallRotRect.center.y - (realHalfWSmall * shiftSmall) * sin
x1 = bigRotRect.center.x - cos * (realHalfWBig * shiftBig)
y1 = bigRotRect.center.y + sin * (realHalfWBig * shiftBig)
} else {
// Big block on the left, small block on the right
bRot = true
x0 = bigRotRect.center.x + (realHalfWBig * shiftBig) * cos
y0 = bigRotRect.center.y - (realHalfWBig * shiftBig) * sin
x1 = smallRotRect.center.x - cos * (realHalfWSmall * shiftSmall)
y1 = smallRotRect.center.y + sin * (realHalfWSmall * shiftSmall)
}
val len1 = Math.sqrt((x0 - x1) * (x0 - x1) + (y0 - y1) * (y0 - y1)).toInt()
val len2 = (realHalfHBig * 1.5).toInt()
val centerX = ((x0 + x1) * 0.5).toInt()
val centerY = ((y0 + y1) * 0.5).toInt()
// Build rotated rect and mask
val rotRect1C = Point(centerX.toDouble(), centerY.toDouble())
val rotRc1Size = if (bWgtH) Size(len1.toDouble(), len2.toDouble())
else Size(len2.toDouble(), len1.toDouble())
val rotRect2 = RotatedRect(rotRect1C, rotRc1Size, angle)
val vPoints = arrayOfNulls<Point>(4)
rotRect2.points(vPoints)
val buildContour = MatOfPoint(*vPoints.requireNoNulls())
val mask = Mat(roiRect.height, roiRect.width, CvType.CV_8UC3)
mask.setTo(black)
Imgproc.drawContours(mask, listOf(buildContour), 0, white, -1)
val maskSrc = Mat()
Core.bitwise_and(roiSrc, mask, maskSrc)
val autoRoi = Imgproc.boundingRect(buildContour)
// Keep buildContour alive — used to draw C/T area outline on srcRgba later
if (autoRoi.x <= 0 || autoRoi.y <= 0
|| (autoRoi.x + autoRoi.width) >= (maskSrc.width() - 1)
|| (autoRoi.y + autoRoi.height) >= (maskSrc.height() - 1)
) {
Log.d(TAG, "locateAndDetectCT fail: autoRoi out of bounds")
sChannel?.release(); sCpy?.release(); sThresoldImg?.release(); kernel5x5?.release()
wbRoiSrc.release(); roiSrc.release(); mask.release(); maskSrc.release()
buildContour.release()
return StripResult(errorCode = -11, paperColor = paperColor)
}
val autoRoiSrc = maskSrc.submat(autoRoi)
// Warp affine for rotation correction
var rotAngle = angle
if (rotAngle < -45) rotAngle += 90.0
val autoRoiCenter = Point(autoRoiSrc.width() * 0.5, autoRoiSrc.height() * 0.5)
val waMat = Imgproc.getRotationMatrix2D(autoRoiCenter, rotAngle, 1.0)
val waRoiSrc = Mat()
Imgproc.warpAffine(autoRoiSrc, waRoiSrc, waMat, autoRoiSrc.size(), Imgproc.INTER_LINEAR)
waMat.release()
val rotRect2W = Math.max(rotRect2.size.width, rotRect2.size.height).toInt()
val rotRect2H = Math.min(rotRect2.size.width, rotRect2.size.height).toInt()
val roi = Rect(
(autoRoiCenter.x - rotRect2W * 0.45).toInt(),
(autoRoiCenter.y - rotRect2H * 0.45).toInt(),
(rotRect2W * 0.9).toInt(),
(rotRect2H * 0.9).toInt()
)
// Clamp roi to waRoiSrc bounds (prevents crash when green blocks are imprecise)
val clampedRoi = Rect(
maxOf(0, roi.x),
maxOf(0, roi.y),
minOf(roi.width, waRoiSrc.cols() - maxOf(0, roi.x)),
minOf(roi.height, waRoiSrc.rows() - maxOf(0, roi.y))
)
if (clampedRoi.width <= 0 || clampedRoi.height <= 0) {
Log.d(TAG, "locateAndDetectCT fail: roi out of waRoiSrc bounds")
waRoiSrc.release(); autoRoiSrc.release(); maskSrc.release(); mask.release()
sChannel?.release(); sCpy?.release(); sThresoldImg?.release(); kernel5x5?.release()
wbRoiSrc.release(); roiSrc.release()
return StripResult(errorCode = -12, paperColor = paperColor)
}
val ctMat = waRoiSrc.submat(clampedRoi) // This is the cropped strip (C++: ctMat)
// =====================================================================
// PHASE 4: C/T LINE DETECTION ON CROPPED STRIP (C++ thresholds)
// =====================================================================
// C++ filter order: RightBottomX → LeftTopX → Rectangularity → Height(0.7) → Width(0.045)
val ctChannels = mutableListOf<Mat>()
Core.split(ctMat, ctChannels)
val lastRoiG = ctChannels[1] // G channel
ctChannels[0].release(); ctChannels[2].release()
// Denoise (C++: medianBlur 3, GaussianBlur 5x5)
Imgproc.medianBlur(lastRoiG, lastRoiG, 3)
Imgproc.GaussianBlur(lastRoiG, lastRoiG, Size(5.0, 5.0), 0.0, 0.0)
// Adaptive threshold (C++: ADAPTIVE_THRESH_MEAN_C, 37, 4)
val matInternal = Mat()
Imgproc.adaptiveThreshold(lastRoiG, matInternal, 255.0,
Imgproc.ADAPTIVE_THRESH_MEAN_C, Imgproc.THRESH_BINARY_INV, 37, 4.0)
// Find C/T contours
val ctContours = mutableListOf<MatOfPoint>()
Imgproc.findContours(matInternal, ctContours, Mat(), Imgproc.RETR_EXTERNAL, Imgproc.CHAIN_APPROX_SIMPLE)
if (ctContours.isEmpty()) {
Log.d(TAG, "locateAndDetectCT: no C/T contours found")
cleanupCT(ctMat, lastRoiG, matInternal, waRoiSrc, autoRoiSrc, maskSrc, mask)
sChannel?.release(); sCpy?.release(); sThresoldImg?.release(); kernel5x5?.release()
wbRoiSrc.release(); roiSrc.release()
buildContour.release()
return StripResult(errorCode = -7, paperColor = paperColor)
}
val ctGfs = mutableListOf<Pair<MatOfPoint, ContourGf>>()
ContourSelector.calContoursGf(ctContours, ctGfs, lastRoiG)
// C++ filter pipeline for C/T lines
val ctRbX = mutableListOf<Pair<MatOfPoint, ContourGf>>()
ContourSelector.selectContour(ctGfs, ctRbX,
lastRoiG.cols() * 0.15, lastRoiG.cols() * 0.95, GfFlag.RightBottomX)
val ctLtX = mutableListOf<Pair<MatOfPoint, ContourGf>>()
ContourSelector.selectContour(ctRbX, ctLtX,
lastRoiG.cols() * 0.05, lastRoiG.cols() * 0.9, GfFlag.LeftTopX)
val ctRect = mutableListOf<Pair<MatOfPoint, ContourGf>>()
ContourSelector.selectContour(ctLtX, ctRect, 0.30, 1.15, GfFlag.Rectangularity)
// C++: Height filter FIRST (0.71.1 of strip height), THEN Width (0.0450.20 of strip width)
val ctHeight = mutableListOf<Pair<MatOfPoint, ContourGf>>()
ContourSelector.selectContour(ctRect, ctHeight,
lastRoiG.rows() * 0.7, lastRoiG.rows() * 1.1, GfFlag.Height)
Log.d(TAG, "locateAndDetectCT C/T filter: raw=${ctGfs.size}" +
"rbX=${ctRbX.size} → ltX=${ctLtX.size}" +
"rect=${ctRect.size} → h(0.7)=${ctHeight.size}")
// Sort by CenterX
ContourSelector.sortContoursByGf(ctHeight, GfFlag.CenterX, false)
// C++ dedup: keep one contour per side (left/right of center)
val ctWidth = mutableListOf<Pair<MatOfPoint, ContourGf>>()
ctWidth.addAll(ctHeight)
if (ctWidth.size > 2) {
val midX = lastRoiG.cols() / 2.0
val leftContours = ctWidth.filter { it.second.contourCenter.x <= midX }
val rightContours = ctWidth.filter { it.second.contourCenter.x > midX }
ctWidth.clear()
if (leftContours.isNotEmpty()) {
ctWidth.add(leftContours.maxByOrNull { it.second.contourCenter.x }!!)
}
if (rightContours.isNotEmpty()) {
ctWidth.add(rightContours.minByOrNull { it.second.contourCenter.x }!!)
}
ctWidth.sortBy { it.second.contourCenter.x }
}
Log.d(TAG, "locateAndDetectCT: found ${ctWidth.size} C/T line(s) after dedup")
// =====================================================================
// RETRY: If only 0-1 C/T lines found, try adaptive threshold with
// smaller block size (21 vs 37) to pick up faint T lines.
// Also relax height filter (0.4 vs 0.7) and width filter (0.02 vs 0.045).
// =====================================================================
var ctRetryWidth = ctWidth
if (ctWidth.size < 2) {
Log.d(TAG, "locateAndDetectCT: retry C/T with relaxed params (block=21, h=0.4, w=0.02)")
// Release previous C/T detection mats
matInternal.release()
ctContours.forEach { it.release() }
ctContours.clear()
// Retry with smaller block size for adaptive threshold
val matInternal2 = Mat()
Imgproc.adaptiveThreshold(lastRoiG, matInternal2, 255.0,
Imgproc.ADAPTIVE_THRESH_MEAN_C, Imgproc.THRESH_BINARY_INV, 21, 4.0)
val ctContours2 = mutableListOf<MatOfPoint>()
Imgproc.findContours(matInternal2, ctContours2, Mat(), Imgproc.RETR_EXTERNAL, Imgproc.CHAIN_APPROX_SIMPLE)
if (ctContours2.isNotEmpty()) {
val ctGfs2 = mutableListOf<Pair<MatOfPoint, ContourGf>>()
ContourSelector.calContoursGf(ctContours2, ctGfs2, lastRoiG)
// Relaxed filter pipeline
val ctRbX2 = mutableListOf<Pair<MatOfPoint, ContourGf>>()
ContourSelector.selectContour(ctGfs2, ctRbX2,
lastRoiG.cols() * 0.15, lastRoiG.cols() * 0.95, GfFlag.RightBottomX)
val ctLtX2 = mutableListOf<Pair<MatOfPoint, ContourGf>>()
ContourSelector.selectContour(ctRbX2, ctLtX2,
lastRoiG.cols() * 0.05, lastRoiG.cols() * 0.9, GfFlag.LeftTopX)
val ctRect2 = mutableListOf<Pair<MatOfPoint, ContourGf>>()
ContourSelector.selectContour(ctLtX2, ctRect2, 0.30, 1.15, GfFlag.Rectangularity)
// Relaxed height: 0.4 instead of 0.7 (no width filter)
val ctHeight2 = mutableListOf<Pair<MatOfPoint, ContourGf>>()
ContourSelector.selectContour(ctRect2, ctHeight2,
lastRoiG.rows() * 0.4, lastRoiG.rows() * 1.1, GfFlag.Height)
Log.d(TAG, "locateAndDetectCT retry C/T filter: raw=${ctGfs2.size}" +
"rbX=${ctRbX2.size} → ltX=${ctLtX2.size}" +
"rect=${ctRect2.size} → h(0.4)=${ctHeight2.size}")
// Sort and dedup
ContourSelector.sortContoursByGf(ctHeight2, GfFlag.CenterX, false)
val ctWidth2 = mutableListOf<Pair<MatOfPoint, ContourGf>>()
ctWidth2.addAll(ctHeight2)
if (ctWidth2.size > 2) {
val midX = lastRoiG.cols() / 2.0
val leftContours = ctWidth2.filter { it.second.contourCenter.x <= midX }
val rightContours = ctWidth2.filter { it.second.contourCenter.x > midX }
ctWidth2.clear()
if (leftContours.isNotEmpty()) {
ctWidth2.add(leftContours.maxByOrNull { it.second.contourCenter.x }!!)
}
if (rightContours.isNotEmpty()) {
ctWidth2.add(rightContours.minByOrNull { it.second.contourCenter.x }!!)
}
ctWidth2.sortBy { it.second.contourCenter.x }
}
Log.d(TAG, "locateAndDetectCT retry: found ${ctWidth2.size} C/T line(s) after dedup")
ctRetryWidth = ctWidth2
}
// Cleanup retry intermediate mats (but keep ctRetryWidth entries)
matInternal2.release()
ctContours2.forEach { it.release() }
}
// =====================================================================
// PHASE 5: EXTRACT ROIs (C++ style)
// =====================================================================
val leftImg = Mat()
val rightImg = Mat()
val wbImg = Mat()
var errorCode = 100
var isNegative = false
when (ctRetryWidth.size) {
2 -> {
// Normal case: both C and T lines detected
val gfLeft = ctRetryWidth[0]; val gfRight = ctRetryWidth[1]
val leftRotRect = gfLeft.second.rotRect
val rightRotRect = gfRight.second.rotRect
val r1 = leftRotRect.boundingRect()
val r2 = rightRotRect.boundingRect()
val wbCenterX = ((leftRotRect.center.x + rightRotRect.center.x) * 0.5).toInt()
val wbCenterY = ((leftRotRect.center.y + rightRotRect.center.y) * 0.5).toInt()
val w = ((r1.width + r2.width) * 0.2).toInt()
val wbRoi = Rect(wbCenterX - w, wbCenterY - w, w * 2, w * 2)
val r1C = clampRect(r1, ctMat.cols(), ctMat.rows())
val r2C = clampRect(r2, ctMat.cols(), ctMat.rows())
val wbRoiC = clampRect(wbRoi, ctMat.cols(), ctMat.rows())
if (wbRoiC.height < MIN_PIX_VAL || wbRoiC.width < MIN_PIX_VAL) {
errorCode = 3
} else {
ctMat.submat(r1C).copyTo(leftImg)
ctMat.submat(r2C).copyTo(rightImg)
ctMat.submat(wbRoiC).copyTo(wbImg)
errorCode = 0
}
}
1 -> {
// Single line detected — check if it's the C line (C++ approach)
isNegative = true
val gfInternal = ctRetryWidth[0]
val rotRect = gfInternal.second.rotRect
val innerRoi = rotRect.boundingRect()
val innerRoiC = clampRect(innerRoi, ctMat.cols(), ctMat.rows())
val gMidX = ctMat.cols() / 2
// C++: C line is on the right when not rotated, or on the left when rotated
val bRight = !bRot && rotRect.center.x > gMidX
val bLeft = bRot && rotRect.center.x < gMidX
if (bRight || bLeft) {
// Extract white balance ROI from middle of strip
val midX = ctMat.cols() / 2; val midY = ctMat.rows() / 2
val wbSize = minOf(ctMat.cols() / 6, ctMat.rows() / 2)
val wbRoiC = clampRect(Rect(maxOf(0, midX - wbSize), maxOf(0, midY - wbSize),
minOf(wbSize * 2, ctMat.cols()), minOf(wbSize * 2, ctMat.rows())),
ctMat.cols(), ctMat.rows())
ctMat.submat(wbRoiC).copyTo(wbImg)
// Create artificial T line on the opposite side (C++ logic)
val artRoi = Rect(innerRoiC.x, innerRoiC.y, innerRoiC.width, innerRoiC.height)
if (bRight) {
val p = innerRoiC.x / maxOf(innerRoiC.width, 1)
artRoi.x = when {
p > 3 -> innerRoiC.x - innerRoiC.width * 3
p > 2 -> innerRoiC.x - innerRoiC.width * 2
else -> 1
}
}
if (bLeft) {
val p = (ctMat.cols() - innerRoiC.x) / maxOf(innerRoiC.width, 1)
artRoi.x = when {
p > 4 -> innerRoiC.x + innerRoiC.width * 3
p > 3 -> innerRoiC.x + innerRoiC.width * 2
else -> innerRoiC.x + innerRoiC.width
}
}
val artRoiC = clampRect(artRoi, ctMat.cols(), ctMat.rows())
ctMat.submat(innerRoiC).copyTo(rightImg)
ctMat.submat(artRoiC).copyTo(leftImg)
errorCode = 0
}
}
else -> {
errorCode = 100
}
}
Log.d(TAG, "locateAndDetectCT: errorCode=$errorCode, isNegative=$isNegative")
// --- Draw C/T area contour on srcRgba for visual feedback ---
// When C/T detection succeeds, draw the rotated rect outline of the
// C/T analysis area on the camera frame. This matches the C++ DrawRect()
// pattern (drawContours with offset = rcRoi).
if (errorCode == 0) {
Imgproc.drawContours(srcRgba, listOf(buildContour), 0,
Scalar(255.0, 0.0, 0.0, 255.0), 3, Imgproc.LINE_8, Mat(), 0,
Point(roiRect.x.toDouble(), roiRect.y.toDouble()))
}
buildContour.release()
// Cleanup
cleanupCT(ctMat, lastRoiG, matInternal, waRoiSrc, autoRoiSrc, maskSrc, mask)
sChannel?.release(); sCpy?.release(); sThresoldImg?.release(); kernel5x5?.release()
wbRoiSrc.release(); roiSrc.release()
return StripResult(
errorCode = errorCode,
isNegative = isNegative,
leftImg = if (leftImg.empty()) null else leftImg,
rightImg = if (rightImg.empty()) null else rightImg,
wbImg = if (wbImg.empty()) null else wbImg,
paperColor = paperColor
)
}
/** Cleanup helper for C/T detection intermediate mats */
private fun cleanupCT(ctMat: Mat, lastRoiG: Mat, matInternal: Mat,
waRoiSrc: Mat, autoRoiSrc: Mat, maskSrc: Mat, mask: Mat) {
ctMat.release(); lastRoiG.release(); matInternal.release()
waRoiSrc.release(); autoRoiSrc.release(); maskSrc.release(); mask.release()
}
// --- Private helpers ---
/**
@@ -1128,7 +1845,7 @@ object StripLocator {
lastRoiSrc: Mat, lastRoiG: Mat, matInternal: Mat,
lastRoiSrcMats: List<Mat>
) {
s.release(); sCpy.release(); sThresoldImg.release(); roiSrc.release()
s.release(); sCpy?.release(); sThresoldImg?.release(); roiSrc.release()
mask.release(); maskSrc.release(); autoRoiSrc.release(); waRoiSrc.release()
lastRoiSrc.release(); lastRoiG.release(); matInternal.release()
lastRoiSrcMats.forEach { it.release() }