【问题标题】:Detect card MinArea Quadrilateral from contour OpenCV从轮廓OpenCV中检测卡片MinArea Quadrilateral
【发布时间】:2023-04-10 23:17:01
【问题描述】:

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

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

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

我使用的方法:

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

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

所以我几乎被困在第 9 步。 我尝试了一张示例图片:

关于调试图片:

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

(结果图像是从minAreaRect中提取出来的)

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

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

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

这里不要求代码,只是想办法解决这个问题,

谢谢!

编辑:Yves Daoust 的解决方案:

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

结果:

编辑 2: 使用霍夫变换(而不是 8 个极值点)在找到 4 个边的所有情况下都能得到更好的结果。如果发现超过 4 行,可能我们有重复,所以使用一些数学来尝试过滤并保留 4 行。我使用行列式(如果平行,则接近 0)和点线距离公式编写了一个草案)

【问题讨论】:

    标签: opencv image-processing


    【解决方案1】:

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

    第一步:检测egdes

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

    第 2 步:找到卡片的角

    • 计算轮廓
    • 长度对轮廓进行排序,只保留最大的一个
    • 生成此轮廓的凸包
    • 从凸包中创建一个蒙版
    • 使用HoughLinesP 查找卡片的4 个面
    • 计算 4 条边的交点

    第 3 步:单应性

    • 使用findHomography 查找卡片的仿射变换(在步骤 2 找到 4 个交点)
    • 使用计算出的单应矩阵扭曲输入图像

    结果如下:

    请注意,您必须找到一种方法来对 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;
    }
    

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

    【讨论】:

    • 好的,类似的方法,但是你使用了 approxPolyDp 而我使用了@Yves Daoust 解决方案。我很好奇您的 approxPolyDp 似乎比我测试的那个干净得多。我将通过检查您的代码进行调查,谢谢!之后的仿射变换部分的巨大优势,也感谢您!
    • @badoualy 实际上,我没有在我的代码中使用approxPolyDP 的结果。第二张图实际上是最大轮廓的凸包。我会更新我的答案。
    • @badoualy 我已经对 Canny 和 HoughLinesP 函数的参数进行了一些调整,以便更好地检测卡片(程序现在适用于两个输入样本)。
    • 如果我这样做,我会错过其他示例中的 1 或 2 行。我只是做了一个小算法来删除重复项(使用线点距离和行列式来识别“重复项”)。
    • 只是另一个想法:扩张 mask 创建了一个带有小内核 (1,1) 的 Step 2 可能有助于合并行,因此增加找到合适的最大轮廓的机会。
    【解决方案2】:

    由于对象被隔离在统一的背景上,我建议从图像轮廓开始寻找边缘,朝向中心,并在第一个遇到的边缘点处停止。

    除非您在背景区域得到误报,否则凸包将为您提供相当好的对象轮廓近似值,尽管边缘点未命中。

    现在要得到包围四边形,你可以在八个基本方向上找到最远的点(最大化x、x+y、y、x-y、-x、-x-y、-y、-x+y)。这给了你一个八边形(可能有合并的顶点)。取四个最长的边并将它们相交以找到角。

    【讨论】:

    • 尝试一下,有什么简单的方法可以区分边(以及比四边形更大的对角线)?我好像想多了
    • @badoualy:区分双方?对角线不会出现在八角形中。
    • 哦,我是做点云的。但我确实需要对这些点进行“排序”,以便将它们列在列表中(顺时针或逆时针)对吧?
    • 如果你按照指示的顺序寻找点,它们将形成一个凸多边形。
    • 好的,完成了,编辑帖子结果,感谢您的帮助!
    猜你喜欢
    • 1970-01-01
    • 2014-07-27
    • 1970-01-01
    • 1970-01-01
    • 2016-10-24
    • 2014-12-12
    • 1970-01-01
    • 1970-01-01
    • 2016-05-28
    相关资源
    最近更新 更多