从轮廓 OpenCV 检测卡片 MinArea Quadrilateral

bad*_*aly 4 opencv image-processing

另一个关于检测图片中的卡片。我已经成功地隔离了图片中的卡片,我有一个很近的凸包,从这里我被卡住了。

对于上下文/约束,目标:

  • 检测图片中的卡片
  • 纯色背景(见示例)
  • 固定在前面的卡片类型(意思是:我们有宽/高比)
  • 每张照片一个对象(至少现在是这样)

我使用的方法:

  1. 缩小规模
  2. 灰度
  3. 光模糊
  4. 精明
  5. 查找轮廓
  6. 删除列表中小于 120 点的所有轮廓(尝试/错误值)
  7. 案例 1:我有 1 个轮廓:我的卡片的完美轮廓:第 9 步
  8. 案例 2:我有多个轮廓
    • 凸壳
    • 近似多边形 ?
  9. ???

第1、3、6步主要是去除噪声和小伪影。

所以我几乎卡在第 9 步。我试过一个示例图片:

样本

在调试图片上:

  • 绿色:轮廓
  • 红色:凸包
  • 紫色/粉红色:使用 approxPolyDp
  • 黄色:minAreaRect

(结果图片是从minAreaRect中提取的)

所以轮廓是可以接受的,我可以通过调整 canny 或第一次模糊的参数来做得更好。但现在这是可以接受的,现在的问题是,我怎样才能得到将形成“minarea 四边形”的 4 个点。如您所见, minAreaRect 给出了一个不完美的矩形,并且 approxPolyDp 丢失了太多的卡片。

有什么线索可以解决这个问题吗?我尝试在使用 approxPolyDp(我使用arcLength*0.1)时使用 epsilon 值,但没有。

这种方法的另一个问题是在 canny 期间丢失了一个角(参见示例),它将不起作用(除非使用 minAreaRect 时)。但这可能可以在之前(通过更好的预处理)或之后解决(因为我们知道宽/高比)。

在此处输入图片说明

不是在这里要求代码,只是想知道如何解决这个问题,

谢谢!

编辑:Yves Daoust 的解决方案:

  • 从凸包中获取与谓词匹配的8个点:(最大化x, x+y, y, -x+y, -x, -xy, -y, xy)
  • 从这个八边形,取4条最长的边,得到交点

结果:

结果

编辑 2:使用 Hough 变换(而不是 8 个极值点)可以为找到 4 个边的所有情况提供更好的结果。如果找到超过 4 行,可能我们有重复,因此使用一些数学方法尝试过滤并保留 4 行。我使用行列式(如果平行则接近 0)和点线距离公式编写了草图工作

Elo*_*ine 7

这是我在您的输入图像上尝试的管道:

第 1 步:检测egdes

  • 使用Canny 过滤器模糊灰度输入并检测边缘

第 2 步:找到卡片的角

  • 计算轮廓
  • 长度对轮廓进行排序,只保留最大的一个
  • 生成这个轮廓的凸包
  • 用凸包创建一个遮罩
  • 使用HoughLinesP查找四面你的卡
  • 计算4条边的交点

第 3 步:单应性

  • 使用findHomography找到你的卡的仿射变换(用4个交叉点,在发现第2步
  • 使用计算出的单应矩阵扭曲输入图像

结果如下: 卡片检测流水线

请注意,您必须找到一种方法来对 4 个交点进行排序,以便始终按相同的顺序排列(否则findHomography将不起作用)。

我知道你没有要求代码,但我必须测试我的管道,所以这里是...... :)

Vec3f calcParams(Point2f p1, Point2f p2) // line's equation Params computation
{
    float a, b, c;
    if (p2.y - p1.y == 0)
    {
        a = 0.0f;
        b = -1.0f;
    }
    else if (p2.x - p1.x == 0)
    {
        a = -1.0f;
        b = 0.0f;
    }
    else
    {
        a = (p2.y - p1.y) / (p2.x - p1.x);
        b = -1.0f;
    }

    c = (-a * p1.x) - b * p1.y;
    return(Vec3f(a, b, c));
}

