OpenCV - 在视频和图像中找到黑板边缘

5

更新

您可以在我的GitHub上找到我用于测试的所有图像:

带有源代码的GitHub存储库

还有两个视频,检测应该也能正常工作。

原始问题

我尝试使用OpenCV 4.x.x查找黑板的边缘(如下所示的图像),但不知何故我无法成功。我目前的代码如下(具有OpenCV和实时相机输入的Android平台),其中imgMat是来自相机输入的Mat:

    Mat gray = new Mat();
    Imgproc.cvtColor(imgMat, gray, Imgproc.COLOR_RGB2BGR);

    Mat blurred = new Mat();
    Imgproc.blur(gray, blurred, new org.opencv.core.Size(3, 3));

    Mat canny = new Mat();
    Imgproc.Canny(blurred, canny, 80, 230);

    Mat kernel = Imgproc.getStructuringElement(Imgproc.MORPH_RECT, new org.opencv.core.Size(2, 2));
    Mat dilated = new Mat();
    Imgproc.morphologyEx(canny, dilated, Imgproc.MORPH_DILATE, kernel, new Point(0, 0), 10);
    Mat rectImage = new Mat();
    Imgproc.morphologyEx(dilated, rectImage, Imgproc.MORPH_CLOSE, kernel, new Point(0, 0), 5);
    Mat endproduct = new Mat();
    Imgproc.Canny(rectImage, endproduct, 120, 230);

    List<MatOfPoint> contours = new ArrayList<>();
    Mat hierarchy = new Mat();
    Imgproc.findContours(endproduct, contours, hierarchy, Imgproc.RETR_LIST, Imgproc.CHAIN_APPROX_SIMPLE);

    double maxArea = 0;
    boolean hasContour = false;
    MatOfPoint2f biggestContour = new MatOfPoint2f();
    Iterator<MatOfPoint> each = contours.iterator();
    while (each.hasNext()) {
        MatOfPoint wrapper = each.next();
        double area = Imgproc.contourArea(wrapper);
        if (area > maxArea) {
            maxArea = area;
            biggestContour = new MatOfPoint2f(wrapper.toArray());
            hasContour = true;
        }
    }

    if (hasContour) {
        Mat output = imgMat.clone();

        MatOfPoint2f approx = new MatOfPoint2f();
        MatOfPoint poly = new MatOfPoint();

        Imgproc.approxPolyDP(biggestContour, approx, Imgproc.arcLength(biggestContour, true) * .02, true);
        approx.convertTo(poly, CvType.CV_32S);

        Rect rect = Imgproc.boundingRect(poly);

     }

虽然这段Python代码在我的电脑上使用视频时有效,但我现在无法使其正常工作。我从矩形中获取输出,并在我的移动屏幕上显示它,但是它会频繁闪烁并且无法正常工作。

这些是我尝试过Python程序的图像,并且它们可以正常工作:

大黑板

大黑板2

我做错了什么?我不能持续检测黑板的边缘。

关于黑板的其他信息:

  • 始终为矩形
  • 可能有不同的光线
  • 应忽略文本,仅应检测到主板
  • 外部黑板也应该被忽略
  • 仅应显示/返回主板的轮廓

感谢任何建议或代码!


1
我建议您将图像转换为HSV格式,并在绿色上进行阈值处理以获取黑板。然后从阈值图像中获取轮廓。 - fmw42
那是一个选项,但我想要检测不仅绿色板子,还有黑色和其他颜色:/ - Lars
1个回答

11

我使用HSV因为这是检测特定颜色最简单的方法。我使用了丰富度测试来自动选择颜色阈值(所以这适用于绿色或蓝色板子)。但是,这个测试会在白色或黑色板子上失败,因为在色调方面,白色和黑色被认为是所有颜色。相反,在HSV中,白色和黑色最容易被检测为非常低的饱和度(白色)或非常低的值(黑色)。

对于每个检测,我进行了三重检查,并选择拥有最多像素的掩模(我假设板子是图像的大多数)。我不确定这在其他图像上如何工作,因为我们这里只有一个,所以这可能适用于其他板子,也可能不适用。

我使用approxPolyDP来减少轮廓中的点数,直到我有4个点,并使用它来绘制形状。

