OpenCV C ++ / Obj-C: Wykrywanie arkusza papieru / Wykrywanie kwadratu

178

Z powodzeniem wdrożyłem przykład wykrywania kwadratu OpenCV w mojej aplikacji testowej, ale teraz muszę filtrować dane wyjściowe, ponieważ jest to dość bałagan - czy mój kod jest nieprawidłowy?

Interesują mnie cztery punkty narożne papieru w celu zmniejszenia pochylenia (jak to ) i dalszego przetwarzania…

Wejście wyjście: Wejście wyjście

Oryginalny obraz:

Kliknij

Kod:

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

EDYCJA 17/08/2012:

Aby narysować wykryte kwadraty na obrazie, użyj tego kodu:

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;
}
dom
źródło
1
Myślę, że możesz zmienić tytuł pytania, na przykład Wykrywanie kartki papieru , jeśli uważasz, że jest to bardziej odpowiednie.
karlphillip
1
@moosgummi Chcę mieć tę samą funkcjonalność, którą zaimplementowałeś, tj. „Wykryj rogi przechwyconego obrazu / dokumentu”. Jak to osiągnąłeś? Czy będę mógł używać OpenCV w mojej aplikacji na iPhone'a? Proszę zasugerować mi lepszy sposób, aby to mieć ...
Ajay Sharma
1
Czy kiedykolwiek zrobiłeś coś z OpenCV? Jakaś aplikacja w ogóle?
karlphillip
6
Warto zauważyć, że flaga CV_RETR_EXTERNAL może być używana podczas znajdowania konturów do odrzucania wszystkich konturów w zamkniętym kształcie.
mehfoos yacoob

Odpowiedzi:

162

Jest to powtarzający się temat w Stackoverflow, a ponieważ nie mogłem znaleźć odpowiedniej implementacji, postanowiłem zaakceptować wyzwanie.

Wprowadziłem kilka modyfikacji kwadratu demonstracyjnego obecnego w OpenCV i wynikowy kod C ++ poniżej jest w stanie wykryć arkusz papieru na obrazie:

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

Po wykonaniu tej procedury arkusz papieru będzie największym kwadratem w vector<vector<Point> >:

wykrywanie arkuszy papieru opencv

Pozwalam ci napisać funkcję znalezienia największego kwadratu. ;)

karlphillip
źródło
4
Dlatego używam kontroli źródła. Najmniejszą przypadkową modyfikację kodu można łatwo wykryć. Jeśli niczego nie zmieniłeś, spróbuj przetestować z innymi obrazami i na końcu ponownie skompiluj / zainstaluj ponownie opencv.
karlphillip
2
OpenCV jest prawie taki sam dla wszystkich platform (Win / Linux / Mac / iPhone / ...). Różnica polega na tym, że niektóre nie obsługują modułu GPU OpenCV. Czy zbudowałeś już OpenCV na iOS ? Czy byłeś w stanie to przetestować? Myślę, że na te pytania musisz odpowiedzieć, zanim spróbujesz czegoś bardziej zaawansowanego. Małymi kroczkami!
karlphillip
1
@karlphillip Przetestowałem ten kod i byłem w stanie wyraźnie wykryć papier, ale zajmuje to dużo czasu. Czy kod jest naprawdę ciężki? istnieje aplikacja o nazwie SayText, w której wykrywanie odbywa się w czasie rzeczywistym ze strumienia wideo. Ten kod byłby niepraktyczny w czasie rzeczywistym, prawda?
alandalusi
1
Prawdopodobnie. To odpowiedź akademicka, niezbyt praktyczna dla branży. Istnieje wiele rodzajów optymalizacji, które możesz wypróbować, poczynając od definicji licznika znajdującego się w for (int c = 0; c < 3; c++), który odpowiada za iterację na każdym kanale obrazu. Na przykład możesz ustawić iterację tylko na jednym kanale :) Nie zapomnij podnieść głosu.
karlphillip
3
@SilentPro angle()to funkcja pomocnicza . Jak stwierdzono w odpowiedzi, ten kod jest oparty na próbkach / cpp / squares.cpp obecnych w OpenCV.
karlphillip
40

O ile nie określono innych wymagań, po prostu przekonwertowałbym twój kolorowy obraz na skalę szarości i pracowałem tylko z tym (nie trzeba pracować na 3 kanałach, obecny kontrast jest już zbyt wysoki). Ponadto, chyba że istnieje jakiś konkretny problem dotyczący zmiany rozmiaru, pracowałbym ze zmniejszoną wersją twoich obrazów, ponieważ są one stosunkowo duże, a rozmiar nic nie dodaje do rozwiązania problemu. Wreszcie twój problem został rozwiązany dzięki filtrowi medianowemu, niektórym podstawowym narzędziom morfologicznym i statystykom (głównie w przypadku progowania Otsu, które jest już dla Ciebie zrobione).

