OpenCV C ++ / Obj-C: rilevamento di un foglio di carta / rilevamento quadrato


178

Ho implementato con successo l'esempio di rilevamento quadrato OpenCV nella mia applicazione di test, ma ora ho bisogno di filtrare l'output, perché è piuttosto disordinato - o il mio codice è sbagliato?

Sono interessato ai quattro punti d'angolo del documento per la riduzione dell'inclinazione (come quella ) e l'ulteriore elaborazione ...

Input Output: Input Output

Immagine originale:

clic

Codice:

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

MODIFICA 17/08/2012:

Per disegnare i quadrati rilevati sull'immagine utilizzare questo codice:

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
Penso che puoi modificare il titolo della domanda per qualcosa come Rilevare un foglio di carta , se pensi che sia più appropriato.
karlphillip,

1
@moosgummi Sto cercando di avere le stesse funzionalità che hai implementato, ad esempio "Rileva gli angoli dell'immagine / documento catturati". Come hai ottenuto questo? Sarei in grado di utilizzare OpenCV all'interno della mia applicazione per iPhone? Per favore, suggeriscimi un modo migliore per avere questo ...
Ajay Sharma,

1
Hai mai fatto qualcosa con OpenCV? Qualche applicazione?
karlphillip,

6
Vale la pena notare che la bandiera CV_RETR_EXTERNAL può essere utilizzata quando si trovano i punti di riferimento per rifiutare tutti i contorni all'interno di una forma chiusa.
Mehfoos Yacoob,

Risposte:


162

Questo è un argomento ricorrente in StackOverflow e poiché non sono riuscito a trovare un'implementazione pertinente, ho deciso di accettare la sfida.

Ho apportato alcune modifiche alla demo dei quadrati presente in OpenCV e il codice C ++ risultante di seguito è in grado di rilevare un foglio di carta nell'immagine:

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

Dopo aver eseguito questa procedura, il foglio di carta sarà il quadrato più grande in vector<vector<Point> >:

rilevamento di fogli di carta opencv

Ti lascio scrivere la funzione per trovare il quadrato più grande. ;)


4
Ecco perché uso il controllo del codice sorgente. La più piccola modifica accidentale al codice può essere facilmente scoperta. Se non hai cambiato nulla, prova a provare con altre immagini e infine ricompila / reinstalla opencv.
karlphillip,

2
OpenCV è praticamente lo stesso per tutte le piattaforme (Win / Linux / Mac / iPhone / ...). La differenza è che alcuni non supportano il modulo GPU di OpenCV. Hai già creato OpenCV per iOS ? Sei riuscito a provarlo? Penso che queste siano le domande a cui devi rispondere prima di provare qualcosa di più avanzato. Piccoli passi!
karlphillip,

1
@karlphillip Ho testato questo codice e sono stato in grado di rilevare chiaramente la carta, ma ci vuole molto tempo. Il codice è davvero pesante? c'è un'app chiamata SayText in cui questo rilevamento avviene in tempo reale da un flusso video. Questo codice sarebbe impraticabile per il tempo reale, vero?
alandalusi,

1
Probabilmente. Questa è una risposta accademica, non molto pratica per l'industria. Ci sono tutti i tipi di ottimizzazioni che puoi provare, a cominciare dalla definizione del contatore in for (int c = 0; c < 3; c++)cui si trova , che è responsabile dell'iterazione su ogni canale dell'immagine. Ad esempio, puoi impostarlo per iterare su un solo canale :) Non dimenticare di votare.
karlphillip,

3
@SilentPro angle()è una funzione di supporto . Come indicato nella risposta, questo codice si basa su samples / cpp / squares.cpp presenti in OpenCV.
karlphillip,

40

A meno che non ci siano altri requisiti non specificati, vorrei semplicemente convertire l'immagine a colori in scala di grigi e lavorare solo con quello (non è necessario lavorare sui 3 canali, il contrasto presente è già troppo alto). Inoltre, a meno che non ci siano problemi specifici relativi al ridimensionamento, lavorerei con una versione ridotta delle immagini, poiché sono relativamente grandi e le dimensioni non aggiungono nulla al problema da risolvere. Quindi, infine, il tuo problema viene risolto con un filtro mediano, alcuni strumenti morfologici di base e statistiche (principalmente per il limite Otsu, che è già stato fatto per te).