enter image description here

enter image description here

import cv2
import numpy as np

# get unique colors (to speed up search) and return the most abundant mask
def getAbundantColor(channel, margin):
    # get uniques
    unique_colors, counts = np.unique(channel, return_counts=True);

    # check for the most abundant color
    most = None;
    biggest_count = -1;
    for col in unique_colors:
        # count number of white pixels
        mask = cv2.inRange(channel, int(col - margin), int(col + margin));
        count = np.count_nonzero(mask);

        # if bigger, set new "most"
        if count > biggest_count:
            biggest_count = count;
            most = mask;
    return most, biggest_count;

# load image
img = cv2.imread("blackboard.jpg");

# it's huge, scale down so that we can see the whole thing
h, w = img.shape[:2];
scale = 0.25;
h = int(scale*h);
w = int(scale*w);
img = cv2.resize(img, (w,h));

# hsv
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV);
h,s,v = cv2.split(hsv);

# median blur to get rid of most of the text
h = cv2.medianBlur(h, 5);
s = cv2.medianBlur(s, 5);
v = cv2.medianBlur(v, 5);

# get most abundant color
color_margin = 30;
hmask, hcount = getAbundantColor(h, color_margin);

# detect white and black separately
light_margin = 30;
# white
wmask = cv2.inRange(s, 0, light_margin);
wcount = np.count_nonzero(wmask);

# black
bmask = cv2.inRange(v, 0, light_margin);
bcount = np.count_nonzero(bmask);

# check which is biggest
sorter = [[hcount, hmask], [wcount, wmask], [bcount, bmask]];
sorter.sort();
mask = sorter[-1][1];

# dilate and erode to close holes
kernel = np.ones((3,3), np.uint8);
mask = cv2.dilate(mask, kernel, iterations = 2);
mask = cv2.erode(mask, kernel, iterations = 4);
mask = cv2.dilate(mask, kernel, iterations = 2);

# get contours # OpenCV 3.4, in OpenCV 2* or 4* it returns (contours, _)
_, contours, _ = cv2.findContours(mask, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE);

# for each contour, approximate a simpler shape until we have 4 points
simplified = [];
for con in contours:
    # go until we have 4 points
    num_points = 999999;
    step_size = 0.01;
    percent = step_size;
    while num_points >= 4:
        # get number of points
        epsilon = percent * cv2.arcLength(con, True);
        approx = cv2.approxPolyDP(con, epsilon, True);
        num_points = len(approx);

        # increment
        percent += step_size;

    # step back and get the points
    # there could be more than 4 points if our step size misses it
    percent -= step_size * 2;
    epsilon = percent * cv2.arcLength(con, True);
    approx = cv2.approxPolyDP(con, epsilon, True);
    simplified.append(approx);
cv2.drawContours(img, simplified, -1, (0,0,200), 2);

# print out the number of points
for points in simplified:
    print("Num Points: " + str(len(points)));

# show image
cv2.imshow("Image", img);
cv2.imshow("Hue", h);
cv2.imshow("Mask", mask);
cv2.waitKey(0);

编辑:为了适应板子颜色和外观的不确定性,我假设板子本身将占据图片的大部分。与分类器相关的线条正在寻找图像中最常见的颜色。如果白色墙壁占据了图像中更多的空间,那么就会选择该颜色用于掩膜。

还有其他方法可以尝试选择板子,但很难想出一个通用的解决方案。如果您能想出一种遮罩板子的方式,那么其余的代码应该可以完成相同的工作。如果你能放弃未知颜色的假设,并提供失败案例的原始图片,那么我可能会想出一个合适的掩膜。


这个简直太好用了!你认为在Java中也可能实现类似的功能吗,因为你使用了numpy?并且你能解释一下排序器具体是怎么做的吗? - Lars
我已经更新了我的问题,你能看一下吗? - Lars
排序器用于选择图像中最常见的颜色。它只是一个包含每种颜色计数和掩码的列表。 - Ian Chu
我添加了更多的图片,你能试着想出一个掩码吗? - Lars
@IanChu,这是一个非常好的解决方案,伙计。 - stateMachine
这个有更新吗?很遗憾,它不能在我所有的图片上工作 :/ - Lars

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接