Oto, co otrzymuję z twojego przykładowego obrazu i jakiegoś innego obrazu z arkuszem papieru, który znalazłem wokół:

wprowadź opis zdjęcia tutaj wprowadź opis zdjęcia tutaj

Filtr środkowy służy do usuwania drobnych szczegółów z obrazu, teraz w skali szarości. Prawdopodobnie usunie cienkie linie wewnątrz białawego papieru, co jest dobre, ponieważ wtedy skończysz z drobnymi połączonymi komponentami, które są łatwe do odrzucenia. Po medianie zastosuj gradient morfologiczny (po prostudilation -erosion ) i binaryzuj wynik przez Otsu. Gradient morfologiczny jest dobrą metodą na utrzymanie silnych krawędzi, należy go częściej stosować. Następnie, ponieważ ten gradient zwiększy szerokość konturu, zastosuj przerzedzenie morfologiczne. Teraz możesz odrzucić małe elementy.

W tym momencie oto, co mamy z prawym obrazem powyżej (przed narysowaniem niebieskiego wielokąta), lewy nie jest pokazany, ponieważ jedynym pozostałym składnikiem jest ten opisujący papier:

wprowadź opis zdjęcia tutaj

Biorąc pod uwagę przykłady, teraz pozostaje tylko kwestia rozróżnienia między komponentami, które wyglądają jak prostokąty, a innymi, które nie. Jest to kwestia ustalenia stosunku między obszarem wypukłego kadłuba zawierającym kształt a obszarem jego obwiedni; stosunek 0,7 działa dobrze dla tych przykładów. Może się zdarzyć, że będziesz musiał także odrzucić komponenty znajdujące się w papierze, ale nie w tych przykładach, używając tej metody (niemniej jednak wykonanie tego kroku powinno być bardzo łatwe, zwłaszcza, że ​​można to zrobić bezpośrednio przez OpenCV).

Dla odniesienia, oto przykładowy kod w 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}]]

Jeśli istnieją bardziej zróżnicowane sytuacje, w których prostokąt papieru nie jest tak dobrze zdefiniowany, lub podejście myli go z innymi kształtami - sytuacje te mogą się zdarzyć z różnych przyczyn, ale częstą przyczyną jest zła akwizycja obrazu - następnie spróbuj połączyć -przetwarzanie kroków z pracą opisaną w artykule „Wykrywanie prostokąta na podstawie transformacji Windough Hougha”.

mmgp
źródło
1
czy jest jakaś znacząca różnica w implementacji twojej i powyższej (tj. odpowiedź @karlphilip)? Przepraszam, że nie udało mi się znaleźć żadnego szybkiego spojrzenia (oprócz kanału 3-kanałowego 1 i Mathematica-OpenCV).
Abid Rahman K
2
@AbidRahmanK tak, są .. Nie używam Canny ani „kilku progów” na początek. Istnieją inne różnice, ale tonem twojego komentarza wydaje się bezcelowe wkładanie jakiegokolwiek wysiłku w mój komentarz.
mmgp
1
Widzę, że oboje najpierw znajdź krawędzie i określ, która krawędź jest kwadratowa. Aby znaleźć krawędzie, używacie różnych metod. On używa sprytu, ty używasz erozji dylatacyjnej. I „kilka progów”, być może pochodzi z próbek OpenCV, używanych do znajdowania kwadratów. Najważniejsze, że czułem, że ogólna koncepcja jest taka sama. „Znajdź krawędzie i wykryj kwadrat”. I szczerze o to zapytałem, nie wiem jaki „ton” otrzymałeś z mojego komentarza lub co (zrozumiałeś / źle zrozumiałeś). Więc jeśli uważasz, że to pytanie jest szczere, chciałbym poznać inne różnice. W przeciwnym razie odrzuć moje komentarze.
Abid Rahman K
1
@AbidRahmanK oczywiście koncepcja jest taka sama, zadanie jest takie samo. Stosuje się filtrowanie medianowe, stosuje się przerzedzanie, nie obchodzi mnie, skąd wziął kilka pomysłów na progi - po prostu go tu nie używa (a więc jak to nie może być różnicy?), Tutaj zmienia się rozmiar obrazu, pomiary komponentów są różne. „Pewna erozja dylatacyjna” nie daje podwójnych krawędzi, do tego używa się otsu. Nie ma sensu o tym wspominać, kod tam jest.
mmgp
1
K. Dziękuję Mam odpowiedź. Concept is the same. (Nigdy nie korzystałem z Mathematiki, więc nie rozumiem kodu.) Wspomniane różnice są różnicami, ale nie innym podejściem ani głównymi. Jeśli nadal tego nie robiłeś Na przykład sprawdź to:
Abid Rahman K
14

