2015-08-12 39 views
9

Ho giocato con OpenCV (cv2) e ho rilevato linee e forme. Dire che mia figlia ha un disegno, in questo modo:Rilevamento di linee e forme in OpenCV con Python

enter image description here

Sto cercando di scrivere uno script Python che analizzare il disegno e convertirlo in dure linee/forme, qualcosa di simile:

enter image description here

Detto questo, ho installato opencv e ho provato a giocarci, ma non ho avuto fortuna oltre a poter disegnare una singola linea verticale attraverso l'immagine. Di seguito è riportato il mio codice finora, qualsiasi suggerimento o suggerimento su come dovrei andare a fare questo con opencv sarebbe molto apprezzato.

import cv2 
import numpy as np 

class File(object): 
    def __init__(self, filename): 
     self.filename = filename 

    def open(self, filename=None, mode='r'): 
     if filename is None: 
      filename = self.filename 

     return cv2.imread(filename), open(filename, mode) 

    def save(self, image=None, filename_override=None): 
     filename = "output/" + self.filename.split('/')[-1] 

     if filename_override: 
      filename = "output/" + filename_override 

     return cv2.imwrite(filename, image) 

class Image(object): 
    def __init__(self, image): 
     self.image = image 

    def grayscale(self): 
     return cv2.cvtColor(self.image, cv2.COLOR_BGR2GRAY) 

    def edges(self): 
     return cv2.Canny(self.image, 0, 255) 

    def lines(self): 
     lines = cv2.HoughLinesP(self.image, 1, np.pi/2, 6, None, 50, 10) 
     for line in lines[0]: 
      pt1 = (line[0],line[1]) 
      pt2 = (line[2],line[3]) 
      cv2.line(self.image, pt1, pt2, (0,0,255), 2) 

if __name__ == '__main__': 
    File = File('images/a.png') 
    Image = Image(File.open()[0]) 
    Image.image = Image.grayscale() 
    Image.lines() 
    File.save(Image.image) 

Purtroppo, per un semplice disegno quadrato tutto ottengo indietro è:

enter image description here

dove la linea verticale nella casella è l'uscita dal codice.

+0

OpenCV C++ sarà un bene per voi? – Miki

+0

@Miki se il mio C++ non era così arrugginito, lo farei sicuramente.Alla fine, mi piacerebbe migrare questa app in C o C++ se il concetto funziona in Python. – jsanc623

+0

