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

EDIT 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 / ...). Різниця полягає в тому, що деякі не підтримують GPU-модуль OpenCV. Ви вже створили OpenCV для iOS ? Ви змогли це випробувати? Я думаю, що це питання, на які потрібно відповісти, перш ніж спробувати щось більш вдосконалене. Дитячі кроки!
karlphillip

1
@karlphillip Я перевірив цей код, і мені вдалося чітко виявити папір, але це займає стільки часу. Чи дійсно код важкий? є додаток під назвою SayText, де це виявлення відбувається в режимі реального часу з відеопотоку. Цей код був би недоцільним для реального часу, я прав?
alandalusi

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

3
@SilentPro angle()- це помічна функція . Як зазначено у відповіді, цей код заснований на зразках / cpp / squares.cpp, присутніх у OpenCV.
karlphillip

40

Якщо не вказано якусь іншу вимогу, я б просто перетворив ваше кольорове зображення в масштаб сірого і працюю тільки з цим (не потрібно працювати над 3-ма каналами, контраст присутній вже занадто високо). Крім того, якщо не існує певної проблеми щодо зміни розміру, я б працював із зменшеною шкалою ваших зображень, оскільки вони відносно великі і розмір нічого не додає до вирішення проблеми. Потім, нарешті, ваша проблема вирішується за допомогою медіанного фільтра, деяких основних морфологічних інструментів та статистики (переважно для порогу Оцу, що вже зроблено для вас).

Ось що я отримую з вашим зразком зображення та деяким іншим зображенням із аркушем паперу, який я знайшов навколо:

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

Середній фільтр використовується для видалення другорядних деталей із теперішнього масштабу зображення. Можливо, це видалить тонкі лінії всередині білуватого паперу, що добре, адже тоді ви закінчите крихітними з'єднаними компонентами, які легко відмовитися. Після медіани застосуйте морфологічний градієнт (просто 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 звичайно концепція однакова, завдання те саме. Середня фільтрація використовується, стоншення використовується, мені байдуже, звідки він взяв кілька порогів ідеї - він просто не використовується тут (таким чином, як це не може бути різницею?), Зображення змінюється тут, Вимірювання компонентів різні. "Деяка дилатація-ерозія" не дає бінарних ребер, для цього використовується otsu. Про це безглуздо згадувати, код є.
mmgp

1
К. Дякую. Отримав відповідь. Concept is the same. (Я ніколи не використовував Mathematica, тому не можу зрозуміти код.) І відмінності, про які ви згадали, - це відмінності, але не інший підхід чи основні. Наприклад, перевірте це:
Абід Рахман К

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.