OpenCV C ++ / Obj-C: Erkennen eines Blattes Papier / Quadraterkennung


178

Ich habe das OpenCV-Quadraterkennungsbeispiel erfolgreich in meiner Testanwendung implementiert, muss jetzt aber die Ausgabe filtern, da sie ziemlich chaotisch ist - oder ist mein Code falsch?

Ich bin für Skew Reduktion in den vier Eckpunkten des Papiers interessiert (wie das ) und die weitere Verarbeitung ...

Input-Output: Input-Output

Original Bild:

klicken

Code:

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:

Verwenden Sie diesen Code, um die erkannten Quadrate auf dem Bild zu zeichnen:

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
Ich denke, Sie können den Titel der Frage für etwas wie das Erkennen eines Blattes Papier anpassen , wenn Sie es für angemessener halten.
Karlphillip

1
@moosgummi Ich möchte die gleiche Funktionalität haben, die Sie implementiert haben, z. B. "Die Ecken des aufgenommenen Bildes / Dokuments erkennen". Wie haben Sie dies erreicht? Kann ich OpenCV in meiner iPhone-Anwendung verwenden? Bitte schlagen Sie mir einen besseren Weg vor, um dies zu haben ..
Ajay Sharma

1
Haben Sie jemals etwas mit OpenCV gemacht? Irgendeine Bewerbung überhaupt?
Karlphillip

6
Es ist erwähnenswert, dass das Flag CV_RETR_EXTERNAL verwendet werden kann, wenn die Countours gefunden werden, um alle Konturen innerhalb einer geschlossenen Form abzulehnen.
Mehfoos Yacoob

Antworten:


162

Dies ist ein wiederkehrendes Thema in Stackoverflow. Da ich keine relevante Implementierung finden konnte, habe ich mich entschlossen, die Herausforderung anzunehmen.

Ich habe einige Änderungen an der in OpenCV vorhandenen Squares-Demo vorgenommen, und der resultierende C ++ - Code unten kann ein Blatt Papier im Bild erkennen:

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

Nachdem dieser Vorgang ausgeführt wurde, ist das Blatt Papier das größte Quadrat in vector<vector<Point> >:

Erkennung von OpenCV-Papierblättern

Ich lasse Sie die Funktion schreiben, um das größte Quadrat zu finden. ;)


4
Deshalb benutze ich die Quellcodeverwaltung. Die kleinste versehentliche Änderung des Codes kann leicht entdeckt werden. Wenn Sie nichts geändert haben, versuchen Sie, mit anderen Bildern zu testen, und kompilieren Sie opencv schließlich neu.
Karlphillip

2
OpenCV ist für alle Plattformen (Win / Linux / Mac / iPhone / ...) ziemlich gleich. Der Unterschied besteht darin, dass einige das GPU-Modul von OpenCV nicht unterstützen. Haben Sie OpenCV für iOS bereits erstellt ? Konnten Sie es testen? Ich denke, dies sind die Fragen, die Sie beantworten müssen, bevor Sie etwas Fortgeschritteneres ausprobieren. Kleine Schritte!
Karlphillip

1
@karlphillip Ich habe diesen Code getestet und konnte das Papier klar erkennen, aber es dauert so lange. Ist der Code wirklich schwer? Es gibt eine App namens SayText, bei der diese Erkennung in Echtzeit aus einem Videostream erfolgt. Dieser Code wäre für Echtzeit unpraktisch, habe ich recht?
Alandalusi

1
Wahrscheinlich. Dies ist eine akademische Antwort, die für die Branche nicht sehr praktisch ist. Es gibt alle Arten von Optimierungen, die Sie ausprobieren können, beginnend mit der Definition des Zählers unter for (int c = 0; c < 3; c++), der für die Iteration auf jedem Kanal des Bildes verantwortlich ist. Zum Beispiel können Sie festlegen, dass es nur auf einem Kanal iteriert :) Vergessen Sie nicht, die Abstimmung zu erhöhen.
Karlphillip

3
@SilentPro angle()ist eine Hilfsfunktion . Wie in der Antwort angegeben, basiert dieser Code auf samples / cpp / squares.cpp, die in OpenCV vorhanden sind.
Karlphillip

40

Sofern keine andere Anforderung nicht angegeben ist, würde ich Ihr Farbbild einfach in Graustufen konvertieren und nur damit arbeiten (keine Notwendigkeit, auf den 3 Kanälen zu arbeiten, der vorhandene Kontrast ist bereits zu hoch). Außerdem würde ich mit einer verkleinerten Version Ihrer Bilder arbeiten, es sei denn, es gibt ein spezifisches Problem bei der Größenänderung, da diese relativ groß sind und die Größe dem zu lösenden Problem nichts hinzufügt. Schließlich wird Ihr Problem mit einem Medianfilter, einigen grundlegenden morphologischen Werkzeugen und Statistiken gelöst (hauptsächlich für die Otsu-Schwellwertbildung, die bereits für Sie durchgeführt wurde).

