OpenCV C ++ / Obj-C: Détection d'une feuille de papier / Détection de carré

178

J'ai implémenté avec succès l'exemple de détection de carrés OpenCV dans mon application de test, mais je dois maintenant filtrer la sortie, car c'est assez compliqué - ou mon code est-il erroné?

Je suis intéressé par les quatre points de coin du papier pour la réduction de biais (comme ça ) et le traitement ultérieur ...

Entrée sortie: Entrée sortie

Image originale:

Cliquez sur

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:

Pour dessiner les carrés détectés sur l'image, utilisez ce code:

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
la source
1
Je pense que vous pouvez ajuster le titre de la question pour quelque chose comme Détecter une feuille de papier , si vous pensez que c'est plus approprié.
karlphillip le
1
@moosgummi Je cherche à avoir la même fonctionnalité que vous avez implémentée à savoir "Détecter les coins de l'image / du document capturé". Comment avez-vous réalisé cela? Pourrais-je utiliser OpenCV dans mon application iPhone? Veuillez me suggérer une meilleure façon d'avoir ceci ..
Ajay Sharma
1
Avez-vous déjà fait quelque chose avec OpenCV? Une application du tout?
karlphillip
6
Il convient de noter que l'indicateur CV_RETR_EXTERNAL peut être utilisé lors de la recherche des compteurs pour rejeter tous les contours à l'intérieur d'une forme fermée.
mehfoos yacoob

Réponses:

162

C'est un sujet récurrent dans Stackoverflow et comme je n'ai pas pu trouver d'implémentation pertinente, j'ai décidé d'accepter le défi.

J'ai apporté quelques modifications à la démo de squares présente dans OpenCV et le code C ++ résultant ci-dessous est capable de détecter une feuille de papier dans l'image:

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

Une fois cette procédure exécutée, la feuille de papier sera le plus grand carré de vector<vector<Point> >:

détection de feuille de papier opencv

Je vous laisse écrire la fonction pour trouver le plus grand carré. ;)

karlphillip
la source
4
C'est pourquoi j'utilise le contrôle de source. La plus petite modification accidentelle du code peut être facilement découverte. Si vous n'avez rien changé, essayez de tester avec d'autres images et enfin recompilez / réinstallez opencv.
karlphillip
2
OpenCV est à peu près le même pour toutes les plates-formes (Win / Linux / Mac / iPhone / ...). La différence est que certains ne prennent pas en charge le module GPU d'OpenCV. Avez-vous déjà construit OpenCV pour iOS ? Avez-vous pu le tester? Je pense que ce sont les questions auxquelles vous devez répondre avant d'essayer quelque chose de plus avancé. Pas de bébé!
karlphillip
1
@karlphillip J'ai testé ce code et j'ai pu détecter clairement le papier, mais cela prend tellement de temps. Le code est-il vraiment lourd? il existe une application appelée SayText où cette détection se produit en temps réel à partir d'un flux vidéo. Ce code ne serait pas pratique en temps réel, n'est-ce pas?
alandalusi
1
Probablement. C'est une réponse académique, pas très pratique pour l'industrie. Il existe toutes sortes d'optimisations que vous pouvez essayer, à commencer par la définition du compteur situé à for (int c = 0; c < 3; c++), qui se charge d'itérer sur chaque canal de l'image. Par exemple, vous pouvez le configurer pour qu'il itère sur un seul canal :) N'oubliez pas de voter.
karlphillip
3
@SilentPro angle()est une fonction d'assistance . Comme indiqué dans la réponse, ce code est basé sur samples / cpp / squares.cpp présents dans OpenCV.
karlphillip
40

Sauf s'il y a une autre exigence non spécifiée, je convertirais simplement votre image couleur en niveaux de gris et travaillerais uniquement avec cela (pas besoin de travailler sur les 3 canaux, le contraste présent est déjà trop élevé). De plus, à moins qu'il n'y ait un problème spécifique concernant le redimensionnement, je travaillerais avec une version réduite de vos images, car elles sont relativement grandes et la taille n'ajoute rien au problème résolu. Ensuite, enfin, votre problème est résolu avec un filtre médian, quelques outils morphologiques de base et des statistiques (principalement pour le seuillage Otsu, qui est déjà fait pour vous).

