检测带有圆角的卡片边缘

13

你好,我目前正在开发一个OCR阅读应用程序,在使用AVFoundation框架成功捕获卡片图像后。

下一步,我需要找到卡片的边缘,以便从主捕获图像中裁剪卡片图像,并将其发送给OCR引擎进行处理。

现在的主要问题是找到卡片的边缘,我正在使用下面的代码(从另一个开源项目中获取),它使用OpenCV来实现这个目的。如果卡片是纯矩形卡或纸张,则它可以正常工作。但当我使用带有圆角(例如驾驶执照)的卡时,它无法检测。同时我对OpenCV没有太多专业知识,请问有谁能帮我解决这个问题吗?

- (void)detectEdges
{
    cv::Mat original = [MAOpenCV cvMatFromUIImage:_adjustedImage];
    CGSize targetSize = _sourceImageView.contentSize;
    cv::resize(original, original, cvSize(targetSize.width, targetSize.height));

    cv::vector<cv::vector<cv::Point>>squares;
    cv::vector<cv::Point> largest_square;

    find_squares(original, squares);
    find_largest_square(squares, largest_square);

    if (largest_square.size() == 4)
    {

        // Manually sorting points, needs major improvement. Sorry.

        NSMutableArray *points = [NSMutableArray array];
        NSMutableDictionary *sortedPoints = [NSMutableDictionary dictionary];

        for (int i = 0; i < 4; i++)
        {
            NSDictionary *dict = [NSDictionary dictionaryWithObjectsAndKeys:[NSValue valueWithCGPoint:CGPointMake(largest_square[i].x, largest_square[i].y)], @"point" , [NSNumber numberWithInt:(largest_square[i].x + largest_square[i].y)], @"value", nil];
            [points addObject:dict];
        }

        int min = [[points valueForKeyPath:@"@min.value"] intValue];
        int max = [[points valueForKeyPath:@"@max.value"] intValue];

        int minIndex;
        int maxIndex;

        int missingIndexOne;
        int missingIndexTwo;

        for (int i = 0; i < 4; i++)
        {
            NSDictionary *dict = [points objectAtIndex:i];

            if ([[dict objectForKey:@"value"] intValue] == min)
            {
                [sortedPoints setObject:[dict objectForKey:@"point"] forKey:@"0"];
                minIndex = i;
                continue;
            }

            if ([[dict objectForKey:@"value"] intValue] == max)
            {
                [sortedPoints setObject:[dict objectForKey:@"point"] forKey:@"2"];
                maxIndex = i;
                continue;
            }

            NSLog(@"MSSSING %i", i);

            missingIndexOne = i;
        }

        for (int i = 0; i < 4; i++)
        {
            if (missingIndexOne != i && minIndex != i && maxIndex != i)
            {
                missingIndexTwo = i;
            }
        }


        if (largest_square[missingIndexOne].x < largest_square[missingIndexTwo].x)
        {
            //2nd Point Found
            [sortedPoints setObject:[[points objectAtIndex:missingIndexOne] objectForKey:@"point"] forKey:@"3"];
            [sortedPoints setObject:[[points objectAtIndex:missingIndexTwo] objectForKey:@"point"] forKey:@"1"];
        }
        else
        {
            //4rd Point Found
            [sortedPoints setObject:[[points objectAtIndex:missingIndexOne] objectForKey:@"point"] forKey:@"1"];
            [sortedPoints setObject:[[points objectAtIndex:missingIndexTwo] objectForKey:@"point"] forKey:@"3"];
        }


        [_adjustRect topLeftCornerToCGPoint:[(NSValue *)[sortedPoints objectForKey:@"0"] CGPointValue]];
        [_adjustRect topRightCornerToCGPoint:[(NSValue *)[sortedPoints objectForKey:@"1"] CGPointValue]];
        [_adjustRect bottomRightCornerToCGPoint:[(NSValue *)[sortedPoints objectForKey:@"2"] CGPointValue]];
        [_adjustRect bottomLeftCornerToCGPoint:[(NSValue *)[sortedPoints objectForKey:@"3"] CGPointValue]];
    }

    original.release();


}

你能给我提供裁剪卡片的链接吗? - faiziii
@raaz,我有类似的需求,你能推荐一下你使用过的开源项目吗?这将是非常有帮助的。 - user3011741
1
@John 你会分享一两张样例图片吗? - karlphillip
根据John的说法,这里是一个示例 - karlphillip
嘿,伙计,如果你认为这个问题已经得到了成功回答,请点击它旁边的复选框 - karlphillip
4个回答