bene, posterò la mia risposta C++ non appena avrò un po 'di tempo libero. Poi ti lascerò decidere se è abbastanza buono per te. A proposito, puoi dare un'occhiata [qui] (http://www.emgu.com/wiki/index.php/Shape_ (Triangle, _Rectangle, _Circle, _Line) _Detection_in_CSharp). È C#, ma è abbastanza facile da seguire. – Miki

risposta

12

Ecco il mio tentativo. È in C++, ma può essere facilmente portato su python poiché la maggior parte sono funzioni OpenCV.

Un breve schema del metodo, i commenti nel codice dovrebbero aiutare anche.

  1. Caricare l'immagine
  2. Converti in scala di grigi
  3. Binaryze l'immagine (soglia)
  4. diradamento, per avere contorni sottili e aiutare findContours
  5. Ottenere contorni
  6. Per ogni profilo, ottenere scafo convesso (per gestire contorni aperti), e classificare secondo circolarità. Gestisci ogni forma in modo diverso.

    • Circle: trovare il cerchio encolsing minima, o il migliore dell'ellisse raccordo
    • Recrangle: trovare la casella boundinx, o la casella minima orientato delimitazione.
    • Triangolo: cerca l'intersezione del cerchio di chiusura minimo con la forma originale, poiché si intersecheranno nei tre vertici del triangolo.

NOTE:

  • avevo bisogno di modificare l'immagine originale a 3 canali RGB da un PNG con trasparenza.
  • Il diradamento codice è da here. C'è anche la versione Python.
  • La circolarità è definita come: A misura la vicinanza di un cerchio alla forma. Per esempio. un esagono regolare ha una circolarità maggiore di un quadrato. È definito come (\ frac {4 * \ pi * Area} {perimetro * perimetro}). Ciò significa che un cerchio ha una circolarità di 1, la circolarità di un quadrato è 0,785 e così via.
  • A causa dei contorni, potrebbe esserci un rilevamento multiplo per ogni forma. Questi possono essere filtrati secondo, ad esempio, l'intersezione sulla condizione dell'unione. Per il momento non ho inserito questa parte nel codice, poiché richiede una logica aggiuntiva non strettamente correlata al compito principale di trovare le forme.

UPDATE - appena notato che in OpenCV 3.0.0 v'è la funzione minEnclosingTriangle. Potrebbe essere utile usare al posto della mia procedura per trovare i vertici del triangolo. Tuttavia, poiché l'inserimento di questa funzione nel codice sarebbe banale, lascerò la mia procedura nel codice nel caso in cui uno non abbia OpenCV 3.0.0.

Il codice:

#include <opencv2\opencv.hpp> 
#include <vector> 
#include <iostream> 

using namespace std; 
using namespace cv; 

///////////////////////////////////////////////////////////////////////////////////////////// 
// Thinning algorithm from here: 
// https://github.com/bsdnoobz/zhang-suen-thinning 
///////////////////////////////////////////////////////////////////////////////////////////// 

void thinningIteration(cv::Mat& img, int iter) 
{ 
    CV_Assert(img.channels() == 1); 
    CV_Assert(img.depth() != sizeof(uchar)); 
    CV_Assert(img.rows > 3 && img.cols > 3); 

    cv::Mat marker = cv::Mat::zeros(img.size(), CV_8UC1); 

    int nRows = img.rows; 
    int nCols = img.cols; 

    if (img.isContinuous()) { 
     nCols *= nRows; 
     nRows = 1; 
    } 

    int x, y; 
    uchar *pAbove; 
    uchar *pCurr; 
    uchar *pBelow; 
    uchar *nw, *no, *ne; // north (pAbove) 
    uchar *we, *me, *ea; 
    uchar *sw, *so, *se; // south (pBelow) 

    uchar *pDst; 

    // initialize row pointers 
    pAbove = NULL; 
    pCurr = img.ptr<uchar>(0); 
    pBelow = img.ptr<uchar>(1); 

    for (y = 1; y < img.rows - 1; ++y) { 
     // shift the rows up by one 
     pAbove = pCurr; 
     pCurr = pBelow; 
     pBelow = img.ptr<uchar>(y + 1); 

     pDst = marker.ptr<uchar>(y); 

     // initialize col pointers 
     no = &(pAbove[0]); 
     ne = &(pAbove[1]); 
     me = &(pCurr[0]); 
     ea = &(pCurr[1]); 
     so = &(pBelow[0]); 
     se = &(pBelow[1]); 

     for (x = 1; x < img.cols - 1; ++x) { 
      // shift col pointers left by one (scan left to right) 
      nw = no; 
      no = ne; 
      ne = &(pAbove[x + 1]); 
      we = me; 
      me = ea; 
      ea = &(pCurr[x + 1]); 
      sw = so; 
      so = se; 
      se = &(pBelow[x + 1]); 

      int A = (*no == 0 && *ne == 1) + (*ne == 0 && *ea == 1) + 
       (*ea == 0 && *se == 1) + (*se == 0 && *so == 1) + 
       (*so == 0 && *sw == 1) + (*sw == 0 && *we == 1) + 
       (*we == 0 && *nw == 1) + (*nw == 0 && *no == 1); 
      int B = *no + *ne + *ea + *se + *so + *sw + *we + *nw; 
      int m1 = iter == 0 ? (*no * *ea * *so) : (*no * *ea * *we); 
      int m2 = iter == 0 ? (*ea * *so * *we) : (*no * *so * *we); 

      if (A == 1 && (B >= 2 && B <= 6) && m1 == 0 && m2 == 0) 
       pDst[x] = 1; 
     } 
    } 

    img &= ~marker; 
} 

void thinning(const cv::Mat& src, cv::Mat& dst) 
{ 
    dst = src.clone(); 
    dst /= 255;   // convert to binary image 

    cv::Mat prev = cv::Mat::zeros(dst.size(), CV_8UC1); 
    cv::Mat diff; 

    do { 
     thinningIteration(dst, 0); 
     thinningIteration(dst, 1); 
     cv::absdiff(dst, prev, diff); 
     dst.copyTo(prev); 
    } while (cv::countNonZero(diff) > 0); 

    dst *= 255; 
} 


int main() 
{ 
    RNG rng(123); 

    // Read image 
    Mat3b src = imread("path_to_image"); 

    // Convert to grayscale 
    Mat1b gray; 
    cvtColor(src, gray, COLOR_BGR2GRAY); 

    // Binarize 
    Mat1b bin; 
    threshold(gray, bin, 127, 255, THRESH_BINARY_INV); 

    // Perform thinning 
    thinning(bin, bin); 

    // Create result image 
    Mat3b res = src.clone(); 

    // Find contours 
    vector<vector<Point>> contours; 
    findContours(bin.clone(), contours, CV_RETR_LIST, CV_CHAIN_APPROX_NONE); 

    // For each contour 
    for (vector<Point>& contour : contours) 
    { 
     // Compute convex hull 
     vector<Point> hull; 
     convexHull(contour, hull); 

     // Compute circularity, used for shape classification 
     double area = contourArea(hull); 
     double perimeter = arcLength(hull, true); 
     double circularity = (4 * CV_PI * area)/(perimeter * perimeter); 

     // Shape classification 

     if (circularity > 0.9) 
     { 
      // CIRCLE 

      //{ 
      // // Fit an ellipse ... 
      // RotatedRect rect = fitEllipse(contour); 
      // Scalar color = Scalar(rng.uniform(0, 255), rng.uniform(0, 255), rng.uniform(0, 255)); 
      // ellipse(res, rect, color, 5); 
      //} 
      { 
       // ... or find min enclosing circle 
       Point2f center; 
       float radius; 
       minEnclosingCircle(contour, center, radius); 
       Scalar color = Scalar(rng.uniform(0, 255), rng.uniform(0, 255), rng.uniform(0, 255)); 
       circle(res, center, radius, color, 5); 
      } 
     } 
     else if (circularity > 0.75) 
     { 
      // RECTANGLE 

      //{ 
      // // Minimum oriented bounding box ... 
      // RotatedRect rect = minAreaRect(contour); 
      // Point2f pts[4]; 
      // rect.points(pts); 

      // Scalar color = Scalar(rng.uniform(0, 255), rng.uniform(0, 255), rng.uniform(0, 255)); 
      // for (int i = 0; i < 4; ++i) 
      // { 
      //  line(res, pts[i], pts[(i + 1) % 4], color, 5); 
      // } 
      //} 
      { 
       // ... or bounding box 
       Rect box = boundingRect(contour); 
       Scalar color = Scalar(rng.uniform(0, 255), rng.uniform(0, 255), rng.uniform(0, 255)); 
       rectangle(res, box, color, 5); 
      } 
     } 
     else if (circularity > 0.7) 
     { 
      // TRIANGLE 

      // Select the portion of the image containing only the wanted contour 
      Rect roi = boundingRect(contour); 
      Mat1b maskRoi(bin.rows, bin.cols, uchar(0)); 
      rectangle(maskRoi, roi, Scalar(255), CV_FILLED); 
      Mat1b triangle(roi.height, roi.height, uchar(0)); 
      bin.copyTo(triangle, maskRoi); 

      // Find min encolsing circle on the contour 
      Point2f center; 
      float radius; 
      minEnclosingCircle(contour, center, radius); 

      // decrease the size of the enclosing circle until it intersects the contour 
      // in at least 3 different points (i.e. the 3 vertices) 
      vector<vector<Point>> vertices; 
      do 
      { 
       vertices.clear(); 
       radius--; 

       Mat1b maskCirc(bin.rows, bin.cols, uchar(0)); 
       circle(maskCirc, center, radius, Scalar(255), 5); 

       maskCirc &= triangle; 
       findContours(maskCirc.clone(), vertices, CV_RETR_LIST, CV_CHAIN_APPROX_NONE); 

      } while (vertices.size() < 3); 

      // Just get the first point in each vertex blob. 
      // You could get the centroid for a little better accuracy 

      Scalar color = Scalar(rng.uniform(0, 255), rng.uniform(0, 255), rng.uniform(0, 255)); 
      line(res, vertices[0][0], vertices[1][0], color, 5); 
      line(res, vertices[1][0], vertices[2][0], color, 5); 
      line(res, vertices[2][0], vertices[0][0], color, 5); 

     } 
     else 
     { 
      cout << "Some other shape..." << endl; 
     } 

    } 

    return 0; 
} 

I risultati (minEnclosingCircle e boundingRect): enter image description here

I risultati (fitEllipse e minAreaRect): enter image description here

+0

Questo è davvero perfetto. Grazie Miki! Trascriverò il codice in Python utilizzando l'API come riferimento e utilizzerò il tuo codice quando eseguo la migrazione in C++ al termine del POC. Molte molte molte grazie! – jsanc623

+1

@ jsanc623 contento che abbia aiutato! – Miki

+0

Risposta stupenda come sempre @Miki :) le tue implementazioni sono di prima classe :) – XoXo