Jestem spóźniony.


Na twoim zdjęciu jest papier white, a tło jest colored. Tak, to lepiej, aby wykryć papier jest Saturation(饱和度)kanał w HSV color space. Najpierw zapoznaj się z wiki HSL_i_HSV . Następnie skopiuję większość pomysłów z mojej odpowiedzi w tym Wykryj kolorowy segment na obrazie .


Główne kroki:

  1. Doszukiwać się ukrytego znaczenia BGR
  2. Konwertuj obraz z bgrna hsvspację
  3. Przekrocz próg kanału S.
  4. Następnie znajdź maksymalny kontur zewnętrzny (lub wykonaj Cannylub, HoughLinesjak chcesz, wybieram findContours), w przybliżeniu, aby uzyskać rogi.

Oto mój wynik:

wprowadź opis zdjęcia tutaj


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

Powiązane odpowiedzi:

  1. Jak wykryć kolorowe plamy na obrazie za pomocą OpenCV?
  2. Wykrywanie krawędzi na kolorowym tle za pomocą OpenCV
  3. OpenCV C ++ / Obj-C: Wykrywanie arkusza papieru / Wykrywanie kwadratu
  4. Jak używać `cv2.findContours` w różnych wersjach OpenCV?
Kinght 金
źródło
Próbowałem użyć S space, ale nadal nie mogłem osiągnąć sukcesu. Zobacz: stackoverflow.com/questions/50699893/…
hchouhan02
3

Potrzebny jest czworokąt zamiast obróconego prostokąta. RotatedRectda nieprawidłowe wyniki. Będziesz także potrzebować projekcji perspektywicznej.

Zasadniczo należy zrobić:

  • Zapętl wszystkie segmenty wielokątów i połącz te, które prawie się wyrównują.
  • Posortuj je, aby uzyskać 4 największe segmenty linii.
  • Przecinaj te linie, a masz 4 najbardziej prawdopodobne punkty narożne.
  • Przekształć matrycę nad perspektywą zebraną od punktów narożnych i współczynnikiem kształtu znanego obiektu.

Zaimplementowałem klasę Quadrangle która zajmuje się konwersją konturu do czworokąta, a także przekształci go we właściwej perspektywie.

Zobacz działającą implementację tutaj: Java OpenCV deskewing kontur

Tim
źródło
1

Po wykryciu ramki granicznej dokumentu możesz wykonać czteropunktową transformację perspektywiczną, aby uzyskać widok z góry z lotu ptaka obrazu. To naprawi pochylenie i wyizoluje tylko pożądany obiekt.


Obraz wejściowy:

Wykryty obiekt tekstowy

Widok z góry dokumentu tekstowego

Kod

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()
nathancy
źródło
-1

Wykrywanie kartki papieru to trochę stara szkoła. Jeśli chcesz zająć się wykrywaniem przekrzywienia, lepiej jest od razu dążyć do wykrycia linii tekstu. Dzięki temu uzyskasz skrajności w lewo, prawo, góra i dół. Odrzuć dowolną grafikę na obrazie, jeśli nie chcesz, a następnie wykonaj statystyki dotyczące segmentów linii tekstu, aby znaleźć najbardziej występujący zakres kątów, a raczej kąt. W ten sposób zawęzisz się do dobrego kąta pochylenia. Teraz ustawiasz te parametry kąt pochylenia i skrajności do prostowania i przycinasz obraz do wymaganego.

Jeśli chodzi o bieżące wymagania dotyczące obrazu, lepiej jest wypróbować CV_RETR_EXTERNAL zamiast CV_RETR_LIST.

Inną metodą wykrywania krawędzi jest trenowanie losowego klasyfikatora lasów na krawędziach papieru, a następnie użycie klasyfikatora do uzyskania mapy krawędzi. Jest to zdecydowanie solidna metoda, ale wymaga szkolenia i czasu.

Losowe lasy będą działać ze scenariuszami o niskiej różnicy kontrastu, na przykład białym papierze na w przybliżeniu białym tle.

Anubhav Rohatgi
źródło