检测照片中纸张角落的算法


97

检测照片中发票/收据/纸的角落的最佳方法是什么?这将用于OCR之前的后续透视校正。

我当前的方法是:

RGB>灰色>具有阈值的Canny边缘检测>扩张(1)>移除小对象(6)>清除边界对象>根据凸面区域选择大博客。> [角落检测-未实施]

我忍不住想,必须有一种更强大的“智能” /统计方法来处理这种类型的细分。我没有很多训练示例,但是我可能可以一起获得100张图像。

更广泛的上下文:

我正在使用matlab进行原型制作,并计划在OpenCV和Tesserect-OCR中实施该系统。这是我需要针对此特定应用程序解决的许多图像处理问题中的第一个。因此,我希望推出自己的解决方案,并重新熟悉图像处理算法。

这是一些我希望算法处理的示例图像:如果您想接受挑战,则可以在http://madteckhead.com/tmp上找到大图像。

情况1
(来源:madteckhead.com

情况2
(来源:madteckhead.com

情况3
(来源:madteckhead.com

情况4
(来源:madteckhead.com

最好的情况是:

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

案例1-发布佳能
(来源:madteckhead.com

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

但是,在其他情况下,它很容易失败:

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

案例2-发布佳能
(来源:madteckhead.com

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

预先感谢所有很棒的主意!我太爱了!

编辑:霍夫变换进展

问:什么样的算法会聚簇霍夫线来寻找拐角?遵循答案的建议,我能够使用霍夫变换,选取线条并对其进行过滤。我当前的方法相当粗糙。我已经假设发票将始终小于与图像对齐的15度。如果是这种情况,我最终得到的行结果合理(请参阅下文)。但是,不能完全确定是否有合适的算法来对直线(或投票)进行聚类以推断出拐角。霍夫线不连续。并且在嘈杂的图像中,可能存在平行线,因此需要某种形式或距线原点度量的距离。有任何想法吗?

情况1 情况2 情况3 情况4
(来源:madteckhead.com


1
是的,我让它在大约95%的情况下都能正常工作。由于时间紧迫,我不得不搁置代码。我会在某个阶段发布后续消息,如果您需要紧急帮助,请随时委托我。很抱歉缺乏良好的跟进服务。我很想重新使用此功能。
内森·凯勒

内森(Nathan),能否请您跟进您最终的做法?我停留在同一点,识别出纸的角/外轮廓。我遇到了与您完全相同的问题,因此我对解决方案非常感兴趣。
2014年

6
所有在这篇文章的图片现在404
ChrisF

Answers:


28

我是Martin的朋友,今年早些时候正在为此工作。这是我有史以来的第一个编码项目,有点匆忙地结束了,所以代码需要一些错误...解码...我会从您已经看到的内容中提供一些技巧,然后明天放假时整理代码。

首先尖,OpenCVpython是真棒,将他们尽快。:D

而不是除去小物体和/或噪音,而是降低三角约束,使它接受更多的边缘,然后找到最大的闭合轮廓(在OpenCV中使用findcontour()一些简单的参数,我想我使用过CV_RETR_LIST)。放在白纸上时可能仍然会遇到困难,但是绝对可以提供最佳结果。

对于Houghline2()Transform,请尝试使用与CV_HOUGH_STANDARD相对的CV_HOUGH_PROBABILISTIC,它将给出rhotheta值,以极坐标定义线,然后可以将线分组到一定范围内。

我的分组用作查找表,对于从hough变换输出的每一行,它将给出rho和theta对。如果这些值在表中一对值的5%之内,则将其丢弃;如果在5%对之内,则将新条目添加到表中。

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

希望这可以帮助。


丹尼尔您好,感谢您的参与。我喜欢你的方法。目前,该路线实际上与我取得了不错的效果。甚至还有OpenCV示例检测矩形。只是必须对结果进行一些过滤。正如您所说的,用这种方法很难检测出白色的白色。但这是一种简单且成本较低的方法。实际上,我没有考虑使用hough方法,而是做了一个多边形近似,看看opencv中的平方示例。我希望看到您实施霍夫投票。预先感谢内森
内森·凯勒

我在使用此方法时遇到问题,如果可以设计出更好的方法以供将来参考,我将发布解决方案
Anshuman Kumar

@AnshumanKumar我真的很需要这个问题的帮助,请你能帮我吗?stackoverflow.com/questions/61216402/...
卡洛斯·圣地亚哥

19

我大学的一个学生团体最近演示了一个iPhone应用程序(和python OpenCV应用程序),他们编写了该应用程序来实现此目的。我记得,这些步骤是这样的:

  • 中值过滤器可完全去除纸上的文字(这是白纸上的手写文字,光线很好,可能无法与打印文字配合使用,效果很好)。原因是它使转角检测更加容易。
  • 行的霍夫变换
  • 在霍夫变换累加器空间中找到峰值,并在整个图像上绘制每条线。
  • 分析线条,并删除彼此非常靠近且角度相似的任何线条(将线条成一团)。这是必需的,因为Hough变换在离散的样本空间中无法正常运行。
  • 查找大致平行且与其他线对相交的线对,以查看哪些线形成四边形。

这似乎工作得很好,他们能够拍摄一张纸或一本书的照片,执行角点检测,然后几乎实时地将图像中的文档映射到平面上(只有一个OpenCV函数可以执行映射)。当我看到它起作用时,没有OCR。


感谢马丁的好主意。Ive采纳了您的建议,并实施了霍夫变换方法。(请参见上面的结果)。我正在努力确定一种可靠的算法,该算法可以推断直线以找到相交点。没有多少行,也有一些误报。您对我如何最好地合并和丢弃行有任何建议?如果您的学生有兴趣,请鼓励他们联系。我很想听听他们在使算法在移动平台上运行的经验。(这是我的下一个目标)。非常感谢您的想法。
内森·凯勒

1
看起来线的HT在除了第二张图像之外的所有图像中都运行良好,但是您是否为累加器中的起始值和终止值定义了阈值公差?HT并没有真正定义开始和结束位置,而是定义了y = mx + c中的m和c值。参见此处 -请注意,这是在累加器中使用极坐标,而不是笛卡尔坐标。通过这种方式,您可以按c,然后按m对线进行分组,以使它们变稀,并通过将线想象为在整个图像上延伸来发现更有用的交点。
马丁·福特

@MartinFoot我真的很需要这个问题的帮助,能帮我吗?stackoverflow.com/questions/61216402/...
卡洛斯·圣地亚哥

16

经过一些试验,我得出了以下结论:

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版本,并且导入cv2对我来说工作完美。
Navneet Singh 2013年

您是否足够友善地更新此代码,使其可以正常工作? 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我真的很需要这个问题的帮助,请你能帮我吗?stackoverflow.com/questions/61216402/...
卡洛斯·圣地亚哥


4

您还可以对Sobel算子结果使用MSER(最大稳定的极值区域)来找到图像的稳定区域。对于MSER返回的每个区域,您可以应用凸包和多边形近似来获得如下所示的结果:

但是,这种检测对实时检测有用的不仅仅是单个图片,而并非总是返回最佳结果。

结果


1
您是否可以为此代码共享更多详细信息,请先感谢一堆
Monty 2016年

我在cv2.CHAIN_APPROX_SIMPLE中收到一个错误,说要解压的值太多。任何想法?我使用的是1024 * 1024图像作为示例
Praveen

1
谢谢大家,刚刚弄清楚了当前Opencv分支的语法变化Answers.opencv.org/question/40329/…–
Praveen

MSER不是要提取斑点吗?我尝试过,它只能检测到大部分文本
Anshuman Kumar,

3

边缘检测后,使用霍夫变换。然后,将这些点及其标签放在带有支持标记的SVM(支持向量机)中,如果示例上有平滑线条,则SVM将很难将示例的必要部分与其他部分分开。我对SVM的建议是输入连接性和长度之类的参数。也就是说,如果连接点很长,那么它们很可能是收据的一行。然后,您可以消除所有其他要点。


嗨,战神,谢谢你的想法!我已经实现了霍夫变换(见上文)。考虑到误报和不连续的线,我无法找到一种可靠的方法来找到拐角。您还有其他想法吗?自从我研究SVM技术以来已经有一段时间了。这是有监督的方法吗?我没有任何训练数据,但是我可以生成一些数据。我将对探索该方法感兴趣,因为它热衷于了解有关SVM的更多信息。您能推荐任何资源吗?亲切的问候。内森
内森·凯勒

3

这是使用C ++的@Vanuan的代码:

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);

行变量定义在哪里?必须是std :: vector <cv :: Vec4i>行;
CanÜrek2014年

@CanÜrek你是对的。std::vector<cv::Vec4i> lines;在我的项目的全局范围内声明。
GBF_Gabriel 2014年

1
  1. 转换为实验室空间

  2. 使用kmeans第2段集群

  3. 然后在其中一个群集(内部)上使用轮廓或轮廓
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.