使用 OpenCV.js 检测角落里有正方形的框架

fur*_*y12 3 javascript opencv image-processing

我一直在尝试使用 Javascript 和 OpenCV.js 创建一个填写的表单扫描仪。我基本上想做的是拍摄一张写有填写好的表格的纸的照片,然后能够扫描照片并分析表格中的答案。第一步是实际找到图片中的形式并应用透视变换以获得论文的“自上而下的视图”。我所做的就是设法让脚本检测这张纸并应用转换以很好地扫描它。我通过应用灰度来做到这一点,然后应用 Canny 边缘检测,迭代找到的边缘,找到最大的有 4 个角的边缘,并假设这一定是我的论文。

在此输入图像描述

这种方法效果相对较好,但有时脚本会混淆纸张的实际内容 - 有时会检测到其他矩形并假定其是纸张,有时拍摄纸张的背景非常亮并且有边缘不清晰(对比度不够)。当脚本认为它已经找到了纸张时,这确实破坏了我的流程,但实际上是其他东西。我想改进这个纸张检测部分,这样我就可以始终确保检测到正确的东西。我想 - 让我们在表单周围添加一个自定义框架,这将更容易检测并在角落添加一些正方形(以仔细检查找到的框架是否 100% 是我正在寻找的框架)。

所以我创建了这样的东西:

在此输入图像描述

现在我希望能够检测框架的角落,并确保“填充”的方块位于角落中,以确保这 100% 是我正在寻找的框架。你能建议一下如何用 openCV 实现它吗?这是正确的方法吗?谢谢!

sta*_*ine 7

我以前曾处理过类似的问题。我使用 OpenCV 的 C++ 实现,但我有一些提示给您。

\n

分割纸张

\n

为了实现更好的分割,请考虑尝试图像量化。该技术将图像分割为N 个簇,即将颜色相似的像素分为一组。然后,该组由一种颜色表示。

\n

与其他技术(例如纯二值阈值)相比,该技术的优点在于它可以识别将被分组为N 个簇的多个颜色分布 \xe2\x80\x93 。看看(抱歉链接,我还不允许发布直接图像):

\n\n

这将帮助您更好地分割论文。该实现使用称为\xe2\x80\x9cK-means\xe2\x80\x9d 的聚类算法(稍后会详细介绍)。在我的示例中,我尝试了 3 个集群和 5 个算法 \xe2\x80\x9cruns\xe2\x80\x9d (或尝试,因为 K 均值通常运行多次)。