Folgendes erhalte ich mit Ihrem Beispielbild und einem anderen Bild mit einem Blatt Papier, das ich gefunden habe:

Geben Sie hier die Bildbeschreibung ein Geben Sie hier die Bildbeschreibung ein

Der Medianfilter wird verwendet, um kleinere Details aus dem jetzt Graustufenbild zu entfernen. Möglicherweise werden dünne Linien im weißlichen Papier entfernt, was gut ist, da Sie dann mit winzigen verbundenen Komponenten enden, die leicht zu verwerfen sind. Wenden Sie nach dem Median einen morphologischen Gradienten an (einfach dilation- erosion) und binarisieren Sie das Ergebnis mit Otsu. Der morphologische Gradient ist eine gute Methode, um starke Kanten zu erhalten. Er sollte häufiger verwendet werden. Wenden Sie dann eine morphologische Ausdünnung an, da dieser Gradient die Konturbreite erhöht. Jetzt können Sie kleine Komponenten verwerfen.

An dieser Stelle haben wir Folgendes mit dem rechten Bild oben (vor dem Zeichnen des blauen Polygons), das linke wird nicht angezeigt, da die einzige verbleibende Komponente diejenige ist, die das Papier beschreibt:

Geben Sie hier die Bildbeschreibung ein

In Anbetracht der Beispiele bleibt nur noch die Unterscheidung zwischen Komponenten, die wie Rechtecke aussehen, und anderen, die dies nicht tun. Hierbei wird ein Verhältnis zwischen der Fläche der konvexen Hülle, die die Form enthält, und der Fläche ihres Begrenzungsrahmens bestimmt. Das Verhältnis 0,7 funktioniert für diese Beispiele gut. Es kann vorkommen, dass Sie auch Komponenten im Papier verwerfen müssen, jedoch nicht in diesen Beispielen, indem Sie diese Methode verwenden (dieser Schritt sollte jedoch sehr einfach sein, insbesondere weil er direkt über OpenCV ausgeführt werden kann).

Als Referenz finden Sie hier einen Beispielcode in 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}]]

Wenn es unterschiedlichere Situationen gibt, in denen das Rechteck des Papiers nicht so gut definiert ist oder der Ansatz es mit anderen Formen verwechselt - diese Situationen können aus verschiedenen Gründen auftreten, aber eine häufige Ursache ist eine schlechte Bildaufnahme -, versuchen Sie, das Pre zu kombinieren -Verarbeitungsschritte mit der im Artikel "Rechteckerkennung basierend auf einer Windowed Hough Transformation" beschriebenen Arbeit.


1
Gibt es einen großen Unterschied in der Implementierung Ihrer und der oben genannten (dh die Antwort von @karlphilip)? Es tut mir leid, dass ich auf keinen Fall einen gefunden habe (außer 3-Kanal-1-Kanal und Mathematica-OpenCV).
Abid Rahman K

2
@AbidRahmanK ja, es gibt .. Ich benutze weder canny noch "mehrere Schwellenwerte", um damit zu beginnen. Es gibt andere Unterschiede, aber aufgrund des Tons Ihres Kommentars erscheint es sinnlos, sich um meinen eigenen Kommentar zu bemühen.
mmgp

1
Ich sehe, dass Sie beide zuerst die Kanten finden und bestimmen, welche Kante quadratisch ist. Um Kanten zu finden, verwenden Sie verschiedene Methoden. Er benutzt Canny, du benutzt etwas Dilatations-Erosion. Und "mehrere Schwellenwerte", die er möglicherweise aus OpenCV-Proben erhalten hat, um Quadrate zu finden. Hauptsache, ich fand das Gesamtkonzept gleich. "Kanten finden und Quadrat erkennen". Und ich habe es aufrichtig gefragt, ich weiß nicht, welchen "Ton" Sie von meinem Kommentar bekommen haben oder was Sie (verstanden / missverstanden) haben. Wenn Sie der Meinung sind, dass diese Frage aufrichtig ist, würde ich gerne weitere Unterschiede erfahren. Ansonsten verwerfe meine Kommentare.
Abid Rahman K

1
@AbidRahmanK natürlich ist das Konzept das gleiche, die Aufgabe ist das gleiche. Medianfilterung wird verwendet, Ausdünnung wird verwendet, es ist mir egal, woher er mehrere Schwellenwertideen genommen hat - es wird hier einfach nicht verwendet (also wie kann es kein Unterschied sein?), Das Bild wird hier in der Größe geändert Komponentenmessungen sind unterschiedlich. "Etwas Dilatations-Erosion" ergibt keine binären Kanten, dafür wird otsu verwendet. Es ist sinnlos, dies zu erwähnen, der Code ist da.
mmgp