Voici ce que j'obtiens avec votre exemple d'image et une autre image avec une feuille de papier que j'ai trouvée autour:

entrez la description de l'image ici entrez la description de l'image ici

Le filtre médian est utilisé pour supprimer les détails mineurs de l'image, désormais en niveaux de gris. Cela supprimera éventuellement les lignes fines à l'intérieur du papier blanchâtre, ce qui est bien car vous vous retrouverez alors avec de minuscules composants connectés faciles à jeter. Après la médiane, appliquez un gradient morphologique (simplement dilation-erosion ) et binarisez le résultat par Otsu. Le gradient morphologique est une bonne méthode pour conserver des arêtes fortes, il devrait être utilisé davantage. Puis, puisque ce dégradé augmentera la largeur du contour, appliquez un amincissement morphologique. Vous pouvez maintenant jeter les petits composants.

À ce stade, voici ce que nous avons avec l'image de droite ci-dessus (avant de dessiner le polygone bleu), celle de gauche n'est pas affichée car le seul composant restant est celui décrivant le papier:

entrez la description de l'image ici

Compte tenu des exemples, le seul problème qui reste est maintenant de faire la distinction entre les composants qui ressemblent à des rectangles et d'autres qui ne le font pas. Il s'agit de déterminer un rapport entre l'aire de la coque convexe contenant la forme et l'aire de sa boîte englobante; le rapport 0,7 fonctionne bien pour ces exemples. Il se peut que vous deviez également supprimer les composants qui se trouvent à l'intérieur du papier, mais pas dans ces exemples en utilisant cette méthode (néanmoins, cette étape devrait être très facile, surtout parce qu'elle peut être effectuée directement via OpenCV).

Pour référence, voici un exemple de code dans 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}]]

S'il y a des situations plus variées où le rectangle du papier n'est pas si bien défini, ou si l'approche le confond avec d'autres formes - ces situations peuvent se produire pour diverses raisons, mais une cause commune est une mauvaise acquisition d'image - alors essayez de combiner le pré -étapes de traitement avec le travail décrit dans l'article "Détection de rectangle basée sur une transformation de Hough fenêtrée".

mmgp
la source
1
y a-t-il une différence majeure dans la mise en œuvre de la vôtre et de celle ci-dessus (c'est-à-dire la réponse de @karlphilip)? Je suis désolé de ne pas en trouver dans un rapide coup d'oeil (sauf 3 channel-1 channel et Mathematica-OpenCV).
Abid Rahman K
2
@AbidRahmanK oui, il y en a .. Je n'utilise pas malin ni "plusieurs seuils" pour commencer. Il y a d'autres différences, mais d'après le ton de votre commentaire, il semble inutile de faire un effort sur mon propre commentaire.
mmgp
1
Je vois que vous trouvez tous les deux les bords et déterminez quel bord est carré. Pour trouver des arêtes, vous utilisez différentes méthodes. Il utilise rusé, vous utilisez une certaine dilatation-érosion. Et "plusieurs seuils", peut-être obtenu à partir d'échantillons OpenCV, utilisés pour trouver des carrés. L'essentiel est que je sentais que le concept global était le même. "Trouver les bords et détecter le carré". Et je l'ai demandé sincèrement, je ne sais pas quel "ton" vous avez tiré de mon commentaire, ou ce que vous (compris / mal compris). Donc, si vous pensez que cette question est sincère, j'aimerais connaître d'autres différences. Sinon, jetez mes commentaires.
Abid Rahman K
1
@AbidRahmanK bien sûr le concept est le même, la tâche est la même. Un filtrage médian est utilisé, un amincissement est utilisé, je me fiche d'où il a pris l'idée de plusieurs seuils - il n'est tout simplement pas utilisé ici (alors comment ne peut-il pas y avoir une différence?), L'image est redimensionnée ici, le les mesures des composants sont différentes. "Une certaine dilatation-érosion" ne donne pas d'arêtes binaires, otsu est utilisé pour cela. Il est inutile de mentionner cela, le code est là.
mmgp
1
K. Merci. J'ai la réponse. Concept is the same. (Je n'ai jamais utilisé Mathematica, donc je ne peux pas comprendre le code.) Et les différences que vous avez mentionnées sont des différences, mais pas une approche différente ou majeure. Si vous ne l'avez toujours pas Par exemple, vérifiez ceci:
Abid Rahman K
14

