193 votes

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

J'ai implémenté avec succès l'exemple de détection de carrés d'OpenCV dans mon application de test, mais j'ai maintenant besoin de filtrer la sortie, parce qu'elle est assez désordonnée - ou mon code est-il mauvais ?

Je suis intéressé par les quatre points d'angle du papier pour la réduction de l'obliquité (comme que ) et traitement ultérieur

Entrée et sortie : Input & Output

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

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

1 votes

1 votes

Je pense que vous pouvez ajuster le titre de la question pour quelque chose comme Détection d'une feuille de papier si vous pensez que c'est plus approprié.

1 votes

@moosgummi Je cherche à avoir la même fonctionnalité que vous avez implémentée, c'est-à-dire "Détecter les coins de l'image ou du document capturé". Serais-je capable d'utiliser OpenCV dans mon application iPhone ? Veuillez me suggérer une meilleure façon de le faire.

172voto

karlphillip Points 46502

C'est un sujet récurrent sur Stackoverflow et comme je n'ai pas réussi à trouver une implémentation pertinente, j'ai décidé de relever le défi.

J'ai fait quelques modifications à la démo de carrés 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);
                    }
            }
        }
    }
}

Après l'exécution de cette procédure, la feuille de papier sera le plus grand carré de l'année. vector<vector<Point> > :

opencv paper sheet detection

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

0 votes

Pour une raison inconnue, je n'arrive plus à le faire fonctionner. Il jette toujours une exception lorsque mixChannels est appelé, ce qui est étrange car il fonctionnait il y a quelques jours. Some OpenCV Error: Assertion failed (j < nsrcs && src[j].depth() == depth) in mixChannels Connaissez-vous ce type d'erreur ? La profondeur des canaux est identique, ce qui n'a aucun sens. Je travaille avec OSX 10.7.2 et OpenCV 2.3.1.

4 votes

C'est pourquoi j'utilise le contrôle de la 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/reinstallez opencv.

0 votes

Ah, j'ai réussi à le faire fonctionner - il semble que parfois photoshop se trompe d'image Et j'utilise maintenant SVN pour gérer les versions. Merci !

42voto

mmgp Points 9153

À moins qu'il n'y ait une autre exigence non spécifiée, je convertirais simplement votre image couleur en niveaux de gris et je travaillerais uniquement avec cela (pas besoin de travailler sur les 3 canaux, le contraste présent est déjà trop élevé). De même, à 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ésoudre. Puis, finalement, votre problème est résolu avec un filtre médian, quelques outils morphologiques de base, et des statistiques (principalement pour le seuillage d'Otsu, qui est déjà fait pour vous).

Voici ce que j'ai obtenu avec votre image d'exemple et une autre image avec une feuille de papier que j'ai trouvée dans le coin :

enter image description hereenter image description here

Le filtre médian est utilisé pour éliminer les détails mineurs de l'image, maintenant en niveaux de gris. Il éliminera éventuellement les lignes fines à l'intérieur du papier blanchâtre, ce qui est une bonne chose car vous vous retrouverez alors avec de minuscules composants connectés qui sont faciles à éliminer. Après le filtre médian, appliquez un gradient morphologique (simplement dilation - erosion ) et binariser le résultat par Otsu. Le gradient morphologique est une bonne méthode pour garder des bords forts, il devrait être plus utilisé. Ensuite, puisque ce gradient va augmenter la largeur du contour, appliquez un amincissement morphologique. Vous pouvez maintenant éliminer les petites composantes.

A 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 montrée car le seul composant restant est celui qui décrit le papier :

enter image description here

Compte tenu de ces exemples, il ne reste plus qu'à distinguer les composants qui ressemblent à des rectangles de ceux qui n'en ont 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 ayez également besoin d'éliminer les composants qui se trouvent à l'intérieur du papier, mais pas dans ces exemples en utilisant cette méthode (néanmoins, faire cette étape devrait être très facile surtout parce qu'elle peut être faite par OpenCV directement).

Pour référence, voici un exemple de code en 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 de l'image -- alors essayez de combiner les étapes de prétraitement avec le travail décrit dans l'article "Rectangle Detection based on a Windowed Hough Transform".

1 votes

Y a-t-il une différence majeure dans l'implémentation de la vôtre et celle de la réponse ci-dessus (c'est-à-dire la réponse de @karlphilip) ? Je suis désolé, je n'ai pas pu en trouver en cherchant rapidement (sauf 3 canaux-1 canal et Mathematica-OpenCV).

2 votes

@AbidRahmanK oui, il y a Je n'utilise ni canny ni "plusieurs seuils" pour commencer. Il y a d'autres différences, mais au vu du ton de votre commentaire, il semble inutile de faire des efforts sur mon propre commentaire.

1 votes

Je vous vois tous les deux commencer par trouver les bords, et déterminer quel bord est carré. Pour trouver les bords, vous utilisez des méthodes différentes. Il utilise canny, vous utilisez une dilatation-érosion. Et "plusieurs seuils", qu'il a peut-être obtenus à partir d'échantillons OpenCV, utilisés pour trouver les carrés. L'important, c'est que le concept général est le même. "Trouver les bords et détecter les carrés". Et je l'ai demandé sincèrement, je ne sais pas quel "ton" vous avez pris dans mon commentaire, ou ce que vous avez (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.

20voto

Silencer Points 6351

Eh bien, je suis en retard.


Dans votre image, le papier est white tandis que l'arrière-plan est colored . Donc, il est préférable de détecter que le papier est Saturation(饱和度) le canal en HSV color space . Se référer au wiki HSL_et_HSV d'abord. Ensuite, je copierai la plupart des idées de ma réponse dans ce Détecter un segment coloré dans une image .


Les étapes principales :

  1. Lire en BGR
  2. Convertir l'image de bgr a hsv espace
  3. Seuil du canal S
  4. Puis trouver le contour extérieur maximal (ou faire Canny ou HoughLines comme vous le souhaitez, je choisis findContours ), environ pour obtenir les coins.

Voici mon résultat :

enter image description here


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 des taches colorées dans une image en utilisant OpenCV ?
  2. Détection des bords sur un fond coloré à l'aide d'OpenCV
  3. OpenCV C++/Obj-C : Détection d'une feuille de papier / Détection de carrés
  4. Comment utiliser `cv2.findContours` dans différentes versions d'OpenCV ?

3voto

Tim Points 773

Ce dont vous avez besoin, c'est d'un quadrangle au lieu d'un rectangle pivoté. RotatedRect vous donnera des résultats incorrects. Vous aurez également besoin d'une projection en perspective.

En gros, ce qu'il faut faire, c'est :

  • Bouclez sur tous les segments du polygone et connectez ceux qui sont presque égaux.
  • Triez-les de manière à obtenir les 4 segments de ligne les plus grands.
  • Intersecter ces lignes et vous avez les 4 points d'angle les plus probables.
  • Transformer la matrice sur la perspective recueillie à partir des points d'angle et du rapport d'aspect de l'objet connu.

J'ai implémenté une classe Quadrangle qui s'occupe de la conversion des contours en quadrilatères et les transformera également sur la bonne perspective.

Voir une mise en œuvre fonctionnelle ici : Java OpenCV désalignement d'un contour

1voto

nathancy Points 6407

Une fois que vous avez détecté le rectangle de délimitation du document, vous pouvez effectuer une analyse de l'image. transformation de la perspective à quatre points pour obtenir une vue d'ensemble de l'image. Cela permettra de corriger l'inclinaison et d'isoler uniquement l'objet souhaité.


Image d'entrée :

Objet texte détecté

Vue de haut en bas d'un 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()

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X