Point findIntersection(Vec3f params1, Vec3f params2)
{
    float x = -1, y = -1;
    float det = params1[0] * params2[1] - params2[0] * params1[1];
    if (det < 0.5f && det > -0.5f) // lines are approximately parallel
    {
        return(Point(-1, -1));
    }
    else
    {
        x = (params2[1] * -params1[2] - params1[1] * -params2[2]) / det;
        y = (params1[0] * -params2[2] - params2[0] * -params1[2]) / det;
    }
    return(Point(x, y));
}

vector<Point> getQuadrilateral(Mat & grayscale, Mat& output) // returns that 4 intersection points of the card
{
    Mat convexHull_mask(grayscale.rows, grayscale.cols, CV_8UC1);
    convexHull_mask = Scalar(0);

    vector<vector<Point>> contours;
    findContours(grayscale, contours, RETR_EXTERNAL, CHAIN_APPROX_NONE);

    vector<int> indices(contours.size());
    iota(indices.begin(), indices.end(), 0);

    sort(indices.begin(), indices.end(), [&contours](int lhs, int rhs) {
        return contours[lhs].size() > contours[rhs].size();
    });

    /// Find the convex hull object
    vector<vector<Point> >hull(1);
    convexHull(Mat(contours[indices[0]]), hull[0], false);

    vector<Vec4i> lines;
    drawContours(convexHull_mask, hull, 0, Scalar(255));
    imshow("convexHull_mask", convexHull_mask);
    HoughLinesP(convexHull_mask, lines, 1, CV_PI / 200, 50, 50, 10);
    cout << "lines size:" << lines.size() << endl;

    if (lines.size() == 4) // we found the 4 sides
    {
        vector<Vec3f> params(4);
        for (int l = 0; l < 4; l++)
        {
            params.push_back(calcParams(Point(lines[l][0], lines[l][1]), Point(lines[l][2], lines[l][3])));
        }

        vector<Point> corners;
        for (int i = 0; i < params.size(); i++)
        {
            for (int j = i; j < params.size(); j++) // j starts at i so we don't have duplicated points
            {
                Point intersec = findIntersection(params[i], params[j]);
                if ((intersec.x > 0) && (intersec.y > 0) && (intersec.x < grayscale.cols) && (intersec.y < grayscale.rows))
                {
                    cout << "corner: " << intersec << endl;
                    corners.push_back(intersec);
                }
            }
        }

        for (int i = 0; i < corners.size(); i++)
        {
            circle(output, corners[i], 3, Scalar(0, 0, 255));
        }

        if (corners.size() == 4) // we have the 4 final corners
        {
            return(corners);
        }
    }
    
    return(vector<Point>());
}

int main(int argc, char** argv)
{
    Mat input = imread("playingcard_input.png");
    Mat input_grey;
    cvtColor(input, input_grey, CV_BGR2GRAY);
    Mat threshold1;
    Mat edges;
    blur(input_grey, input_grey, Size(3, 3));
    Canny(input_grey, edges, 30, 100);

    vector<Point> card_corners = getQuadrilateral(edges, input);
    Mat warpedCard(400, 300, CV_8UC3);
    if (card_corners.size() == 4)
    {
        Mat homography = findHomography(card_corners, vector<Point>{Point(warpedCard.cols, 0), Point(warpedCard.cols, warpedCard.rows), Point(0,0) , Point(0, warpedCard.rows)});
        warpPerspective(input, warpedCard, homography, Size(warpedCard.cols, warpedCard.rows));
    }

    imshow("warped card", warpedCard);
    imshow("edges", edges);
    imshow("input", input);
    waitKey(0);

    return 0;
}
Run Code Online (Sandbox Code Playgroud)

编辑:我已经稍微调整了CannyHoughLinesP函数的参数以更好地检测卡(程序现在适用于两个输入样本)。

  • 只是另一个想法:扩张**掩模**创建一个带有小内核(1,1)的_Step 2_可能有助于合并线,从而增加找到合适的最大轮廓的机会。 (2认同)