在照片中检测纸张边角的算法

113

如何最好地在照片中检测发票/收据/纸张的角落?这将用于后续透视校正,OCR之前。

我当前的方法是:

RGB > Gray > Canny边缘检测,带有阈值处理 > 膨胀(1) > 删除小对象(6) > 清除边框对象 > 基于凸包面积选择最大的对象。> [未实现角检测]

我不禁想到,一定有更加健壮的“智能”/统计方法来处理这种类型的分割。我没有很多训练样例,但我可以凑齐100张图像。

更广泛的背景:

我正在使用Matlab进行原型设计,并计划在OpenCV和Tesseract-OCR上实现该系统。这是我需要解决的一系列图像处理问题中的第一个。所以我想自己开发解决方案,并重新熟悉图像处理算法。

以下是我希望算法处理的一些示例图像: 如果您愿意接受挑战,则可在http://madteckhead.com/tmp中查看大型图像。

case 1
(来源:madteckhead.com)

案例2
(来源: madteckhead.com)

案例3
(来源: madteckhead.com)

案例4
(来源: madteckhead.com)

在最佳情况下,这将给出:

案例1 - Canny
(来源: madteckhead.com)

案例1-发布Canny
(来源:madteckhead.com)

案例1-最大博客
(来源:madteckhead.com)

然而,它在其他情况下很容易失败:

案例2-Canny
(来源:madteckhead.com)

案例2-发布Canny
(来源:madteckhead.com)

案例2-最大博客
(来源: madteckhead.com)

编辑:霍夫变换进展

问:哪种算法可以将霍夫线聚类以找到角点? 根据答案的建议,我能够使用霍夫变换、选择线条并过滤它们。我的当前方法相当粗糙。我假设发票始终与图像少于15度不对齐。如果是这种情况,我得到了合理的线条结果(见下文)。但我不确定适合聚类线条(或投票)以推断角落的合适算法。霍夫线不连续。在嘈杂的图像中,可能会有平行线,因此需要某种形式或距离来自线起点的指标。有什么想法吗?

案例1 案例2 案例3 案例4
(来源: madteckhead.com)


1
是的,我成功地解决了大约95%的情况。由于时间不足,我现在不得不搁置这段代码。我会在某个阶段发布后续信息,如果您需要紧急帮助,请随时委托我。对于缺乏良好跟进的问题,我感到抱歉。我很想回到这个功能上继续工作。 - Nathan Keller
Nathan,请问你能否跟进一下你最终是如何解决的吗?我也卡在了同样的地方,无法识别纸张的角落/外轮廓。我遇到了和你一模一样的问题,所以我非常希望能得到解决方案。 - tim
7
本文中的所有图像现在都已404。 - ChrisF
8个回答

31

我是马丁的朋友,之前在这个项目上工作过。这是我第一次编码项目,由于时间紧迫,代码需要进行一些译码...明天我休息时会整理一下。

第一个提示,OpenCVpython 很棒,请尽快转向它们。:D

不要去除小物体或噪音,而是降低 Canny 约束条件,使其接受更多边缘,然后找到最大的闭合轮廓(在 OpenCV 中使用 findcontour() 与一些简单的参数,我想我用了 CV_RETR_LIST)。尽管在白纸上可能仍然有困难,但肯定提供了最佳结果。

对于 Houghline2() 变换,尝试使用 CV_HOUGH_STANDARD 而不是 CV_HOUGH_PROBABILISTIC,它将给出 rho 和 theta 值,定义了直线在极坐标中的位置,然后你可以在某个容限内分组这些线。

我的分组工作就像是一个查找表,对于从 Hough 变换输出的每条线,它都会给出 rho 和 theta 对。如果这些值在表中某一对值的 5% 范围内,它们就会被丢弃,如果它们超出了这 5%,则会向表中添加一条新的记录。

然后您可以更轻松地进行并行线或线之间距离的分析。

希望这些能有所帮助。


嗨,丹尼尔,感谢你的参与。我喜欢你的方法。这实际上是我目前取得良好结果的路线。甚至有一个OpenCV示例可以检测矩形。只需要对结果进行一些过滤。正如你所说,白色在白色背景下很难检测到。但这是一种简单且成本较低的方法,比霍夫更好。我实际上已经将霍夫方法从我的算法中删除,并进行了多边形逼近,请查看OpenCV中的正方形示例。我想看看你的霍夫投票实现。提前感谢,内森。 - Nathan Keller
我在使用这种方法时遇到了问题,如果我能想出更好的解决方案以供将来参考,我会发布一个解决方案。 - Anshuman Kumar
@AnshumanKumar,我真的需要关于这个问题的帮助,你能帮我吗?https://dev59.com/q1IH5IYBdhLWcg3wGZC5 - Carlos Diego

22

经过一些实验,这是我想出来的:

import cv, cv2, numpy as np
import sys

def get_new(old):
    new = np.ones(old.shape, np.uint8)
    cv2.bitwise_not(new,new)
    return new