Ecco cosa ottengo con la tua immagine di esempio e qualche altra immagine con un foglio di carta che ho trovato in giro:

inserisci qui la descrizione dell'immagine inserisci qui la descrizione dell'immagine

Il filtro mediano viene utilizzato per rimuovere dettagli minori dall'immagine, ora in scala di grigi. Rimuoverà eventualmente le sottili linee all'interno della carta biancastra, il che è positivo perché poi finirai con piccoli componenti collegati che sono facili da scartare. Dopo la mediana, applica un gradiente morfologico (semplicemente dilation- erosion) e binarizza il risultato di Otsu. Il gradiente morfologico è un buon metodo per mantenere bordi forti, dovrebbe essere usato di più. Quindi, poiché questo gradiente aumenterà la larghezza del contorno, applicare un assottigliamento morfologico. Ora puoi scartare piccoli componenti.

A questo punto, ecco quello che abbiamo con l'immagine a destra sopra (prima di disegnare il poligono blu), quello a sinistra non viene mostrato perché l'unico componente rimanente è quello che descrive il foglio:

inserisci qui la descrizione dell'immagine

Dati gli esempi, ora l'unico problema rimasto è la distinzione tra componenti che sembrano rettangoli e altri che non lo fanno. Si tratta di determinare un rapporto tra l'area dello scafo convesso contenente la forma e l'area del suo riquadro di delimitazione; il rapporto 0.7 funziona bene per questi esempi. Potrebbe essere necessario scartare anche i componenti che si trovano all'interno della carta, ma non in questi esempi utilizzando questo metodo (tuttavia, questo passaggio dovrebbe essere molto semplice soprattutto perché può essere eseguito direttamente tramite OpenCV).

Per riferimento, ecco un codice di esempio 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}]]

Se ci sono situazioni più varie in cui il rettangolo della carta non è così ben definito, o l'approccio lo confonde con altre forme - queste situazioni potrebbero verificarsi a causa di vari motivi, ma una causa comune è la cattiva acquisizione delle immagini - quindi provare a combinare il pre -processi di elaborazione con il lavoro descritto nel documento "Rilevamento rettangolo basato su una trasformazione di Hough con finestra".


1
c'è qualche grande differenza nell'implementazione della tua e di quella sopra (cioè la risposta di @karlphilip)? Mi dispiace di non averne trovato uno in uno sguardo veloce (tranne 3 canali-1 canale e Mathematica-OpenCV).
Abid Rahman K

2
@AbidRahmanK sì, ci sono .. Non uso canny né "diverse soglie" per cominciare. Ci sono altre differenze, ma dal tono del tuo commento sembra inutile fare uno sforzo sul mio commento.
mmgp,

1
Vedo che entrambi trovano prima i bordi e determinano quale bordo è quadrato. Per trovare i bordi, voi persone usate metodi diversi. Usa astuto, tu usi un po 'di dilatazione-erosione. E "diverse soglie", potrebbe essere ottenuto da campioni OpenCV, usati per trovare quadrati. La cosa principale è che ho pensato che il concetto generale fosse lo stesso. "Trova i bordi e rileva il quadrato". E l'ho chiesto sinceramente, non so quale "tono" hai ricevuto dal mio commento o cosa tu (capito / frainteso). Quindi, se ritieni che questa domanda sia sincera, vorrei conoscere altre differenze. Altrimenti scarta i miei commenti.
Abid Rahman K

1
@AbidRahmanK ovviamente il concetto è lo stesso, il compito è lo stesso. Viene utilizzato il filtro mediano, viene utilizzato il diradamento, non mi interessa da dove ha preso l'idea di diverse soglie - non viene usato qui (quindi come può non essere una differenza?), L'immagine viene ridimensionata qui, il le misure dei componenti sono diverse. "Qualche dilatazione-erosione" non dà bordi binari, per questo viene usato otsu. È inutile menzionarlo, il codice è lì.
mmgp,