15

这个简单的实现基于在OpenCV示例目录中提供的squares.cpp中演示的一些技术。以下帖子也讨论了类似的应用:

@John,下面的代码已经使用您提供的示例图像和我创建的另一个图像进行了测试:

处理流程从findSquares()开始,这是OpenCV的squares.cpp演示中实现相同函数的简化版本。此函数将输入图像转换为灰度并应用模糊以提高边缘检测(Canny):

边缘检测不错,但需要形态学操作(膨胀)来连接附近的线:

之后,我们尝试查找轮廓(边缘)并组装成正方形。如果我们尝试在输入图像上绘制所有检测到的正方形,结果会是这样:

看起来很好,但不完全符合我们要查找的内容,因为检测到太多正方形。然而,最大的正方形实际上就是卡片,所以从这里开始就很简单了,我们只需找出哪个正方形最大。这正是findLargestSquare()所做的。

一旦我们知道最大的正方形,我们就可以为调试目的在正方形的角落处涂上红点:

正如你所看到的,检测不是完美的,但对于大多数用途来说似乎足够好了。这并不是一个强大的解决方案,我只想分享一种解决问题的方法。我相信还有其他处理这个问题的方式可能更有趣。祝您好运!

#include <iostream>
#include <cmath>
#include <vector>

#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <opencv2/imgproc/imgproc_c.h>

/* angle: finds a cosine of angle between vectors, from pt0->pt1 and from pt0->pt2
 */
double angle(cv::Point pt1, cv::Point pt2, cv::Point pt0)
{
    double dx1 = pt1.x - pt0.x;
    double dy1 = pt1.y - pt0.y;
    double dx2 = pt2.x - pt0.x;
    double dy2 = pt2.y - pt0.y;
    return (dx1*dx2 + dy1*dy2)/sqrt((dx1*dx1 + dy1*dy1)*(dx2*dx2 + dy2*dy2) + 1e-10);
}

/* findSquares: returns sequence of squares detected on the image
 */
void findSquares(const cv::Mat& src, std::vector<std::vector<cv::Point> >& squares)
{
    cv::Mat src_gray;
    cv::cvtColor(src, src_gray, cv::COLOR_BGR2GRAY);

    // Blur helps to decrease the amount of detected edges
    cv::Mat filtered;
    cv::blur(src_gray, filtered, cv::Size(3, 3));
    cv::imwrite("out_blur.jpg", filtered);

    // Detect edges
    cv::Mat edges;
    int thresh = 128;
    cv::Canny(filtered, edges, thresh, thresh*2, 3);
    cv::imwrite("out_edges.jpg", edges);

    // Dilate helps to connect nearby line segments
    cv::Mat dilated_edges;
    cv::dilate(edges, dilated_edges, cv::Mat(), cv::Point(-1, -1), 2, 1, 1); // default 3x3 kernel
    cv::imwrite("out_dilated.jpg", dilated_edges);

    // Find contours and store them in a list
    std::vector<std::vector<cv::Point> > contours;
    cv::findContours(dilated_edges, contours, cv::RETR_LIST, cv::CHAIN_APPROX_SIMPLE);

    // Test contours and assemble squares out of them
    std::vector<cv::Point> approx;
    for (size_t i = 0; i < contours.size(); i++)
    {
        // approximate contour with accuracy proportional to the contour perimeter
        cv::approxPolyDP(cv::Mat(contours[i]), approx, cv::arcLength(cv::Mat(contours[i]), true)*0.02, true);

        // Note: absolute value of an area is used because
        // area may be positive or negative - in accordance with the
        // contour orientation
        if (approx.size() == 4 && std::fabs(contourArea(cv::Mat(approx))) > 1000 &&
            cv::isContourConvex(cv::Mat(approx)))
        {
            double maxCosine = 0;
            for (int j = 2; j < 5; j++)
            {
                double cosine = std::fabs(angle(approx[j%4], approx[j-2], approx[j-1]));
                maxCosine = MAX(maxCosine, cosine);
            }

            if (maxCosine < 0.3)
                squares.push_back(approx);
        }
    }
}

/* findLargestSquare: find the largest square within a set of squares
 */