\n
cv::Mat imageQuantization( cv::Mat inputImage, int numberOfClusters = 3, int iterations = 5 ){\n\n        //step 1 : map the src to the samples\n        cv::Mat samples(inputImage.total(), 3, CV_32F);\n        auto samples_ptr = samples.ptr<float>(0);\n        for( int row = 0; row != inputImage.rows; ++row){\n            auto src_begin = inputImage.ptr<uchar>(row);\n            auto src_end = src_begin + inputImage.cols * inputImage.channels();\n            //auto samples_ptr = samples.ptr<float>(row * src.cols);\n            while(src_begin != src_end){\n                samples_ptr[0] = src_begin[0];\n                samples_ptr[1] = src_begin[1];\n                samples_ptr[2] = src_begin[2];\n                samples_ptr += 3; src_begin +=3;\n            }\n        }\n\n        //step 2 : apply kmeans to find labels and centers\n        int clusterCount = numberOfClusters; //Number of clusters to split the set by\n        cv::Mat labels;\n        int attempts = iterations; //Number of times the algorithm is executed using different initial labels\n        cv::Mat centers;\n        int flags = cv::KMEANS_PP_CENTERS;\n        cv::TermCriteria criteria = cv::TermCriteria( CV_TERMCRIT_ITER | CV_TERMCRIT_EPS,\n                                                      10, 0.01 );\n\n        //the call to kmeans:\n        cv::kmeans( samples, clusterCount, labels, criteria, attempts, flags, centers );\n\n        //step 3 : map the centers to the output\n        cv::Mat clusteredImage( inputImage.size(), inputImage.type() );\n        for( int row = 0; row != inputImage.rows; ++row ){\n            auto clusteredImageBegin = clusteredImage.ptr<uchar>(row);\n            auto clusteredImageEnd = clusteredImageBegin + clusteredImage.cols * 3;\n            auto labels_ptr = labels.ptr<int>(row * inputImage.cols);\n\n            while( clusteredImageBegin != clusteredImageEnd ){\n                int const cluster_idx = *labels_ptr;\n                auto centers_ptr = centers.ptr<float>(cluster_idx);\n                clusteredImageBegin[0] = centers_ptr[0];\n                clusteredImageBegin[1] = centers_ptr[1];\n                clusteredImageBegin[2] = centers_ptr[2];\n                clusteredImageBegin += 3; ++labels_ptr;\n            }\n        }   \n\n        //return the output:\n        return clusteredImage;\n}\n
Run Code Online (Sandbox Code Playgroud)\n

请注意,该算法还生成两个附加矩阵。“标签”是用整数标记的实际像素,该整数标识其簇。“中心”是每个簇的平均值

\n

检测边缘

\n

现在,在此分割图像上运行边缘检测器很简单。让\xe2\x80\x99s 尝试一下Canny。当然,您可以调整参数。在这里,我尝试了下阈值0f 30,上阈值90。相当标准,只需确保上阈值遵循 = 3 * LowerThreshold 的条件,按照 Canny 的建议。这是结果:

\n\n
    cv::Mat testEdges;\n    float lowerThreshold = 30;\n    float upperThreshold = 3 * lowerThreshold;\n    cv::Canny( testSegmented, testEdges, lowerThreshold, upperThreshold );\n
Run Code Online (Sandbox Code Playgroud)\n

检测线条

\n

好的。想要检测边缘检测器产生的线条吗?在这里,至少有两个选择。第一个也是最直接的:使用Hough\xe2\x80\x99s Line Detector。然而,正如您肯定已经看到的那样,调整霍夫以找到您真正正在寻找的线条可能很困难。

\n

过滤 Hough 返回的行的一种可能的解决方案是运行\xe2\x80\x9cangle filter\xe2\x80\x9d,因为我们只寻找(接近)垂直和水平线。您还可以按长度过滤行。

\n

这个代码片段给出了这个想法,你需要实际实现过滤器:\n//运行Hough的线检测器:\ncv::HoughLinesP(grad,linesP,1,CV_PI/180,minVotes,minLineLength,maxLineGap) ;

\n
    // Process the points (lines)\n    for( size_t i = 0; i < linesP.size(); i++ ) //points are stored in linesP\n    {\n        //get the line\n        cv::Vec4i l = linesP[i]; //get the line\n\n        //get the points:\n        cv::Point startPoint = cv::Point( l[0], l[1] );\n        cv::Point endPoint = cv::Point( l[2], l[3] );\n\n        //filter horizontal & vertical:\n        float dx = abs(startPoint.x - endPoint.x);\n        float dy = abs(startPoint.y - endPoint.y);\n\n        //angle filtering, delta y and delta x\n        if ( (dy < maxDy) || (dx < maxDx) ){\n          //got my target lines!\n        }\n    }\n
Run Code Online (Sandbox Code Playgroud)\n

在上面的代码中,我实际上正在使用线组件,而不是角度。因此,我的“角度”限制由 2 个最小分量长度定义:maxDy - y 轴上的最大“增量”长度,以及x 轴上的maxDx 。

\n

线条检测的另一个解决方案是利用这样的事实:您只查看具有拐角或之间大约 90 度的线条。您可以运行形态过滤器通过命中或未命中操作来检测这些 \xe2\x80\x9cpatterns\xe2\x80\x9d :)

\n

不管怎样,回到霍夫,这是我在没有太多参数调整并且应用角度/长度线滤波器之后得到的检测:

\n\n

凉爽的。绿点代表线条的起点和终点。正如你所看到的,有\xe2\x80\x99s 一堆。我们如何 \xe2\x80\x9ccombine\xe2\x80\x9d 它们?如果我们计算这些点的平均值怎么办?好的,但是我们应该得到 PER \xe2\x80\x9cquadrant\xe2\x80\x9d 行的平均值。如下图所示,I\xe2\x80\x99 将输入图像分为 4 个象限(黄线):

\n\n

希望每个象限都包含描述纸张角的点。对于每个象限,检查哪些点落在给定象限上并计算它们的平均值。这\xe2\x80\x99就是总体思路。

\n

\xe2\x80\x99 需要编写相当多的代码。幸运的是,如果我们稍微研究一下这个问题,我们可以看到所有绿点都倾向于聚集在一些非常明确的区域(或者,正如我们之前所说的,\xe2\x80\x9cquadrants\xe2\x80\x9d)。再次输入 K-means。

\n

无论如何,K 均值都会对具有相似值的数据进行分组。它可以是像素,可以是空间点,可以是任何东西,只要给它数据集和你想要的簇数,它就会吐出找到的簇和所述簇的平均值 \xe2\x80\x93好的!

\n

如果我使用 Hough 返回的线点运行 K 均值,我会得到最后一个图像中显示的结果。我还丢弃了与平均值相差太远的点。点的平均值通过“中心”矩阵返回,这里它们以橙色呈现 - \xe2\x80\x99s 非常接近!

\n

希望其中一些对您有所帮助!:)

\n