if __name__ == '__main__':
    orig = cv2.imread(sys.argv[1])

    # these constants are carefully picked
    MORPH = 9
    CANNY = 84
    HOUGH = 25

    img = cv2.cvtColor(orig, cv2.COLOR_BGR2GRAY)
    cv2.GaussianBlur(img, (3,3), 0, img)


    # this is to recognize white on white
    kernel = cv2.getStructuringElement(cv2.MORPH_RECT,(MORPH,MORPH))
    dilated = cv2.dilate(img, kernel)

    edges = cv2.Canny(dilated, 0, CANNY, apertureSize=3)

    lines = cv2.HoughLinesP(edges, 1,  3.14/180, HOUGH)
    for line in lines[0]:
         cv2.line(edges, (line[0], line[1]), (line[2], line[3]),
                         (255,0,0), 2, 8)

    # finding contours
    contours, _ = cv2.findContours(edges.copy(), cv.CV_RETR_EXTERNAL,
                                   cv.CV_CHAIN_APPROX_TC89_KCOS)
    contours = filter(lambda cont: cv2.arcLength(cont, False) > 100, contours)
    contours = filter(lambda cont: cv2.contourArea(cont) > 10000, contours)

    # simplify contours down to polygons
    rects = []
    for cont in contours:
        rect = cv2.approxPolyDP(cont, 40, True).copy().reshape(-1, 2)
        rects.append(rect)

    # that's basically it
    cv2.drawContours(orig, rects,-1,(0,255,0),1)

    # show only contours
    new = get_new(img)
    cv2.drawContours(new, rects,-1,(0,255,0),1)
    cv2.GaussianBlur(new, (9,9), 0, new)
    new = cv2.Canny(new, 0, CANNY, apertureSize=3)

    cv2.namedWindow('result', cv2.WINDOW_NORMAL)
    cv2.imshow('result', orig)
    cv2.waitKey(0)
    cv2.imshow('result', dilated)
    cv2.waitKey(0)
    cv2.imshow('result', edges)
    cv2.waitKey(0)
    cv2.imshow('result', new)
    cv2.waitKey(0)

    cv2.destroyAllWindows()

不完美,但至少适用于所有样本:

1 2 3 4


4
我正在进行类似的项目。我运行了代码,但出现了“没有名为cv的模块”的错误。我安装了Open CV 2.4版本,而且import cv2对我来说完美地运行着。 - Navneet Singh
你能不能更新一下这段代码让它工作起来?https://pastebin.com/PMH5Y0M8 它现在只会给我一个黑屏。 - the7erm
你有没有想过如何将以下代码转换为Java:for line in lines[0]: cv2.line(edges, (line[0], line[1]), (line[2], line[3]), (255,0,0), 2, 8) # finding contours contours, _ = cv2.findContours(edges.copy(), cv.CV_RETR_EXTERNAL, cv.CV_CHAIN_APPROX_TC89_KCOS) contours = filter(lambda cont: cv2.arcLength(cont, False) > 100, contours) contours = filter(lambda cont: cv2.contourArea(cont) > 10000, contours) - aurelianr
Vanuan,我真的需要帮助解决这个问题,你能帮我吗?https://dev59.com/q1IH5IYBdhLWcg3wGZC5 - Carlos Diego

20
我校的一个学生团队最近展示了他们编写的iPhone应用程序(和Python OpenCV应用程序),以执行以下操作:
  • 中值滤波器完全消除纸上的文本(这是白纸手写文本,照明条件良好,可能无法用于印刷文本,但效果很好)。 这是因为它使角点检测更加容易。
  • 使用Hough变换进行直线检测
  • 在Hough变换累加器空间中找到峰值,并在整个图像上绘制每条直线。
  • 分析线条并删除任何非常接近且角度相似的线条(将线条聚类为一起)。 这是必要的,因为Hough变换在离散样本空间中运作时并不完美。
  • 找到大致平行且与其他对相交的线对形成四边形的线对。
这似乎运行得相当不错,他们能够拍摄一张纸或书的照片,执行角点检测,然后将图像中的文档映射到几乎实时的平面上(有一个OpenCV函数来执行映射)。 我看到它工作时没有OCR。

感谢你的好建议,马丁。我已经采用了霍夫变换方法(请参见上面的结果)。我正在努力确定一个强大的算法来推断线条以找到交点。由于线条不多,有一些误报。你有什么建议可以帮我更好地合并和丢弃线条吗?如果你的学生们有兴趣,请鼓励他们与我联系。我很想听听他们在移动平台上运行算法的经验。(那是我的下一个目标)。非常感谢你的建议。 - Nathan Keller
1
看起来直线的霍夫变换在除了你的第二张图像之外都很好,但是你是否为累加器中的起始和结束值定义了阈值容差?霍夫变换实际上并没有定义起始和结束位置,而是在y=mx+c中的m和c值。请参见这里 - 请注意,这是在累加器中使用极坐标而不是笛卡尔坐标。通过这种方式,您可以按c和m分组线条以使其稀疏,并将线条想象为延伸到整个图像,从而找到更有用的交点。 - Martin Foot
@MartinFoot,我真的需要帮助解决这个问题,你能帮我吗?https://dev59.com/q1IH5IYBdhLWcg3wGZC5 - Carlos Diego

