的方式
This commit is contained in:
@@ -16,6 +16,7 @@ import org.opencv.core.Core
|
||||
import org.opencv.core.CvType
|
||||
import org.opencv.core.Mat
|
||||
import org.opencv.core.Rect
|
||||
import org.opencv.imgproc.Imgproc
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
/**
|
||||
@@ -157,8 +158,12 @@ class ScanViewModel : ViewModel() {
|
||||
roiRect = Rect(0, 0, procW, procH)
|
||||
}
|
||||
|
||||
// Step 1: Locate the strip
|
||||
val stripResult = StripLocator.locate(srcRgba, roiRect!!, drawAnnotations = true)
|
||||
// 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()
|
||||
|
||||
if (stripResult.errorCode != 0) {
|
||||
// Update preview bitmap even on failure (shows the source image)
|
||||
|
||||
@@ -61,6 +61,10 @@ object StripLocator {
|
||||
// --- Constants ---
|
||||
private const val H_GREEN_MIN = 30.5
|
||||
private const val H_GREEN_MAX = 85.0
|
||||
private const val H_YELLOW_MIN = 14.0
|
||||
private const val H_YELLOW_MAX = 29.0
|
||||
private const val H_BLUE_MIN = 95.0
|
||||
private const val H_BLUE_MAX = 139.5
|
||||
private const val MIN_PIX_VAL = 4
|
||||
|
||||
// Morphology kernels (initialized in init block)
|
||||
@@ -111,48 +115,160 @@ object StripLocator {
|
||||
)
|
||||
}
|
||||
|
||||
// --- Step 1: Convert to grayscale and OTSU threshold ---
|
||||
// =====================================================================
|
||||
// GREEN BLOCK DETECTION — ported from ImageLocationColloidalGold.cpp
|
||||
// Key improvements over the old grayscale+OTSU approach:
|
||||
// 1. Uses HSV S-channel (saturation) for OTSU — green blocks have high
|
||||
// saturation, making them stand out even under reflections/glare.
|
||||
// 2. 5×5 morphology kernel (was 15×15) — preserves green block shapes
|
||||
// instead of merging or destroying them.
|
||||
// 3. Color pre-filtering — each contour is checked for green color BEFORE
|
||||
// being used. Non-green contours (from reflections, shadows) are discarded.
|
||||
// 4. Absolute area filtering (150–100000 px) — more robust than expecting
|
||||
// exactly 2 contours.
|
||||
// 5. Adaptive height filtering relative to the largest contour.
|
||||
// =====================================================================
|
||||
|
||||
// --- Step 1: Convert to HSV, extract S (saturation) channel, OTSU ---
|
||||
// OLD APPROACH (grayscale OTSU, commented out):
|
||||
// val s = Mat()
|
||||
// Imgproc.cvtColor(roiSrc, s, Imgproc.COLOR_RGBA2GRAY)
|
||||
// Imgproc.threshold(s, sThresoldImg, 128.0, 255.0, THRESH_BINARY | THRESH_OTSU)
|
||||
// Problem: grayscale is sensitive to brightness/reflections. Green blocks
|
||||
// may not contrast well with background under glare.
|
||||
val s = Mat()
|
||||
Imgproc.cvtColor(roiSrc, s, Imgproc.COLOR_RGBA2GRAY)
|
||||
Imgproc.cvtColor(roiSrc, roiSrc, Imgproc.COLOR_RGBA2RGB) // RGBA → BGR
|
||||
val hsv = Mat()
|
||||
Imgproc.cvtColor(roiSrc, hsv, Imgproc.COLOR_RGB2HSV) // BGR → HSV
|
||||
val hsvMats = mutableListOf<Mat>()
|
||||
Core.split(hsv, hsvMats)
|
||||
val sChannel = hsvMats[1] // S channel — saturation is robust against brightness
|
||||
hsvMats[0].release(); hsvMats[2].release(); hsv.release()
|
||||
|
||||
val sCpy = Mat()
|
||||
s.copyTo(sCpy)
|
||||
sChannel.copyTo(sCpy)
|
||||
|
||||
val sThresoldImg = Mat()
|
||||
Imgproc.threshold(s, sThresoldImg, 128.0, 255.0,
|
||||
Imgproc.threshold(sChannel, sThresoldImg, 128.0, 255.0,
|
||||
Imgproc.THRESH_BINARY or Imgproc.THRESH_OTSU)
|
||||
|
||||
// --- Step 2: Morphological open ---
|
||||
Imgproc.morphologyEx(sThresoldImg, sThresoldImg, Imgproc.MORPH_OPEN, m2)
|
||||
// --- Step 2: Morphological open with 5×5 kernel (was 15×15) ---
|
||||
// OLD: m2 = 15×15 kernel — too large, merges or destroys green blocks
|
||||
// NEW: 5×5 kernel — cleans noise while preserving block shapes
|
||||
val kernel5x5 = Imgproc.getStructuringElement(Imgproc.MORPH_RECT, Size(5.0, 5.0))
|
||||
Imgproc.morphologyEx(sThresoldImg, sThresoldImg, Imgproc.MORPH_OPEN, kernel5x5)
|
||||
|
||||
// --- Step 3: 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, "Step 3 fail: no contours found after OTSU+morph")
|
||||
s.release(); sCpy.release(); sThresoldImg.release(); roiSrc.release()
|
||||
Log.d(TAG, "Step 3 fail: no contours found after OTSU+morph (S-channel)")
|
||||
sChannel.release(); sCpy.release(); sThresoldImg.release(); kernel5x5.release()
|
||||
roiSrc.release()
|
||||
return StripResult(errorCode = -9, paperColor = PaperColor.Unknown)
|
||||
}
|
||||
|
||||
// --- Step 4: Calculate geometric features ---
|
||||
val gfsList0 = mutableListOf<Pair<MatOfPoint, ContourGf>>()
|
||||
ContourSelector.calContoursGf(contours0, gfsList0, s)
|
||||
if (gfsList0.size != 2) {
|
||||
Log.d(TAG, "Step 4 fail: expected 2 contours, got ${gfsList0.size}")
|
||||
s.release(); sCpy.release(); sThresoldImg.release(); roiSrc.release()
|
||||
return StripResult(errorCode = -9, paperColor = PaperColor.Unknown)
|
||||
val gfsListAll = mutableListOf<Pair<MatOfPoint, ContourGf>>()
|
||||
ContourSelector.calContoursGf(contours0, gfsListAll, sChannel)
|
||||
|
||||
// Filter by position: RightBottomX
|
||||
val gfsFiltered1 = mutableListOf<Pair<MatOfPoint, ContourGf>>()
|
||||
ContourSelector.selectContour(gfsListAll, gfsFiltered1,
|
||||
sChannel.cols() * 0.05, sChannel.cols() * 0.95, GfFlag.RightBottomX)
|
||||
|
||||
// Filter by position: LeftTopX
|
||||
val gfsFiltered2 = mutableListOf<Pair<MatOfPoint, ContourGf>>()
|
||||
ContourSelector.selectContour(gfsFiltered1, gfsFiltered2,
|
||||
sChannel.cols() * 0.05, sChannel.cols() * 0.95, GfFlag.LeftTopX)
|
||||
|
||||
// Filter by absolute area (50–100000 px) — lowered from 150 to catch small blocks in camera frames
|
||||
val gfsFiltered3 = mutableListOf<Pair<MatOfPoint, ContourGf>>()
|
||||
ContourSelector.selectContour(gfsFiltered2, gfsFiltered3, 50.0, 100000.0, GfFlag.Area)
|
||||
|
||||
// Filter by rectangularity (0.45–1.1) — relaxed from 0.55 for camera
|
||||
val gfsFiltered4 = mutableListOf<Pair<MatOfPoint, ContourGf>>()
|
||||
ContourSelector.selectContour(gfsFiltered3, gfsFiltered4, 0.45, 1.1, GfFlag.Rectangularity)
|
||||
|
||||
// --- Color pre-filtering: only keep contours that are actually green ---
|
||||
// This is the key anti-reflection measure — non-green contours
|
||||
// (from glare, shadows, background) are discarded here.
|
||||
val gfsGreenOnly = mutableListOf<Pair<MatOfPoint, ContourGf>>()
|
||||
for ((contour, gf) in gfsFiltered4) {
|
||||
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) {
|
||||
gfsGreenOnly.add(Pair(contour, gf))
|
||||
}
|
||||
}
|
||||
}
|
||||
Log.d(TAG, "locate filter stages: raw=${gfsListAll.size} → " +
|
||||
"rbX=${gfsFiltered1.size} → ltX=${gfsFiltered2.size} → " +
|
||||
"area=${gfsFiltered3.size} → rect=${gfsFiltered4.size} → green=${gfsGreenOnly.size}")
|
||||
|
||||
// --- Fallback: if S-channel OTSU didn't find enough green contours, ---
|
||||
// try direct HSV color thresholding (H channel for green + S channel for saturation).
|
||||
// This is more robust for camera feeds where S-channel OTSU fails.
|
||||
var effectiveGreenContours = gfsGreenOnly
|
||||
if (gfsGreenOnly.size < 2) {
|
||||
Log.d(TAG, "S-channel OTSU found <2 green contours, trying direct HSV thresholding fallback")
|
||||
val hsvFallbackContours = tryDirectHsvThresholding(roiSrc)
|
||||
if (hsvFallbackContours.size >= 2) {
|
||||
Log.d(TAG, "Direct HSV fallback: found ${hsvFallbackContours.size} green contours")
|
||||
effectiveGreenContours = hsvFallbackContours
|
||||
} else {
|
||||
Log.d(TAG, "Direct HSV fallback also failed: ${hsvFallbackContours.size} contours")
|
||||
}
|
||||
}
|
||||
|
||||
// --- Step 5: Sort by area, validate area ratio [1.5, 4.5] ---
|
||||
ContourSelector.sortContoursByGf(gfsList0, GfFlag.Area, true)
|
||||
val maxArea = gfsList0[0].second.contourArea
|
||||
val secondArea = gfsList0[1].second.contourArea
|
||||
if (maxArea.toDouble() / secondArea < 1.2 || maxArea.toDouble() / secondArea > 12.0) {
|
||||
Log.d(TAG, "Step 5 fail: area ratio ${maxArea.toDouble() / secondArea} not in [1.2, 12.0]")
|
||||
s.release(); sCpy.release(); sThresoldImg.release(); roiSrc.release()
|
||||
return StripResult(errorCode = -8, paperColor = PaperColor.Unknown)
|
||||
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()
|
||||
roiSrc.release()
|
||||
return StripResult(errorCode = -9, paperColor = PaperColor.Green)
|
||||
}
|
||||
|
||||
// --- Step 6: Validate aspect ratio and position ---
|
||||
// --- Adaptive height filtering relative to the largest contour ---
|
||||
// Sort by height, filter to keep contours with height >= 50% of largest
|
||||
ContourSelector.sortContoursByGf(effectiveGreenContours, GfFlag.Height, true)
|
||||
val maxHeight = effectiveGreenContours[0].second.size.height
|
||||
val gfsFilteredH = mutableListOf<Pair<MatOfPoint, ContourGf>>()
|
||||
ContourSelector.selectContour(effectiveGreenContours, gfsFilteredH,
|
||||
maxHeight * 0.50, maxHeight * 1.0, GfFlag.Height)
|
||||
|
||||
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()
|
||||
roiSrc.release()
|
||||
return StripResult(errorCode = -9, paperColor = PaperColor.Green)
|
||||
}
|
||||
|
||||
// Sort by center X for left/right ordering
|
||||
ContourSelector.sortContoursByGf(gfsFilteredH, GfFlag.CenterX, false)
|
||||
|
||||
// --- Area ratio validation (2.0–4.0) ---
|
||||
val firstArea = gfsFilteredH[0].second.contourArea
|
||||
val lastArea = gfsFilteredH[gfsFilteredH.size - 1].second.contourArea
|
||||
val areaRatio = if (firstArea > lastArea) firstArea.toDouble() / lastArea
|
||||
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()
|
||||
roiSrc.release()
|
||||
return StripResult(errorCode = -8, paperColor = PaperColor.Green)
|
||||
}
|
||||
|
||||
// Keep only the largest and smallest (leftmost and rightmost) contours
|
||||
val gfsList0 = mutableListOf(gfsFilteredH[0], gfsFilteredH[gfsFilteredH.size - 1])
|
||||
|
||||
// --- Step 6: Validate position (boundary check only) ---
|
||||
// Note: whRatio is NOT checked on the large block (C++ doesn't check it).
|
||||
// Small block whRatio and height similarity are checked later.
|
||||
val gfp0 = gfsList0[0]
|
||||
val leftX = gfp0.second.leftTop.x.toInt()
|
||||
val topY = gfp0.second.leftTop.y.toInt()
|
||||
@@ -165,21 +281,23 @@ object StripLocator {
|
||||
val realHalfWBig = (Math.max(rotRcW, rotRcH) * 0.5).toInt()
|
||||
val realHalfHBig = (Math.min(rotRcW, rotRcH) * 0.5).toInt()
|
||||
|
||||
val whRatio = realHalfWBig.toDouble() / realHalfHBig
|
||||
|
||||
if (whRatio < 4.25 || whRatio > 13.5
|
||||
|| realHalfHBig < MIN_PIX_VAL * 2
|
||||
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, "Step 6 fail: whRatio=$whRatio, halfH=$realHalfHBig, bounds=($leftX,$topY)-($rightX,$bottomY) src=${srcWid}x${srcHei}")
|
||||
s.release(); sCpy.release(); sThresoldImg.release(); roiSrc.release()
|
||||
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()
|
||||
return StripResult(errorCode = -10, paperColor = PaperColor.Unknown)
|
||||
}
|
||||
|
||||
// --- Step 7: Judge paper color ---
|
||||
// --- Step 7: Auto white balance, then judge paper color ---
|
||||
// OLD: judged paper color directly on roiSrc
|
||||
// NEW: apply auto white balance first to compensate for lighting variations
|
||||
val wbRoiSrc = Mat()
|
||||
autoWhiteBalance(roiSrc, wbRoiSrc)
|
||||
|
||||
val rcColor1 = Rect(
|
||||
(rotRect0.center.x - realHalfHBig / 2).toInt(),
|
||||
(rotRect0.center.y - realHalfHBig / 2).toInt(),
|
||||
@@ -193,7 +311,8 @@ object StripLocator {
|
||||
|| rcColor1.x <= 0 || rcColor1.x + rcColor1.width >= srcWid
|
||||
|| rcColor1.y <= 0 || rcColor1.y + rcColor1.height >= srcHei
|
||||
) {
|
||||
s.release(); sCpy.release(); sThresoldImg.release(); roiSrc.release()
|
||||
sChannel.release(); sCpy.release(); sThresoldImg.release(); kernel5x5.release()
|
||||
wbRoiSrc.release(); roiSrc.release()
|
||||
return StripResult(errorCode = -11, paperColor = PaperColor.Unknown)
|
||||
}
|
||||
|
||||
@@ -210,13 +329,14 @@ object StripLocator {
|
||||
// Continue anyway — original code warned but didn't always return
|
||||
}
|
||||
|
||||
// --- Step 9: Judge paper color of the larger block ---
|
||||
val mc1 = roiSrc.submat(rcColor1)
|
||||
// --- Step 9: Judge paper color of the larger block (on white-balanced image) ---
|
||||
val mc1 = wbRoiSrc.submat(rcColor1)
|
||||
paperColor = judgePaperColor(mc1)
|
||||
mc1.release()
|
||||
if (paperColor == PaperColor.Unknown || paperColor != PaperColor.Green) {
|
||||
Log.d(TAG, "Step 9 fail: paperColor=$paperColor")
|
||||
s.release(); sCpy.release(); sThresoldImg.release(); roiSrc.release()
|
||||
sChannel.release(); sCpy.release(); sThresoldImg.release(); kernel5x5.release()
|
||||
wbRoiSrc.release(); roiSrc.release()
|
||||
return StripResult(errorCode = -1, paperColor = paperColor)
|
||||
}
|
||||
|
||||
@@ -230,11 +350,13 @@ object StripLocator {
|
||||
bWgtH = false
|
||||
realAngle = -angle - 90
|
||||
} else {
|
||||
s.release(); sCpy.release(); sThresoldImg.release(); roiSrc.release()
|
||||
Log.d(TAG, "Step 10 fail: rotRcH == rotRcW (square block, cannot determine orientation)")
|
||||
sChannel.release(); sCpy.release(); sThresoldImg.release(); kernel5x5.release()
|
||||
wbRoiSrc.release(); roiSrc.release()
|
||||
return StripResult(errorCode = -1, paperColor = paperColor)
|
||||
}
|
||||
|
||||
// Verify second block is also green
|
||||
// Verify second block is also green (on white-balanced image)
|
||||
val gfp1 = gfsList0[1]
|
||||
val rotRect1 = gfp1.second.rotRect
|
||||
val rotRcWSmall = rotRect1.size.width.toInt()
|
||||
@@ -246,11 +368,34 @@ object StripLocator {
|
||||
(rotRect1.center.y - realHalfHSmall / 2).toInt(),
|
||||
realHalfHSmall, realHalfHSmall
|
||||
)
|
||||
val mc2 = roiSrc.submat(rcColor2)
|
||||
val mc2 = wbRoiSrc.submat(rcColor2)
|
||||
val color2 = judgePaperColor(mc2)
|
||||
mc2.release()
|
||||
if (paperColor != color2) {
|
||||
s.release(); sCpy.release(); sThresoldImg.release(); roiSrc.release()
|
||||
Log.d(TAG, "Step 10 fail: second block color mismatch: first=$paperColor second=$color2")
|
||||
sChannel.release(); sCpy.release(); sThresoldImg.release(); kernel5x5.release()
|
||||
wbRoiSrc.release(); roiSrc.release()
|
||||
return StripResult(errorCode = -1, paperColor = paperColor)
|
||||
}
|
||||
|
||||
// --- Small block whRatio check (from C++ _filterAndSaveLocatorsInfo) ---
|
||||
// Physical specs: small block 10mm wide, strip 3-5mm tall → whRatio 2~3.3
|
||||
// Relaxed to 1.5~7.0 — camera angle and OTSU segmentation can stretch the apparent ratio
|
||||
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()
|
||||
wbRoiSrc.release(); roiSrc.release()
|
||||
return StripResult(errorCode = -1, paperColor = paperColor)
|
||||
}
|
||||
|
||||
// --- Height similarity check: both blocks should have similar heights ---
|
||||
// Physical specs: both blocks are on the same strip, heights should match within 20%
|
||||
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()
|
||||
wbRoiSrc.release(); roiSrc.release()
|
||||
return StripResult(errorCode = -1, paperColor = paperColor)
|
||||
}
|
||||
|
||||
@@ -261,7 +406,9 @@ object StripLocator {
|
||||
)
|
||||
val distRadio = centerPointDist / (realHalfWBig * 2.0)
|
||||
if (distRadio < 1 || distRadio > 1.65) {
|
||||
s.release(); sCpy.release(); sThresoldImg.release(); roiSrc.release()
|
||||
Log.d(TAG, "Step 10c fail: center distance ratio $distRadio not in [1, 1.65]")
|
||||
sChannel.release(); sCpy.release(); sThresoldImg.release(); kernel5x5.release()
|
||||
wbRoiSrc.release(); roiSrc.release()
|
||||
return StripResult(errorCode = -1, paperColor = paperColor)
|
||||
}
|
||||
|
||||
@@ -324,7 +471,8 @@ object StripLocator {
|
||||
|| (autoRoi.x + autoRoi.width) >= (maskSrc.width() - 1)
|
||||
|| (autoRoi.y + autoRoi.height) >= (maskSrc.height() - 1)
|
||||
) {
|
||||
s.release(); sCpy.release(); sThresoldImg.release(); roiSrc.release()
|
||||
sChannel.release(); sCpy.release(); sThresoldImg.release(); kernel5x5.release()
|
||||
wbRoiSrc.release(); roiSrc.release()
|
||||
mask.release(); maskSrc.release()
|
||||
return StripResult(errorCode = -11, paperColor = paperColor)
|
||||
}
|
||||
@@ -549,6 +697,91 @@ object StripLocator {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Direct HSV color thresholding fallback for green block detection.
|
||||
*
|
||||
* When S-channel OTSU fails to find enough green contours (common in camera
|
||||
* feeds where the strip is small), this method directly thresholds the H and S
|
||||
* channels of HSV to find green regions.
|
||||
*
|
||||
* @param roiSrc The ROI image in BGR format (3 channels)
|
||||
* @return List of (contour, geometric features) pairs that pass filtering
|
||||
*/
|
||||
private fun tryDirectHsvThresholding(roiSrc: Mat): MutableList<Pair<MatOfPoint, ContourGf>> {
|
||||
val result = mutableListOf<Pair<MatOfPoint, ContourGf>>()
|
||||
|
||||
// Convert BGR to HSV
|
||||
val hsv = Mat()
|
||||
Imgproc.cvtColor(roiSrc, hsv, Imgproc.COLOR_RGB2HSV)
|
||||
val hsvChannels = mutableListOf<Mat>()
|
||||
Core.split(hsv, hsvChannels)
|
||||
val hChannel = hsvChannels[0] // Hue
|
||||
val sChannel = hsvChannels[1] // Saturation
|
||||
|
||||
// Create green mask: H in [30, 85] AND S > 30
|
||||
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)
|
||||
|
||||
val hMask = Mat()
|
||||
Core.bitwise_and(hMaskLow, hMaskHigh, hMask)
|
||||
val greenMask = Mat()
|
||||
Core.bitwise_and(hMask, sMask, greenMask)
|
||||
|
||||
// Morphological open to clean noise
|
||||
val kernel3x3 = Imgproc.getStructuringElement(Imgproc.MORPH_RECT, Size(3.0, 3.0))
|
||||
Imgproc.morphologyEx(greenMask, greenMask, Imgproc.MORPH_OPEN, kernel3x3)
|
||||
|
||||
// Find contours
|
||||
val contours = mutableListOf<MatOfPoint>()
|
||||
Imgproc.findContours(greenMask, contours, Mat(), Imgproc.RETR_EXTERNAL, Imgproc.CHAIN_APPROX_SIMPLE)
|
||||
|
||||
if (contours.isEmpty()) {
|
||||
hsv.release(); hChannel.release(); sChannel.release()
|
||||
hMaskLow.release(); hMaskHigh.release(); sMask.release()
|
||||
hMask.release(); greenMask.release(); kernel3x3.release()
|
||||
hsvChannels[2].release()
|
||||
return result
|
||||
}
|
||||
|
||||
// Calculate geometric features
|
||||
val gfsAll = mutableListOf<Pair<MatOfPoint, ContourGf>>()
|
||||
ContourSelector.calContoursGf(contours, gfsAll, sChannel)
|
||||
|
||||
// Filter by position and size (same pipeline as S-channel OTSU)
|
||||
val gfs1 = mutableListOf<Pair<MatOfPoint, ContourGf>>()
|
||||
ContourSelector.selectContour(gfsAll, gfs1,
|
||||
roiSrc.cols() * 0.05, roiSrc.cols() * 0.95, GfFlag.RightBottomX)
|
||||
|
||||
val gfs2 = mutableListOf<Pair<MatOfPoint, ContourGf>>()
|
||||
ContourSelector.selectContour(gfs1, gfs2,
|
||||
roiSrc.cols() * 0.05, roiSrc.cols() * 0.95, GfFlag.LeftTopX)
|
||||
|
||||
val gfs3 = mutableListOf<Pair<MatOfPoint, ContourGf>>()
|
||||
ContourSelector.selectContour(gfs2, gfs3, 30.0, 100000.0, GfFlag.Area)
|
||||
|
||||
val gfs4 = mutableListOf<Pair<MatOfPoint, ContourGf>>()
|
||||
ContourSelector.selectContour(gfs3, gfs4, 0.40, 1.1, GfFlag.Rectangularity)
|
||||
|
||||
Log.d(TAG, "Direct HSV fallback stages: raw=${gfsAll.size} → " +
|
||||
"rbX=${gfs1.size} → ltX=${gfs2.size} → " +
|
||||
"area=${gfs3.size} → rect=${gfs4.size}")
|
||||
|
||||
// All contours from HSV thresholding are already green by definition,
|
||||
// so no need for color pre-filtering. Just return them.
|
||||
result.addAll(gfs4)
|
||||
|
||||
hsv.release(); hChannel.release(); sChannel.release()
|
||||
hMaskLow.release(); hMaskHigh.release(); sMask.release()
|
||||
hMask.release(); greenMask.release(); kernel3x3.release()
|
||||
hsvChannels[2].release()
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect C/T lines and extract ROIs from an already-cropped strip image.
|
||||
*
|
||||
@@ -616,11 +849,11 @@ object StripLocator {
|
||||
|
||||
val gfsList102 = mutableListOf<Pair<MatOfPoint, ContourGf>>()
|
||||
ContourSelector.selectContour(gfsList101, gfsList102,
|
||||
imgW * 0.01, imgW * 0.35, GfFlag.Width)
|
||||
imgW * 0.005, imgW * 0.35, GfFlag.Width)
|
||||
|
||||
val gfsList11 = mutableListOf<Pair<MatOfPoint, ContourGf>>()
|
||||
ContourSelector.selectContour(gfsList102, gfsList11,
|
||||
imgH * 0.15, imgH * 1.1, GfFlag.Height)
|
||||
imgH * 0.02, imgH * 1.1, GfFlag.Height)
|
||||
|
||||
Log.d(TAG, "locateCroppedStrip filter stages: raw=${gfsList10.size} → " +
|
||||
"rbX=${gfsList01.size} → ltX=${gfsList110.size} → " +
|
||||
@@ -737,9 +970,58 @@ object StripLocator {
|
||||
|
||||
// --- Private helpers ---
|
||||
|
||||
/**
|
||||
* Auto white balance — compensates for lighting variations.
|
||||
* Scales each channel so their means equal the overall mean.
|
||||
* Ported from ImageLocationColloidalGold::_autoWhitebalance.
|
||||
*/
|
||||
private fun autoWhiteBalance(src: Mat, dst: Mat) {
|
||||
val avgRgb = Core.mean(src)
|
||||
val scale = (avgRgb.`val`[0] + avgRgb.`val`[1] + avgRgb.`val`[2]) / 3.0
|
||||
val mv = mutableListOf<Mat>()
|
||||
Core.split(src, mv)
|
||||
// OpenCV BGR order: mv[0]=B, mv[1]=G, mv[2]=R
|
||||
Core.convertScaleAbs(mv[0], mv[0], scale / (avgRgb.`val`[0] + 0.001), 0.0)
|
||||
Core.convertScaleAbs(mv[1], mv[1], scale / (avgRgb.`val`[1] + 0.001), 0.0)
|
||||
Core.convertScaleAbs(mv[2], mv[2], scale / (avgRgb.`val`[2] + 0.001), 0.0)
|
||||
Core.merge(mv, dst)
|
||||
mv.forEach { it.release() }
|
||||
}
|
||||
|
||||
/**
|
||||
* Judge paper color at a specific contour center point.
|
||||
* Samples a 10×10 region around the point and checks HSV.
|
||||
* Ported from ImageLocationColloidalGold::_readPaperColor(ContourGfExtend*).
|
||||
*/
|
||||
private fun judgePaperColorAtPoint(sample: Mat, hsvMats: List<Mat>): PaperColor {
|
||||
Imgproc.blur(sample, sample, Size(3.0, 3.0))
|
||||
val mcAvgBgr = Core.mean(sample)
|
||||
val avgB = mcAvgBgr.`val`[0]
|
||||
val avgG = mcAvgBgr.`val`[1]
|
||||
val avgR = mcAvgBgr.`val`[2]
|
||||
|
||||
// Get HSV H value at the sample center from the pre-computed HSV
|
||||
val hsvSample = Mat()
|
||||
Imgproc.cvtColor(sample, hsvSample, Imgproc.COLOR_RGB2HSV)
|
||||
val hsvSplit = mutableListOf<Mat>()
|
||||
Core.split(hsvSample, hsvSplit)
|
||||
val hVal = Core.mean(hsvSplit[0]).`val`[0]
|
||||
hsvSplit.forEach { it.release() }
|
||||
hsvSample.release()
|
||||
|
||||
if ((avgR > avgB && avgG > avgB) && (hVal >= H_YELLOW_MIN && hVal <= H_YELLOW_MAX))
|
||||
return PaperColor.Yellow
|
||||
if ((avgG > avgB && avgG > avgR) && (hVal >= H_GREEN_MIN && hVal <= H_GREEN_MAX))
|
||||
return PaperColor.Green
|
||||
if ((avgB > avgG && avgB > avgR) && (hVal >= H_BLUE_MIN && hVal <= H_BLUE_MAX))
|
||||
return PaperColor.Blue
|
||||
return PaperColor.Unknown
|
||||
}
|
||||
|
||||
/**
|
||||
* Judge paper color from an HSV analysis of the image region.
|
||||
* Only returns [PaperColor.Green] or [PaperColor.Unknown] for green strips.
|
||||
* Uses relaxed H stddev threshold (35 instead of 22.5) for better tolerance
|
||||
* of varying lighting — ported from ImageLocationColloidalGold::_readPaperColor.
|
||||
*/
|
||||
private fun judgePaperColor(img: Mat): PaperColor {
|
||||
Imgproc.blur(img, img, Size(3.0, 3.0))
|
||||
@@ -760,7 +1042,8 @@ object StripLocator {
|
||||
// Release split channels
|
||||
mvHsv.forEach { it.release() }
|
||||
|
||||
if (d < 22.5) {
|
||||
// C++ uses d < 35 (was 22.5) — more tolerant of lighting variation
|
||||
if (d < 35.0) {
|
||||
if ((avgG > avgB && avgG > avgR) && (avgH >= H_GREEN_MIN && avgH <= H_GREEN_MAX)) {
|
||||
return PaperColor.Green
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user