1

Si potrebbe verificare un paio di risorse.

In primo luogo, si potrebbe prendere in considerazione la possibilità di porre domande a answers.opencv.org. Probabilmente c'è una maggiore concentrazione di specialisti di ottica.

In secondo luogo, il libro Practical OpenCV di Samarth Brahmbhatt è disponibile come pdf gratuito e si trova facilmente su google. Contiene molti esempi relativi a ciò che stai cercando.

Ad esempio, è possibile separare contorni diversi (non sovrapposti), come mostrato nell'esempio 6.1 a pagina 68. Ha un semplice programma per trovare cerchi e linee nell'esempio 6.4 a pagina 78. È anche possibile trovare un Ellisse-finder basato su RANSAC (molto più complicato, ma sarebbe molto utile qui) nell'esempio 6.5 a pagina 82.

Il libro è in C++, ma immagino che sarà molto rilevante, solo tu avrai bisogno di un'API riferimento per tradurlo in python.

Personalmente, per il tuo progetto, vorrei analizzare un contorno alla volta, iniziando dal suo cercatore di ellisse e dove non è possibile trovare un'ellisse adatta, potresti usare una trasformazione di Hough della soglia regolabile e troncare le linee risultanti alle loro intersezioni, e bam! Hai poligoni.

+0

Grazie a @rexroni per il commento, esaminerò il libro di Brahmbhatt e vedrò se sarà utile! – jsanc623