Обнаружение краев карты с закругленными углами

Привет, в настоящее время я работаю над приложением чтения OCR, где я успешно смогу захватить изображение карты с помощью AVFoundation framework.

Для следующего шага мне нужно выяснить края карты, чтобы я мог обрезать изображение карты из основного захваченного изображения, а затем я могу отправить его в 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();


}

Ответ 1

Эта наивная реализация основана на некоторых методах, продемонстрированных в squares.cpp, доступных в OpenCV образец каталога. Следующие сообщения также обсуждают похожие приложения:

@John, приведенный ниже код был протестирован с образцом, который вы предоставили, и другим, который я создал:

4cZi9.pngtb0Ki.png

Конвейер обработки начинается с findSquares(), упрощения той же функции, реализованной с помощью демонстрации OpenCV squares.cpp. Эта функция преобразует входное изображение в оттенки серого и применяет размытие, чтобы улучшить обнаружение краев (Canny):

tE5BU.pngn0MUx.png

Обнаружение кромок хорошее, но для соединения близлежащих линий требуется морфологическая операция (расширение):

lg1JV.pngmD0d5.png

После этого мы пытаемся найти контуры (ребра) и собрать квадраты из них. Если бы мы попытались нарисовать все обнаруженные квадраты на входных изображениях, это было бы результатом:

Qh91y.pngXNb43.png

Это выглядит хорошо, но это не совсем то, что мы ищем, поскольку слишком много обнаруженных квадратов. Тем не менее, самая большая площадь на самом деле является карточкой, поэтому отсюда она довольно проста, и мы просто выясняем, какая из квадратов самая большая. Именно это делает findLargestSquare().

Как только мы узнаем самый большой квадрат, мы просто рисуем красные точки в углах квадрата для целей отладки:

l04Oc.pngt8V7U.png

Как вы можете видеть, обнаружение не идеальное, но оно кажется достаточно хорошим для большинства целей. Это не надежное решение, и я хотел бы поделиться одним из подходов к решению проблемы. Я уверен, что есть другие способы справиться с этим, что может быть более интересно для вас. Удачи!

#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;
}

Ответ 2

вместо "чистых" прямоугольных капель, попробуйте пойти почти на прямоугольные.

1- гауссовское размытие

2 - определение оттенков серого и canny edge

3 извлеките все капли (контуры) в свое изображение и отфильтруйте маленькие. для этой цели вы будете использовать функции findcontours и contourarea.

4- используя moments, отфильтруйте непрямоугольные. Сначала вам нужно проверить моменты прямоугольных объектов. Вы можете сделать это самостоятельно или google. Затем перечислите эти моменты и найдите сходство между объектами, создайте свой фильтр как таковой.

Ex: после теста скажите, что вы обнаружили, что центральный момент m30 аналогичен для прямоугольных объектов → фильтровать объекты с неточным m30.

Ответ 3

Я знаю, может быть, слишком поздно для этого сообщения, но я отправляю это на случай, если он может помочь кому-то другому.

Рамка iOS Core Image уже имеет хороший инструмент для обнаружения таких функций, как прямоугольники (с iOS 5), лица, QR-коды и даже регионы, содержащие текст в неподвижном изображении. Если вы посмотрите класс CIDetector, вы найдете то, что вам нужно. Я использую его для приложения OCR тоже, он очень прост и очень надежный по сравнению с тем, что вы можете делать с OpenCV (я не очень хорошо знаком с OpenCV, но CIDetector дает гораздо лучшие результаты с 3-5 строками кода).

Ответ 4

Я не знаю, является ли это вариантом, но вы могли бы определить его края, а не пытаться сделать это программно.