1
K. Grazie. Ho la risposta Concept is the same. (Non ho mai usato Mathematica, quindi non riesco a capire il codice.) E le differenze che hai citato sono differenze, ma non un approccio diverso o quelli principali. Se non l'hai ancora fatto, ad esempio, controlla questo:
Abid Rahman K

14

Bene, sono in ritardo.


Nella tua immagine, la carta è white, mentre lo sfondo è colored. Quindi, è meglio rilevare la carta Saturation(饱和度)nel canale HSV color space. Prendi prima riferimento al wiki HSL_and_HSV . Quindi copierò la maggior parte dell'idea dalla mia risposta in questo Rileva segmento colorato in un'immagine .


Passaggi principali:

  1. Leggi in BGR
  2. Converti l'immagine da bgrnello hsvspazio
  3. Soglia del canale S.
  4. Quindi trova il contorno esterno massimo (o fai Canny, o HoughLinescome preferisci, ho scelto findContours), circa per ottenere gli angoli.

Questo è il mio risultato:

inserisci qui la descrizione dell'immagine


Il codice 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)

Risposte correlate:

  1. Come rilevare patch colorate in un'immagine usando OpenCV?
  2. Rilevamento dei bordi su sfondo colorato con OpenCV
  3. OpenCV C ++ / Obj-C: rilevamento di un foglio di carta / rilevamento quadrato
  4. Come usare `cv2.findContours` in diverse versioni di OpenCV?

Ho provato a usare lo spazio S, ma non riesco ancora ad avere successo. Vedere questo: stackoverflow.com/questions/50699893/...
hchouhan02

3

Ciò di cui hai bisogno è un quadrangolo invece di un rettangolo ruotato. RotatedRectti darà risultati errati. Inoltre avrai bisogno di una proiezione prospettica.

Fondamentalmente ciò che deve essere fatto è:

  • Passa attraverso tutti i segmenti poligonali e collega quelli che sono quasi equel.
  • Ordinali in modo da avere i 4 segmenti di linea più grandi.
  • Interseca quelle linee e hai i 4 punti d'angolo più probabili.
  • Trasforma la matrice sulla prospettiva raccolta dai punti d'angolo e le proporzioni dell'oggetto noto.

Ho implementato una classe Quadrangleche si occupa del contorno per quadrangolare la conversione e la trasformerà anche nella giusta prospettiva.

Guarda un'implementazione funzionante qui: Java OpenCV che disegna un contorno


1

Dopo aver rilevato il rettangolo di selezione del documento, è possibile eseguire una trasformazione prospettica in quattro punti per ottenere una vista dall'alto dall'alto verso il basso dell'immagine. Ciò risolverà l'inclinazione e isolerà solo l'oggetto desiderato.


Immagine di input:

Oggetto di testo rilevato

Vista dall'alto verso il basso del documento di testo

Codice

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

Rilevare un foglio di carta è una specie di vecchia scuola. Se si desidera affrontare il rilevamento dell'inclinazione, è meglio mirare subito al rilevamento della riga di testo. Con questo otterrai gli estremi a sinistra, a destra, in alto e in basso. Eliminare qualsiasi elemento grafico nell'immagine se non si desidera e quindi eseguire alcune statistiche sui segmenti della riga di testo per trovare l'intervallo di angoli più frequente o piuttosto l'angolo. Questo è il modo in cui ti restringerai ad un buon angolo di inclinazione. Ora, dopo aver inserito questi parametri, l'angolo di inclinazione e gli estremi per inclinare e tagliare l'immagine a ciò che è richiesto.

Per quanto riguarda l'attuale requisito dell'immagine, è meglio provare CV_RETR_EXTERNAL anziché CV_RETR_LIST.

Un altro metodo per rilevare i bordi è quello di addestrare un classificatore di foreste casuali sui bordi della carta e quindi utilizzare il classificatore per ottenere la mappa dei bordi. Questo è di gran lunga un metodo solido ma richiede addestramento e tempo.

Le foreste casuali funzioneranno con scenari a bassa differenza di contrasto, ad esempio carta bianca su sfondo approssimativamente bianco.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.