Eh bien, je suis en retard.


Dans votre image, le papier est white, tandis que l'arrière-plan est colored. Il est donc préférable de détecter le Saturation(饱和度)canal d'entrée du papier HSV color space. Reportez-vous d' abord au wiki HSL_and_HSV . Ensuite, je vais copier la plupart des idées de ma réponse dans ce segment Détecter la couleur dans une image .


Principales étapes:

  1. Lire dans BGR
  2. Convertir l'image de bgrla hsvplace
  3. Seuil du canal S
  4. Ensuite, trouvez le contour externe maximum (ou faites Canny, ou HoughLinescomme vous le souhaitez, je choisis findContours), environ pour obtenir les coins.

Voici mon résultat:

entrez la description de l'image ici


Le code 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)

Réponses connexes:

  1. Comment détecter les taches colorées dans une image en utilisant OpenCV?
  2. Détection des bords sur fond coloré à l'aide d'OpenCV
  3. OpenCV C ++ / Obj-C: Détection d'une feuille de papier / Détection de carré
  4. Comment utiliser `cv2.findContours` dans différentes versions d'OpenCV?
Kinght 金
la source
J'ai essayé d'utiliser l'espace S mais je n'ai toujours pas réussi. Voir ceci: stackoverflow.com/questions/50699893/…
hchouhan02
3

Ce dont vous avez besoin est un quadrilatère au lieu d'un rectangle pivoté. RotatedRectvous donnera des résultats incorrects. Vous aurez également besoin d'une projection en perspective.

Fondamentalement, ce qui doit être fait est:

  • Faites une boucle sur tous les segments de polygone et connectez ceux qui sont presque égaux.
  • Triez-les pour avoir les 4 segments de ligne les plus grands.
  • Intersectez ces lignes et vous avez les 4 coins les plus probables.
  • Transformez la matrice sur la perspective recueillie à partir des points d'angle et le rapport hauteur / largeur de l'objet connu.

J'ai implémenté une classe Quadrangle qui s'occupe de la conversion de contour en quadrangle et la transformera également dans la bonne perspective.

Voir une implémentation fonctionnelle ici: Java OpenCV redresser un contour

Tim
la source
1

Une fois que vous avez détecté le cadre de délimitation du document, vous pouvez effectuer une transformation de perspective en quatre points pour obtenir une vue plongeante de l’image de haut en bas. Cela corrigera le biais et isolera uniquement l'objet souhaité.


Image d'entrée:

Objet texte détecté

Vue de haut en bas du document texte

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()
Nathancy
la source
-1

Détecter une feuille de papier est une sorte de vieille école. Si vous souhaitez vous attaquer à la détection de biais, il est préférable de viser immédiatement la détection de ligne de texte. Avec cela, vous obtiendrez les extrêmes gauche, droite, haut et bas. Supprimez tous les graphiques de l'image si vous ne le souhaitez pas, puis effectuez des statistiques sur les segments de ligne de texte pour trouver la plage d'angle ou plutôt l'angle le plus fréquent. C'est ainsi que vous vous réduirez à un bon angle d'inclinaison. Maintenant, après cela, vous mettez ces paramètres l'angle d'inclinaison et les extrêmes pour redresser et couper l'image à ce qui est requis.

En ce qui concerne l'exigence d'image actuelle, il est préférable d'essayer CV_RETR_EXTERNAL au lieu de CV_RETR_LIST.

Une autre méthode de détection des bords consiste à former un classificateur de forêts aléatoires sur les bords du papier, puis à utiliser le classificateur pour obtenir la carte des bords. C'est de loin une méthode robuste mais qui nécessite de la formation et du temps.

Les forêts aléatoires fonctionneront avec des scénarios de différence de faible contraste, par exemple du papier blanc sur fond à peu près blanc.

Anubhav Rohatgi
la source