11

你可以使用角点检测,而不是从边缘检测开始。

Marvin框架为此提供了Moravec算法的实现。你可以将纸张的角作为起始点。以下是Moravec算法的输出:

enter image description here


5

您还可以使用 MSER(极值稳定区域)与Sobel算子结果相结合,找到图像的稳定区域。对于MSER返回的每个区域,您可以应用凸包和多项式逼近来获得以下类似结果:

但是这种检测方法更适用于实时检测,而不是单张图片,因为它并不总是能够返回最佳结果。

result


1
你能分享更多细节吗?也许一些代码,非常感谢。 - Monty
我在使用cv2.CHAIN_APPROX_SIMPLE时收到了一个错误,提示有太多的值需要解包。你有什么想法吗?我正在使用1024*1024像素的图像作为我的样本。 - Praveen
1
谢谢大家,我刚刚弄清楚了当前Opencv分支中语法的更改。http://answers.opencv.org/question/40329/python-valueerror-too-many-values-to-unpack/ - Praveen
MSER 不是用来提取 blob 的吗?我尝试了一下,它只检测到了大部分文本。 - Anshuman Kumar

3

进行边缘检测后,使用霍夫变换。然后,将这些点和它们的标签放入支持向量机(SVM)中,如果示例上有平滑的线条,SVM将没有任何困难来划分示例的必要部分和其他部分。我的建议是在SVM中设置像连通性和长度这样的参数。也就是说,如果点是连接的且很长,它们很可能是收据的一条线。然后,您可以消除所有其他点。


嗨Ares,谢谢你的想法!我已经实现了霍夫变换(见上文)。我无法找到一个稳健的方法来找到角落,因为有一些误报和不连续的线。你还有什么其他的想法吗?我已经很久没有看SVM技术了。这是一种监督式方法吗?我没有任何训练数据,但我可以生成一些。我对探索这种方法很感兴趣,因为我想更多地了解SVM。你能推荐一些资源吗?顺祝商祺。Nathan - Nathan Keller

3
这里是@Vanuan使用C++的代码:
cv::cvtColor(mat, mat, CV_BGR2GRAY);
cv::GaussianBlur(mat, mat, cv::Size(3,3), 0);
cv::Mat kernel = cv::getStructuringElement(cv::MORPH_RECT, cv::Point(9,9));
cv::Mat dilated;
cv::dilate(mat, dilated, kernel);

cv::Mat edges;
cv::Canny(dilated, edges, 84, 3);

std::vector<cv::Vec4i> lines;
lines.clear();
cv::HoughLinesP(edges, lines, 1, CV_PI/180, 25);
std::vector<cv::Vec4i>::iterator it = lines.begin();
for(; it!=lines.end(); ++it) {
    cv::Vec4i l = *it;
    cv::line(edges, cv::Point(l[0], l[1]), cv::Point(l[2], l[3]), cv::Scalar(255,0,0), 2, 8);
}
std::vector< std::vector<cv::Point> > contours;
cv::findContours(edges, contours, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_TC89_KCOS);
std::vector< std::vector<cv::Point> > contoursCleaned;
for (int i=0; i < contours.size(); i++) {
    if (cv::arcLength(contours[i], false) > 100)
        contoursCleaned.push_back(contours[i]);
}
std::vector<std::vector<cv::Point> > contoursArea;

for (int i=0; i < contoursCleaned.size(); i++) {
    if (cv::contourArea(contoursCleaned[i]) > 10000){
        contoursArea.push_back(contoursCleaned[i]);
    }
}
std::vector<std::vector<cv::Point> > contoursDraw (contoursCleaned.size());
for (int i=0; i < contoursArea.size(); i++){
    cv::approxPolyDP(Mat(contoursArea[i]), contoursDraw[i], 40, true);
}
Mat drawing = Mat::zeros( mat.size(), CV_8UC3 );
cv::drawContours(drawing, contoursDraw, -1, cv::Scalar(0,255,0),1);

“lines”变量的定义在哪里?必须是std::vectorcv::Vec4i lines;。 - Can Ürek
@CanÜrek 你是对的。std::vector<cv::Vec4i> lines; 在我的项目中是在全局范围内声明的。 - GBF_Gabriel
嘿@GBF_Gabriel,你能告诉我如何找到所需图像的四个角落或边缘而不是画线吗? - Akash Chaudhary
1
我遇到了这个错误 E/cv::error(): OpenCV(4.3.0) Error: Assertion failed (reader.ptr != NULL) in cvDrawContours。 - Akash Chaudhary

1
  1. 转换为Lab空间

  2. 使用kmeans分割成2个聚类

  3. 然后在其中一个聚类上使用轮廓或霍夫变换(内部)

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