void findLargestSquare(const std::vector<std::vector<cv::Point> >& squares,
                       std::vector<cv::Point>& biggest_square)
{
    if (!squares.size())
    {
        std::cout << "findLargestSquare !!! No squares detect, nothing to do." << std::endl;
        return;
    }

    int max_width = 0;
    int max_height = 0;
    int max_square_idx = 0;
    for (size_t i = 0; i < squares.size(); i++)
    {
        // Convert a set of 4 unordered Points into a meaningful cv::Rect structure.
        cv::Rect rectangle = cv::boundingRect(cv::Mat(squares[i]));

        //std::cout << "find_largest_square: #" << i << " rectangle x:" << rectangle.x << " y:" << rectangle.y << " " << rectangle.width << "x" << rectangle.height << endl;

        // Store the index position of the biggest square found
        if ((rectangle.width >= max_width) && (rectangle.height >= max_height))
        {
            max_width = rectangle.width;
            max_height = rectangle.height;
            max_square_idx = i;
        }
    }

    biggest_square = squares[max_square_idx];
}

int main()
{
    cv::Mat src = cv::imread("cc.png");
    if (src.empty())
    {
        std::cout << "!!! Failed to open image" << std::endl;
        return -1;
    }

    std::vector<std::vector<cv::Point> > squares;
    findSquares(src, squares);

    // Draw all detected squares
    cv::Mat src_squares = src.clone();
    for (size_t i = 0; i < squares.size(); i++)
    {
        const cv::Point* p = &squares[i][0];
        int n = (int)squares[i].size();
        cv::polylines(src_squares, &p, &n, 1, true, cv::Scalar(0, 255, 0), 2, CV_AA);
    }
    cv::imwrite("out_squares.jpg", src_squares);
    cv::imshow("Squares", src_squares);

    std::vector<cv::Point> largest_square;
    findLargestSquare(squares, largest_square);

    // Draw circles at the corners
    for (size_t i = 0; i < largest_square.size(); i++ )
        cv::circle(src, largest_square[i], 4, cv::Scalar(0, 0, 255), cv::FILLED);
    cv::imwrite("out_corners.jpg", src);

    cv::imshow("Corners", src);
    cv::waitKey(0);

    return 0;
}

karlphillip - 感谢您的回复,使用此代码时,我们无法每次都检测到同一图像上卡片的保存边缘。它随机工作。 - Jignesh Fadadu
不是来自OpenCV方面的事情。我建议您使用我帖子中的示例图像进行测试。 - karlphillip
嘿,@karlphilip,我尝试了你的代码,但它没有检测到图像中的任何正方形。我尝试了许多组合。 - Mukesh
@muku 我相信你。这段代码仅仅是在一些特定情况下能够运行的概念验证代码,不适用于生产环境。人们仍需要改进它。 ;) - karlphillip
大家好,我能够找到正方形,但是当我记录cv::Point时,它没有与设备坐标一起绘制。我想要将正方形的角落制作成UIView。但是cv::Point不符合要求。cv::Point的值为(1850, 2450等),图像大小为(3264 * 2448)。请帮帮我。谢谢。 - Mukesh
显示剩余4条评论

3

不要使用“纯”矩形块,尝试使用近似矩形块。

1- 高斯模糊

2- 灰度和Canny边缘检测

3- 提取图像中的所有块(contours)并过滤掉小的块。您将使用findcontours和contourarea函数来实现此目的。

4- 使用moments,过滤非矩形块。首先需要检查类似矩形对象的矩。您可以自行完成或使用Google搜索。然后列出这些矩并找到对象之间的相似之处,创建您的筛选器。

例如:经过测试,假设您发现矩形对象的中心矩m30相似,则过滤具有不准确m30的对象。


2

我知道这篇文章可能发得有点晚了,但我发出来是希望能对其他人有所帮助。

iOS Core Image框架已经有一个很好的工具可以检测静态图片中的矩形(自iOS 5以来),人脸,QR码,甚至包含文本的区域。如果你查看CIDetector类,你会找到需要的内容。我也在用它做OCR应用程序,使用起来非常简单,并且比OpenCV更可靠(我不擅长OpenCV,但CIDetector只需3-5行代码就可以获得更好的效果)。


0
我不知道是否有这个选项,但您可以让用户定义它的边缘,而不是试图通过编程来实现。

3
这句话最好作为评论放在问题中。 - karlphillip

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