web-dev-qa-db-fra.com

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

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

Je m'intéresse aux quatre points essentiels du document sur la réduction du biais (comme que ) et le traitement ultérieur…

Entrée et sortie: Input & Output

Image originale:

clic

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

C’est un sujet récurrent dans Stackoverflow et comme je n’étais pas capable de trouver une implémentation pertinente, j’ai décidé d’accepter le défi.

J'ai apporté quelques modifications à la démonstration 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);
                    }
            }
        }
    }
}

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

opencv paper sheet detection

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

153
karlphillip

À moins d'une autre exigence non spécifiée, je convertirais simplement votre image couleur en niveaux de gris et ne travaillerais qu'avec cela (pas besoin de travailler sur les 3 canaux, le contraste est déjà trop élevé). De plus, sauf en cas de problème de redimensionnement spécifique, je travaillerais avec une version réduite de vos images, car elles sont relativement volumineuses et que leur taille n’ajoute rien au problème à résoudre. Enfin, votre problème est résolu avec un filtre médian, des outils morphologiques de base et des statistiques (principalement pour le seuillage d’Otsu, ce 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:

enter image description hereenter image description here

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

À ce stade, voici ce que nous avons avec la bonne image 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 des exemples, il ne reste plus maintenant qu'à distinguer entre des composants qui ressemblent à des rectangles et d'autres qui ne le sont 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 de sélection; le ratio 0,7 fonctionne bien pour ces exemples. Il se peut que vous deviez également supprimer des composants se trouvant dans le document, mais pas dans ces exemples en utilisant cette méthode (néanmoins, cette étape devrait être très facile, notamment parce que cela peut être fait 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 existe des situations plus variées où le rectangle du papier n'est pas aussi bien défini ou si l'approche le confond avec d'autres formes - ces situations peuvent se produire pour diverses raisons, mais une cause fréquente est la mauvaise acquisition d'image - essayez de combiner les -étapes de traitement avec le travail décrit dans l'article "Détection de rectangle basé sur une transformation de houghows".

38
mmgp

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 que le papier est le canal Saturation(饱和度) dans HSV color space. Prenez référence à wiki HSL_and_HSV en premier. Ensuite, je recopierai l'essentiel de ma réponse dans ce message Détecter un segment coloré dans une image .


Principales étapes:

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

Ceci est 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 associées:

  1. Comment détecter des taches colorées dans une image en utilisant OpenCV?
  2. Détection des contours sur un fond coloré avec 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?
11
Kinght 金

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

Fondamentalement, ce qui doit être fait est:

  • Parcourez tous les segments de polygones et connectez ceux qui sont presque équivalents.
  • Triez-les pour obtenir les 4 plus grands segments de ligne.
  • Intersectez ces lignes et vous obtenez les 4 angles les plus probables.
  • Transformez la matrice en perspective regroupée à partir des points d’angle et du format de l’objet connu.

J'ai implémenté une classe Quadrangle qui prend en charge la conversion contour en quadrilatère et la transforme également sur la bonne perspective.

Voir une implémentation de travail ici: Java OpenCV redressant un contour

2
Tim