1
K. Danke. Habe die Antwort. Concept is the same. (Ich habe Mathematica nie verwendet, daher kann ich den Code nicht verstehen.) Und die Unterschiede, die Sie erwähnt haben, sind Unterschiede, aber keine anderen oder größeren Ansätze. Wenn Sie dies beispielsweise noch nicht getan haben, überprüfen Sie Folgendes:
Abid Rahman K

14

Nun, ich bin zu spät.


In Ihrem Bild ist das Papier white, während der Hintergrund ist colored. Es ist also besser zu erkennen, dass das Papier Saturation(饱和度)kanalisiert ist HSV color space. Lesen Sie zuerst das Wiki HSL_and_HSV . Dann kopiere ich die meisten Ideen aus meiner Antwort in dieses farbige Segment erkennen in ein Bild .


Hauptschritte:

  1. Lesen Sie in BGR
  2. Konvertieren Sie das Bild von bgrin den hsvWeltraum
  3. Schwelle zum S-Kanal
  4. Finden Sie dann die maximale Außenkontur (oder tun Sie Cannyoder HoughLineswie Sie möchten, ich wähle findContours), ca., um die Ecken zu erhalten.

Das ist mein Ergebnis:

Geben Sie hier die Bildbeschreibung ein


Der Python-Code (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)

Verwandte Antworten:

  1. Wie erkenne ich mit OpenCV farbige Flecken in einem Bild?
  2. Kantenerkennung auf farbigem Hintergrund mit OpenCV
  3. OpenCV C ++ / Obj-C: Erkennen eines Blattes Papier / Quadraterkennung
  4. Wie verwende ich "cv2.findContours" in verschiedenen OpenCV-Versionen?

Ich habe versucht, S-Space zu verwenden, konnte aber immer noch keinen Erfolg haben. Siehe dies: stackoverflow.com/questions/50699893/…
hchouhan02

3

Was Sie brauchen, ist ein Viereck anstelle eines gedrehten Rechtecks. RotatedRectgibt Ihnen falsche Ergebnisse. Außerdem benötigen Sie eine perspektivische Projektion.

Grundsätzlich muss Folgendes getan werden:

  • Durchlaufen Sie alle Polygonsegmente und verbinden Sie diejenigen, die fast gleich sind.
  • Sortieren Sie sie so, dass Sie die 4 größten Liniensegmente haben.
  • Schneiden Sie diese Linien und Sie haben die 4 wahrscheinlichsten Eckpunkte.
  • Transformieren Sie die Matrix über die aus den Eckpunkten und dem Seitenverhältnis des bekannten Objekts gesammelte Perspektive.

Ich habe eine Klasse implementiert, Quadrangledie sich um die Konvertierung von Konturen in Vierecke kümmert und diese auch über die richtige Perspektive transformiert.

Eine funktionierende Implementierung finden Sie hier: Java OpenCV entwirft eine Kontur


1

Sobald Sie den Begrenzungsrahmen des Dokuments erkannt haben, können Sie eine Vierpunkt-Perspektiventransformation durchführen , um eine Draufsicht auf das Bild von oben nach unten zu erhalten. Dadurch wird der Versatz behoben und nur das gewünschte Objekt isoliert.


Eingabebild:

Erkanntes Textobjekt

Ansicht des Textdokuments von oben nach unten

Code

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

Das Erkennen eines Blattes Papier ist eine alte Schule. Wenn Sie sich mit der Erkennung von Schräglauf befassen möchten, ist es besser, wenn Sie sofort die Erkennung von Textzeilen anstreben. Damit erhalten Sie die Extrema links, rechts, oben und unten. Verwerfen Sie alle Grafiken im Bild, wenn Sie dies nicht möchten, und führen Sie dann einige Statistiken zu den Textzeilensegmenten durch, um den am häufigsten auftretenden Winkelbereich bzw. Winkel zu ermitteln. Auf diese Weise werden Sie auf einen guten Schräglaufwinkel eingrenzen. Danach setzen Sie diese Parameter auf den Schräglaufwinkel und die Extreme, um das Bild zu entschneiden und auf das zu schneiden, was erforderlich ist.

Für die aktuelle Bildanforderung ist es besser, CV_RETR_EXTERNAL anstelle von CV_RETR_LIST zu verwenden.

Eine andere Methode zum Erkennen von Kanten besteht darin, einen zufälligen Waldklassifizierer an den Papierkanten zu trainieren und dann den Klassifizierer zu verwenden, um die Kantenzuordnung zu erhalten. Dies ist bei weitem eine robuste Methode, erfordert jedoch Schulung und Zeit.

Zufällige Wälder funktionieren mit Szenarien mit geringem Kontrastunterschied, z. B. Whitepaper auf ungefähr weißem Hintergrund.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.