OpenCV C ++ / Obj-C: обнаружение листа бумаги / определение квадратов


178

Я успешно реализовал пример определения квадрата OpenCV в своем тестовом приложении, но теперь мне нужно отфильтровать вывод, потому что он довольно грязный - или мой код неверен?

Я заинтересован в четырех угловых пунктах статьи для уменьшения перекоса (как этот ) и дальнейшей обработки ...

Ввод, вывод: Ввод, вывод

Исходное изображение:

щелчок

Код:

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

- (std::vector<std::vector<cv::Point> >)findSquaresInImage:(cv::Mat)_image
{
    std::vector<std::vector<cv::Point> > squares;
    cv::Mat pyr, timg, gray0(_image.size(), CV_8U), gray;
    int thresh = 50, N = 11;
    cv::pyrDown(_image, pyr, cv::Size(_image.cols/2, _image.rows/2));
    cv::pyrUp(pyr, timg, _image.size());
    std::vector<std::vector<cv::Point> > contours;
    for( int c = 0; c < 3; c++ ) {
        int ch[] = {c, 0};
        mixChannels(&timg, 1, &gray0, 1, ch, 1);
        for( int l = 0; l < N; l++ ) {
            if( l == 0 ) {
                cv::Canny(gray0, gray, 0, thresh, 5);
                cv::dilate(gray, gray, cv::Mat(), cv::Point(-1,-1));
            }
            else {
                gray = gray0 >= (l+1)*255/N;
            }
            cv::findContours(gray, contours, CV_RETR_LIST, CV_CHAIN_APPROX_SIMPLE);
            std::vector<cv::Point> approx;
            for( size_t i = 0; i < contours.size(); i++ )
            {
                cv::approxPolyDP(cv::Mat(contours[i]), approx, arcLength(cv::Mat(contours[i]), true)*0.02, true);
                if( approx.size() == 4 && fabs(contourArea(cv::Mat(approx))) > 1000 && cv::isContourConvex(cv::Mat(approx))) {
                    double maxCosine = 0;

                    for( int j = 2; j < 5; j++ )
                    {
                        double cosine = fabs(angle(approx[j%4], approx[j-2], approx[j-1]));
                        maxCosine = MAX(maxCosine, cosine);
                    }

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

РЕДАКТИРОВАТЬ 17/08/2012:

Чтобы нарисовать обнаруженные квадраты на изображении, используйте этот код:

cv::Mat debugSquares( std::vector<std::vector<cv::Point> > squares, cv::Mat image )
{
    for ( int i = 0; i< squares.size(); i++ ) {
        // draw contour
        cv::drawContours(image, squares, i, cv::Scalar(255,0,0), 1, 8, std::vector<cv::Vec4i>(), 0, cv::Point());

        // draw bounding rect
        cv::Rect rect = boundingRect(cv::Mat(squares[i]));
        cv::rectangle(image, rect.tl(), rect.br(), cv::Scalar(0,255,0), 2, 8, 0);

        // draw rotated rect
        cv::RotatedRect minRect = minAreaRect(cv::Mat(squares[i]));
        cv::Point2f rect_points[4];
        minRect.points( rect_points );
        for ( int j = 0; j < 4; j++ ) {
            cv::line( image, rect_points[j], rect_points[(j+1)%4], cv::Scalar(0,0,255), 1, 8 ); // blue
        }
    }

    return image;
}


1
Я думаю, что вы можете изменить название вопроса, например, « Обнаружение листа бумаги» , если вы считаете, что это более уместно.
karlphillip

1
@moosgummi Мне нужна та же функциональность, что и у вас, например, «Определить углы захваченного изображения / документа». Как вы этого добились? Смогу ли я использовать OpenCV в своем приложении для iPhone? Пожалуйста, предложите мне лучший способ получить это ..
Аджай Шарма

1
Вы когда-нибудь делали что-то с OpenCV? Любое приложение вообще?
karlphillip

6
Стоит отметить, что флаг CV_RETR_EXTERNAL можно использовать при поиске счетчиков для отклонения всех контуров внутри замкнутой формы.
mehfoos yacoob

Ответы:


162

Это повторяющаяся тема в Stackoverflow, и, поскольку я не смог найти подходящую реализацию, я решил принять вызов.

Я внес некоторые изменения в демонстрацию квадратов, присутствующую в OpenCV, и полученный ниже код C ++ может обнаружить лист бумаги на изображении:

void find_squares(Mat& image, vector<vector<Point> >& squares)
{
    // blur will enhance edge detection
    Mat blurred(image);
    medianBlur(image, blurred, 9);

    Mat gray0(blurred.size(), CV_8U), gray;
    vector<vector<Point> > contours;

    // find squares in every color plane of the image
    for (int c = 0; c < 3; c++)
    {
        int ch[] = {c, 0};
        mixChannels(&blurred, 1, &gray0, 1, ch, 1);

        // try several threshold levels
        const int threshold_level = 2;
        for (int l = 0; l < threshold_level; l++)
        {
            // Use Canny instead of zero threshold level!
            // Canny helps to catch squares with gradient shading
            if (l == 0)
            {
                Canny(gray0, gray, 10, 20, 3); // 

                // Dilate helps to remove potential holes between edge segments
                dilate(gray, gray, Mat(), Point(-1,-1));
            }
            else
            {
                    gray = gray0 >= (l+1) * 255 / threshold_level;
            }

            // Find contours and store them in a list
            findContours(gray, contours, CV_RETR_LIST, CV_CHAIN_APPROX_SIMPLE);

            // Test contours
            vector<Point> approx;
            for (size_t i = 0; i < contours.size(); i++)
            {
                    // approximate contour with accuracy proportional
                    // to the contour perimeter
                    approxPolyDP(Mat(contours[i]), approx, arcLength(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 &&
                            fabs(contourArea(Mat(approx))) > 1000 &&
                            isContourConvex(Mat(approx)))
                    {
                            double maxCosine = 0;

                            for (int j = 2; j < 5; j++)
                            {
                                    double cosine = fabs(angle(approx[j%4], approx[j-2], approx[j-1]));
                                    maxCosine = MAX(maxCosine, cosine);
                            }

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

После выполнения этой процедуры лист бумаги станет самым большим квадратом в vector<vector<Point> >:

обнаружение листа бумаги opencv

Я позволяю вам написать функцию, чтобы найти самый большой квадрат. ;)


4
Вот почему я использую контроль источников. Наименьшая случайная модификация кода может быть легко обнаружена. Если вы ничего не меняли, попробуйте протестировать другие образы и, наконец, перекомпилировать / переустановить opencv.
karlphillip

2
OpenCV практически одинаков для всех платформ (Win / Linux / Mac / iPhone / ...). Разница в том, что некоторые не поддерживают графический модуль OpenCV. Вы уже создали OpenCV для iOS ? Вы были в состоянии проверить это? Я думаю, что это те вопросы, на которые нужно ответить, прежде чем пытаться что-то более продвинутое. Шаги малыша!
karlphillip

1
@karlphillip Я протестировал этот код и смог четко определить документ, но это занимает так много времени. Код действительно тяжелый? есть приложение под названием SayText, где это обнаружение происходит в режиме реального времени из видеопотока. Этот код был бы непрактичным для реального времени, я прав?
Аландалуси

1
Наверное. Это академический ответ, не очень практичный для отрасли. Вы можете попробовать все виды оптимизации, начиная с определения счетчика, расположенного в for (int c = 0; c < 3; c++), который отвечает за итерацию на каждом канале изображения. Например, вы можете настроить его на итерацию только на одном канале :) Не забудьте проголосовать.
karlphillip

3
@SilentPro angle()- это вспомогательная функция . Как указано в ответе, этот код основан на samples / cpp / squares.cpp, присутствующих в OpenCV.
karlphillip

40

Если не указано иное требование, я просто преобразовал бы ваше цветное изображение в оттенки серого и работал бы только с этим (нет необходимости работать с 3 каналами, контрастность слишком высока). Кроме того, если нет особых проблем с изменением размера, я бы работал с уменьшенной версией ваших изображений, так как они относительно велики, а размер ничего не добавляет к решаемой проблеме. Затем, наконец, ваша проблема решается с помощью медианного фильтра, некоторых основных морфологических инструментов и статистики (в основном для порога Otsu, который уже сделан для вас).

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

введите описание изображения здесь введите описание изображения здесь

Медианный фильтр используется для удаления мелких деталей из изображения, теперь серого. Это, возможно, удалит тонкие линии внутри беловатой бумаги, что хорошо, потому что тогда вы закончите с крошечными соединенными компонентами, которые легко выбросить. После медианы примените морфологический градиент (просто dilation- erosion) и оцифруйте результат по Оцу. Морфологический градиент - это хороший способ сохранить сильные края, его следует использовать чаще. Затем, поскольку этот градиент увеличит ширину контура, примените морфологическое истончение. Теперь вы можете отказаться от мелких компонентов.

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

введите описание изображения здесь

Учитывая примеры, теперь единственной проблемой остается различие между компонентами, которые выглядят как прямоугольники, и компонентами, которые не похожи. Это вопрос определения соотношения между площадью выпуклой оболочки, содержащей форму, и площадью ее ограничительной рамки; соотношение 0,7 прекрасно работает для этих примеров. Возможно, вам также придется отказаться от компонентов, которые находятся внутри документа, но не в этих примерах с помощью этого метода (тем не менее, выполнение этого шага должно быть очень простым, особенно потому, что это можно сделать напрямую через OpenCV).

Для справки вот пример кода в Mathematica:

f = Import["http://thwartedglamour.files.wordpress.com/2010/06/my-coffee-table-1-sa.jpg"]
f = ImageResize[f, ImageDimensions[f][[1]]/4]
g = MedianFilter[ColorConvert[f, "Grayscale"], 2]
h = DeleteSmallComponents[Thinning[
     Binarize[ImageSubtract[Dilation[g, 1], Erosion[g, 1]]]]]
convexvert = ComponentMeasurements[SelectComponents[
     h, {"ConvexArea", "BoundingBoxArea"}, #1 / #2 > 0.7 &], 
     "ConvexVertices"][[All, 2]]
(* To visualize the blue polygons above: *)
Show[f, Graphics[{EdgeForm[{Blue, Thick}], RGBColor[0, 0, 1, 0.5], 
     Polygon @@ convexvert}]]

Если есть более разнообразные ситуации, когда прямоугольник бумаги не так хорошо определен, или подход смешивает его с другими формами - эти ситуации могут произойти по разным причинам, но общей причиной является плохое получение изображения - тогда попробуйте объединить предварительные -процессы обработки с работой, описанной в статье "Обнаружение прямоугольника на основе оконного преобразования Хафа".


1
Есть ли существенная разница в реализации вашей и вышеупомянутой (например, ответ @karlphilip)? Мне жаль, что я не смог найти ничего быстрого (кроме 3-канального канала 1 и Mathematica-OpenCV).
Абид Рахман К

2
@AbidRahmanK да, есть .. Я не использую canny ни "несколько порогов" для начала. Существуют и другие отличия, но, судя по тону вашего комментария, бессмысленно прилагать усилия к моему собственному комментарию.
mmgp

1
Я вижу, что вы оба сначала найдете ребра и определите, какой из них квадратный. Для поиска ребер вы, люди, используете разные методы. Он использует хитрость, вы используете некоторую дилатацию-эрозию. И «несколько порогов», может быть, он получил из образцов OpenCV, используемых для поиска квадратов. Главное, я чувствовал, что общая концепция такая же. Msgstr "Найти края и обнаружить квадрат". И я искренне спросил, я не знаю, какой «тон» вы получили из моего комментария, или что вы (поняли / неправильно поняли). Поэтому, если вы чувствуете, что этот вопрос искренний, я хотел бы знать другие различия. В противном случае откажитесь от моих комментариев.
Абид Рахман К

1
@AbidRahmanK Конечно, концепция та же, задача та же. Используется медианная фильтрация, используется прореживание, мне все равно, откуда он взял идею о нескольких порогах - она ​​просто здесь не используется (таким образом, как это не может быть разницей?), Размер изображения здесь изменяется, Измерения компонентов различны. «Некоторая дилатационная эрозия» не дает двойных граней, для этого используется отсу. Бессмысленно упоминать об этом, код есть.
ММГП

1
К. Спасибо. Получил ответ. Concept is the same, (Я никогда не использовал Mathematica, поэтому не могу понять код.) И упомянутые вами различия - это различия, но не другой подход или основные. Если вы еще не сделали, например, проверьте это:
Абид Рахман K

14

Ну, я опоздал.


На вашем изображении бумага есть white, а фон есть colored. Таким образом, лучше обнаружить, что бумага находится в Saturation(饱和度)канале HSV color space. Возьми сначала ссылку на вики HSL_and_HSV . Затем я скопирую большинство идей из моего ответа в этом разделе «Обнаружение цветного сегмента» на изображении .


Основные шаги:

  1. Читать в BGR
  2. Преобразовать изображение из bgrв hsvкосмос
  3. Порог канал S
  4. Затем найдите максимальный внешний контур (или сделайте Canny, или, HoughLinesкак вам нравится, я выберу findContours), приблизительно, чтобы получить углы.

Это мой результат:

введите описание изображения здесь


Код Python (Python 3.5 + OpenCV 3.3):

#!/usr/bin/python3
# 2017.12.20 10:47:28 CST
# 2017.12.20 11:29:30 CST

import cv2
import numpy as np

##(1) read into  bgr-space
img = cv2.imread("test2.jpg")

##(2) convert to hsv-space, then split the channels
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
h,s,v = cv2.split(hsv)

##(3) threshold the S channel using adaptive method(`THRESH_OTSU`) or fixed thresh
th, threshed = cv2.threshold(s, 50, 255, cv2.THRESH_BINARY_INV)

##(4) find all the external contours on the threshed S
#_, cnts, _ = cv2.findContours(threshed, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
cnts = cv2.findContours(threshed, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[-2]

canvas  = img.copy()
#cv2.drawContours(canvas, cnts, -1, (0,255,0), 1)

## sort and choose the largest contour
cnts = sorted(cnts, key = cv2.contourArea)
cnt = cnts[-1]

## approx the contour, so the get the corner points
arclen = cv2.arcLength(cnt, True)
approx = cv2.approxPolyDP(cnt, 0.02* arclen, True)
cv2.drawContours(canvas, [cnt], -1, (255,0,0), 1, cv2.LINE_AA)
cv2.drawContours(canvas, [approx], -1, (0, 0, 255), 1, cv2.LINE_AA)

## Ok, you can see the result as tag(6)
cv2.imwrite("detected.png", canvas)

Связанные ответы:

  1. Как обнаружить цветные пятна на изображении с помощью OpenCV?
  2. Обнаружение краев на цветном фоне с использованием OpenCV
  3. OpenCV C ++ / Obj-C: обнаружение листа бумаги / определение квадратов
  4. Как использовать `cv2.findContours` в разных версиях OpenCV?

Я пытался использовать пространство S, но все равно не смог добиться успеха. Смотрите это: stackoverflow.com/questions/50699893/…
hchouhan02

3

Вам нужен четырехугольник вместо повернутого прямоугольника. RotatedRectдаст вам неверные результаты. Также вам понадобится перспективная проекция.

В основном, что должно быть сделано:

  • Переберите все сегменты многоугольника и соедините те, которые почти равны.
  • Сортируйте их так, чтобы у вас было 4 самых больших отрезка.
  • Пересечь эти линии, и у вас есть 4 наиболее вероятных угловых точек.
  • Преобразуйте матрицу по перспективе, собранной из угловых точек и соотношению сторон известного объекта.

Я реализовал класс, Quadrangleкоторый заботится о преобразовании контура в четырехугольник, а также преобразует его в правильной перспективе.

Смотрите рабочую реализацию здесь: Java OpenCV выравнивание контура


1

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


Входное изображение:

Обнаруженный текстовый объект

Вид сверху текстового документа

Код

from imutils.perspective import four_point_transform
import cv2
import numpy

# Load image, grayscale, Gaussian blur, Otsu's threshold
image = cv2.imread("1.png")
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
blur = cv2.GaussianBlur(gray, (7,7), 0)
thresh = cv2.threshold(blur, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)[1]

# Find contours and sort for largest contour
cnts = cv2.findContours(thresh, cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)
cnts = cnts[0] if len(cnts) == 2 else cnts[1]
cnts = sorted(cnts, key=cv2.contourArea, reverse=True)
displayCnt = None

for c in cnts:
    # Perform contour approximation
    peri = cv2.arcLength(c, True)
    approx = cv2.approxPolyDP(c, 0.02 * peri, True)
    if len(approx) == 4:
        displayCnt = approx
        break

# Obtain birds' eye view of image
warped = four_point_transform(image, displayCnt.reshape(4, 2))

cv2.imshow("thresh", thresh)
cv2.imshow("warped", warped)
cv2.imshow("image", image)
cv2.waitKey()

-1

Обнаружение листа бумаги - своего рода старая школа. Если вы хотите заняться обнаружением перекоса, то лучше сразу же стремиться к обнаружению текстовых строк. При этом вы получите крайние значения слева, справа, сверху и снизу. Откажитесь от любой графики на изображении, если вы не хотите, а затем сделайте некоторую статистику по сегментам текстовой линии, чтобы найти наиболее часто встречающийся угол или, точнее, угол. Вот как вы будете сужаться до хорошего угла наклона. Теперь после этого вы устанавливаете эти параметры для угла наклона и экстремумов и выравниваете изображение до требуемого.

Что касается текущего требования к изображению, лучше, если вы попробуете CV_RETR_EXTERNAL вместо CV_RETR_LIST.

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

Случайные леса будут работать с низкоконтрастными сценариями, например, на белой бумаге с примерно белым фоном.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.