@@ -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 50– 100000 (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.55– 1.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.0– 4.0, relaxed to 1.0– 30.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.0– 15.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.3– 5.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.0– 1.7, relaxed to 0.8– 1.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.7– 1.1 of strip height), THEN Width (0.045– 0.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 ( ) }