Walter
OpenCV e il riconoscimento dei contorni di un'immagine
Tutorials -
Giovedì 09 Settembre 2010 08:39
Scritto da Walter

OpenCV è una libreria open source per la Computer Vision in C/C++.
Uno degli algoritmi più utili nella computer vision applicata alla robotica è quello del riconoscimento dei contorni. Il riconoscimento di contorni (Contour Detection) è alla base di molti algoritmi più evoluti utilizzati per il riconoscimento di oggetti in un'immagine.

OpenCV permette di riconoscere e disegnare facilmente i contorni di un'immagine, generando come risultato una struttura facilmente visitabile.
Purtroppo la documentazione ufficiale non è molto chiara e il risultato finale non è di facile interpretazione.
Partiamo dall'inizio: le funzioni utili al nostro scopo.

  • cvCanny: evidenzia i contorni dell'immagine utilizzando l'algoritmo di Canny

  • cvDilate: applica l'operatore morfologico di 'dilate' all'immagine.

  • cvFindContours: analizza i contorni dell'immagine e li immagazzina in una struttura dati

  • cvDrawContours: disegna i contorni riconosciuti (utile in fase di debug)

Analizziamo le funzioni nelle loro particolarità:

cvCanny( const CvArr* image, CvArr* edges,
double threshold1, double threshold2, int aperture_size=3 )
:
image: l'immagine da elaborare. Deve essere obbligatoriamente un'immagine a UN CANALE, quindi un'immagine a toni di grigio. Esistono versioni modificate dell'algoritmo di Canny che agiscono direttamente su immagini a colori, io stesso ne ho realizzando una versione modificando il codice della cvCanny.
edges: l'immagine che conterrà il risultato. Anche questa immagine dovrà essere ad un canale e delle stesse dimesioni dell'immagine originale.
threshold1/threshold1: il più piccolo dei due valori è utilizzato per la 'fusione dei bordi', il valore più grande per trovare i segmenti iniziali dei vertici più 'forti'.
Se un pixel ha gradiente maggiore del threshold più grande è subito accettato, se il gradiente è minore del più basso è scartato. Se il gradiente è compreso tra i due è accettato solo se è vicino ad un pixel con gradiente più alto del threshold maggiore. Canny raccomanda un rapporto tra i due threshold tra 2:1 e 3:1.
aperture_size: dimensione del filtro di Sobel (vd cvSobel). Il filtro di Sobel esegue la derivata direzionale dell'immagine per evidenziare le variazioni di tonalità nell'avvicinarsi ad un contorno.

cvDilate( const CvArr* src, CvArr* dst, IplConvKernel* element=NULL, int iterations=1 ):
src: l'immagine da elaborare.
dst: l'immagine destinazione del risultato.
element: Kernel di convoluzione realizzato dall'utente.
iterations: numero di volte in cui il filtro è applicato all'immagine.
L'operazione di dilate permette di migliorare il risultato della cvCanny unendo contorni vicini che per approssimazione risulterebbero erroneamente 'aperti'.

int cvFindContours( CvArr* image, CvMemStorage* storage, CvSeq** first_contour, int header_size=sizeof(CvContour), int mode=CV_RETR_LIST, int method=CV_CHAIN_APPROX_SIMPLE, CvPoint offset=cvPoint(0,0) ):
image: l'immagine da elaborare. E' bene che sia il risultato di un 'pre-filtraggio' che evidenzi i contorni da ricercare. Nel nostro caso il prefiltraggio è realizzato con filtro di Canny e successivo dilate (vd sopra).
storage: spazio di memoria utilizzato dalla funzione per l'elaborazione dei dati e 'contenitore' per il risultato finale.
first_contour: puntatore al primo contorno trovato. In seguito sarà descritta la struttura dati che conterrà il risultato delle elaborazioni.
header_size: dimensione in BYTE dell'header che descrive la struttura di contenimento del risultato.
mode: modalità di raccoglimento dei dati. Sono disponibili diverse modalità: CV_RETR_EXTERNAL (solo contorni esterni), CV_RETR_LIST (tutti i contorni immagazzinati in una lista 'mono livello'), CV_RETR_CCOMP (struttura su due livelli: bordo esterno, bordo dei 'buchi'), CV_RETR_TREE (tutti i contorni organizzati in una struttura gerarchica ad albero).
method: metodo di ricerca dei contorni: CV_CHAIN_CODE, CV_CHAIN_APPROX_NONE, CV_CHAIN_APPROX_SIMPLE, CV_CHAIN_APPROX_TC89_L1, CV_CHAIN_APPROX_TC89_KCOS.
offset: Offset di spostamento di ogni contorno individuato. Utile nel caso sia utilizzata una ROI (Region of Interest) sull'immagine, in tal caso l'offset è pari al vertice in alto a sinistra della ROI.
La funzione ritorna sempre il numero di contorni individuati. In questo contesto tratterò unicamente risultati contenuti in una struttura ad albero (CV_RETR_TREE).
Per gli altri tipi di strutture consiglio la lettura del capitolo 8 del libro Learning OpenCV - O'Reilly, facilmente reperibile tramite www.amazon.com.

cvDrawContours( CvArr *img, CvSeq* contour, CvScalar external_color, CvScalar hole_color, int max_level, int thickness=1, int line_type=8, CvPoint offset=cvPoint(0,0) ):
img: l'immagine dove disegnare i contorni. Può essere un'immagine vuota oppure può essere utilizzato un clone dell'immagine elaborata per visualizzare i contorni direttamente sull'immagine 'reale'.
contour: la struttura dati contenente il risultato della funzione cvFindContour. Successivamente questa struttura dati sarà esaminata in oni dettaglio nel caso di struttura ad albero.
external_color/hole_color: colore del contorno esterno e dei fori. E' comodo l'utilizzo della macro OpenCV CV_RGB(r,g,b) per realizzare il colore voluto.
max_level: indica quali contorni disegnare: 0-solo il contorno puntato da contour. 1-il contorno puntato da contour e tutti i contorni sullo stesso livello. 2-tutti i contorni sullo stesso livello del contorno puntato da contour e tutti i contorni un livello sotto. <0-il contorno puntato da contour e gli 'n' livelli sotto di esso, dove n=abs(max_level)-1.
thickness: spessore del bordo del contorno
line_type: modalità di disegno del contorno

Discusse le funzioni utili a questo punto passiamo a parlare della struttura dati principale dove sono memorizzati i contorni individuati da cvFindContour: CvSeq

Codice (dalla documentazione OpenCV)
  1. #define CV_SEQUENCE_FIELDS() \
  2. int flags; /* micsellaneous flags */ \
  3. int header_size; /* size of sequence header */ \
  4. struct CvSeq* h_prev; /* previous sequence */ \
  5. struct CvSeq* h_next; /* next sequence */ \
  6. struct CvSeq* v_prev; /* 2nd previous sequence */ \
  7. struct CvSeq* v_next; /* 2nd next sequence */ \
  8. int total; /* total number of elements */ \
  9. int elem_size;/* size of sequence element in bytes */ \
  10. char* block_max;/* maximal bound of the last block */ \
  11. char* ptr; /* current write pointer */ \
  12. int delta_elems; /* how many elements allocated when the sequence grows (sequence granularity) */ \
  13. CvMemStorage* storage; /* where the seq is stored */ \
  14. CvSeqBlock* free_blocks; /* free blocks list */ \
  15. CvSeqBlock* first; /* pointer to the first sequence block */
  16.  
  17.  
  18. typedef struct CvSeq
  19. {
  20. CV_SEQUENCE_FIELDS()
  21. } CvSeq;
  22.  


La struttura è intuitiva e già commentata, ma alcuni campi che ci saranno utili hanno bisogno di una piccola ulteriore spiegazione:
flags: indica il tipo della sequenza. Nel nostro caso flag è uguale a CV_SEQ_ELTYPE_POINT|CV_SEQ_KIND_CURVE|CV_SEQ_FLAG_CLOSED che indica una sequenza di PUNTI che formano una CURVA CHIUSA.
header_size: è la dimensione in BYTE dell'header della sequenza. Può assumere due valori: sizeof(CvContour) (come nel nostro caso) o sizeof(CvChain) se si utilizza una struttura dati di tipo 'chain' non trattata in questo articolo.
h_prev/h_prev: questi sono due campi importanti per la navigazione della struttura. Sono puntatori al contorno precedente e successivo SULLO STESSO LIVELLO del contorno preso in considerazione. (PUNTATORI ORIZZONTALI)
h_prev/h_prev: come i due campi precedenti questi sono importanti per la navigazione della struttura. Sono puntatori al contorno un livello sopra e un livello sotto al contorno preso in considerazione. (PUNTATORI VERTICALI)
total: indica di quanti punti è composto il contorno.

Di seguito riporto un programma di esempio utile a dimostrare un metodo di visita dell'albero dei contorni.
(Il codice C++ di seguito riportato vuole essere unicamente un esempio didattico e non è ottimizzato per l'operazione che deve eseguire... ricordatevi che innestare così tanti while non è mai utile e soprattutto è sempre una facile causa di errore)

Codice
  1. #include "stdio.h"
  2. #include "cv.h"
  3. #include "highgui.h"
  4.  
  5. int main( int argc, char* argv[] )
  6. {
  7. IplImage* img = cvCreateImage( cvSize( 640, 480),8, 1 );
  8. cvSet( img, cvScalarAll(0) );
  9. IplImage* res = cvCreateImage( cvSize(640,480), 8, 3 );
  10. cvSetZero( res );
  11.  
  12. // -----> Disegno di due bersagli da analizzare contenuti in un rettangolo
  13. cvRectangle( img, cvPoint(30,30), cvPoint(630,440), CV_RGB(255,255,255), -1 );
  14.  
  15. cvCircle( img, cvPoint( 200,200), 120, CV_RGB(150,150,150), -1 );
  16. cvCircle( img, cvPoint( 200,200), 80, CV_RGB(100,100,100), -1 );
  17. cvCircle( img, cvPoint( 200,200), 40, CV_RGB(50,50,50), -1 );
  18. cvCircle( img, cvPoint( 200,200), 10, CV_RGB(10,10,10), -1 );
  19.  
  20. cvCircle( img, cvPoint( 480,300), 120, CV_RGB(150,150,150), -1 );
  21. cvCircle( img, cvPoint( 480,300), 80, CV_RGB(100,100,100), -1 );
  22. cvCircle( img, cvPoint( 480,300), 40, CV_RGB(50,50,50), -1 );
  23. cvCircle( img, cvPoint( 480,300), 10, CV_RGB(10,10,10), -1 );
  24. // <----- Disegno di due bersagli da analizzare contenuti in un rettangolo
  25.  
  26.  
  27. cvNamedWindow( "Originale" );
  28. cvShowImage( "Originale", img );
  29.  
  30. cvNamedWindow( "Risultato" );
  31.  
  32. // Evidenziazione dei contorni
  33. cvCanny( img, img, 5, 90 );
  34.  
  35. cvNamedWindow( "Canny" );
  36. cvShowImage( "Canny", img );
  37.  
  38. // Espansione dei contorni in modo da analizzarli in modo "utile"
  39. // (Provate a vedere cosa succede commentando la riga successiva)
  40. cvDilate( img, img, NULL, 1 );
  41.  
  42. cvNamedWindow( "Canny dilated" );
  43. cvShowImage( "Canny dilated", img );
  44.  
  45. CvMemStorage* storage = cvCreateMemStorage( 0);
  46. CvSeq* contours=NULL;
  47. int contour_num;
  48.  
  49. // Analisi dei contorni
  50. contour_num = cvFindContours( img, storage, &contours, sizeof(CvContour),
  51. CV_RETR_TREE, CV_CHAIN_APPROX_NONE );
  52.  
  53. printf( "Trovati %d contorni\r\n", contour_num );
  54.  
  55. CvSeq* succ = contours;
  56.  
  57. int cont=0;
  58.  
  59. while( succ!=NULL )
  60. {
  61. cvDrawContours( res, succ, CV_RGB(255,0,0), CV_RGB(255,0,0), 0, -1 );
  62. cont++;
  63.  
  64. cvShowImage( "Risultato", res );
  65. cvWaitKey(0);
  66.  
  67. CvSeq* hole_0 = succ->v_next;
  68. while( hole_0!=NULL )
  69. {
  70. cvDrawContours( res, hole_0, CV_RGB(0,255,0), CV_RGB(0,255,0),0, -1 );
  71. cont++;
  72.  
  73. cvShowImage( "Risultato", res );
  74. cvWaitKey(0);
  75.  
  76. CvSeq* hole_1 = hole_0->v_next;
  77. while( hole_1!=NULL )
  78. {
  79. cvDrawContours( res, hole_1, CV_RGB(0,0,255), CV_RGB(0,0,255), 0, -1 );
  80. cont++;
  81.  
  82. cvShowImage( "Risultato", res );
  83. cvWaitKey(0);
  84.  
  85. CvSeq* hole_2 = hole_1->v_next;
  86. while( hole_2!=NULL )
  87. {
  88. cvDrawContours( res, hole_2, CV_RGB(0,255,255), CV_RGB(0,255,255), 0, -1 );
  89. cont++;
  90.  
  91. cvShowImage( "Risultato", res );
  92. cvWaitKey(0);
  93.  
  94. CvSeq* hole_3 = hole_2->v_next;
  95. while( hole_3!=NULL )
  96. {
  97. cvDrawContours( res, hole_3, CV_RGB(255,255,0), CV_RGB(255,255,0), 0, -1 );
  98. cont++;
  99.  
  100. cvShowImage( "Risultato", res );
  101. cvWaitKey(0);
  102.  
  103. CvSeq* hole_4 = hole_3->v_next;
  104. while( hole_4!=NULL )
  105. {
  106. cvDrawContours( res, hole_4, CV_RGB(255,0,255), CV_RGB(255,0,255), 0, -1 );
  107. cont++;
  108.  
  109. cvShowImage( "Risultato", res );
  110. cvWaitKey(0);
  111.  
  112. CvSeq* hole_5 = hole_4->v_next;
  113. while( hole_5!=NULL )
  114. {
  115. cvDrawContours( res, hole_5, CV_RGB(255,255,255), CV_RGB(255,255,255), 0, -1 );
  116. cont++;
  117.  
  118. cvShowImage( "Risultato", res );
  119. cvWaitKey(0);
  120.  
  121.  
  122. CvSeq* hole_6 = hole_5->v_next;
  123. while( hole_6!=NULL )
  124. {
  125. cvDrawContours( res, hole_6, CV_RGB(100,100,100), CV_RGB(100,100,100), 0, -1 );
  126. cont++;
  127.  
  128. cvShowImage( "Risultato", res );
  129. cvWaitKey(0);
  130.  
  131.  
  132. CvSeq* hole_7 = hole_6->v_next;
  133. while( hole_7!=NULL )
  134. {
  135. cvDrawContours( res, hole_7, CV_RGB(0,0,0), CV_RGB(0,0,0), 0, -1 );
  136. cont++;
  137.  
  138. cvShowImage( "Risultato", res );
  139. cvWaitKey(0);
  140.  
  141.  
  142. CvSeq* hole_8 = hole_7->v_next;
  143. while( hole_8!=NULL )
  144. {
  145. cvDrawContours( res, hole_8, CV_RGB(200,200,200), CV_RGB(200,200,200), 0, -1 );
  146. cont++;
  147.  
  148. cvShowImage( "Risultato", res );
  149. cvWaitKey(0);
  150.  
  151. hole_8 = hole_8->h_next;
  152. }
  153.  
  154. hole_7 = hole_7->h_next;
  155. }
  156.  
  157. hole_6 = hole_6->h_next;
  158. }
  159.  
  160. hole_5 = hole_5->h_next;
  161. }
  162.  
  163. hole_4 = hole_4->h_next;
  164. }
  165.  
  166. hole_3 = hole_3->h_next;
  167. }
  168.  
  169. hole_2 = hole_2->h_next;
  170. }
  171.  
  172. hole_1 = hole_1->h_next;
  173. }
  174.  
  175. hole_0 = hole_0->h_next;
  176. }
  177.  
  178. succ = succ->h_next;
  179. }
  180.  
  181. printf( "Disegnati %d contorni\r\n", cont );
  182.  
  183. // Alla fine
  184. printf( "\r\nPremi un tasto per terminare..." );
  185. cvWaitKey(0);
  186.  
  187. cvReleaseImage( &img );
  188. cvReleaseImage( &res );
  189. cvReleaseMemStorage( &storage );
  190. cvDestroyAllWindows();
  191.  
  192. return 0;
  193. }
  194.  



Effettuiamo ora un'analisi del codice discutendo i punti più importanti:
La prima parte si occupa del disegno di una struttura di contorni da analizzare. Il risultato del disegno è visibile nella figura seguente:

la struttura da analizzare è composta da un rettangolo esterno che contiene due bersagli.
Il BORDO ESTERNO del rettangolo sarà la "radice" ("root") del nostro albero di contorni, i due bersagli i suoi "figli" o anche "rami" ("child","branch"), infine i due cerchi più interni saranno le "foglie" ("leaf").
Per ben capire in che ordine sono analizzati i contorni vi consiglio di eseguire il codice (se avete problemi di compilazione o linking non esitate a contattarmi tramite l'apposito thread del forum).
Il funzionamento del programma è semplice: innanzitutto sarà visualizzata l"immagine originale da analizzare e i passaggi di filtraggio della stessa. Quindi è visualizzata la finestra del risultato dell"analisi con il PRIMO contorno riconosciuto.
Premendo un tasto si passerà al bordo successivo e così via. Ho inserito questa pausa tra il disegno di un bordo e il successivo per far ben capire qual"è la direzione di visita dell"albero dei contorni (tips: associate i colori ai contorni disegnati)
Come avrete notato l'albero ricostruito ha questa struttura:

Struttura ad albero:
  root (succ rosso)
  |
  |
  succ->v_next (hole_0 verde)
  |
  ___________________________________
  |                                                  |
  hole_0->v_next (hole_1 blu)          hole_0->h_next (hole_1 blu)
  |                                                  |
  |                                                  |
  hole_1->v_next (hole_2 ciano)        hole_1->v_next (hole_2 ciano)
  |                                                  |
  |                                                  |
  hole_2->v_next (hole_3 giallo)       hole_2->v_next (hole_3 giallo)
  |                                                  |
  |                                                  |
  hole_3->v_next (hole_4 rosa)         hole_3->v_next (hole_4 rosa)
  |                                                  |
  |                                                  |
  hole_4->v_next (hole_5 bianco)       hole_4->v_next (hole_5 bianco)
  |                                                  |
  |                                                  |
  hole_5->v_next (hole_6 grigio scuro)     hole_5->v_next (hole_6 grigio scuro)
  |                                                  |
  |                                                  |
  hole_6->v_next (hole_7 nero)         hole_6->v_next (hole_7 nero)
  |                                                  |
  |                                                  |
  hole_7->v_next (hole_8 grigio chiaro)     hole_7->v_next (hole_8 grigio chiaro)

        



Ora rimane da capire come si arriva all'albero sopra illustrato.
Il primo passo è l'applicazione del Filtro di Canny (cvCanny) per mettere in evidenza i contorni ("edge"). Il risultato è visibile nella seguente figura:

Il passo successivo è l'applicazione dell'operatore morfologico di dilatazione (cvDilate) per mettere ancor più in evidenza i contorni. Il filtro di dilate ha l'ulteriore scopo, non evidente in immagini nitide e ben separate come queste, di connette punti dei contorni che non risulterebbero completamente congiunti dal solo filtro di Canny.
La dilatazione è ben evidente nella seguente figura:

[Come consigliato in un commento nel codice provate a eseguire il programma commentando la cvDilate per osservare le differenze nel risultato]

Finalmente viene utilizzato il comando cvFindContours per cercare i contorni dell'immagine e organizzarli nella struttura dati come precedentemente indicato.
La struttura dei contorni sarà dunque ad albero (CV_RETR_TREE) e non sarà effettuata nessuna approssimazione (CV_CHAIN_APPROX_NONE).
Le successive righe di codice sono puramente "didattiche" e mostrano quali campi della struttura dati CvSeq utilizzare per poter accedere ai contorni.
La nidificazione di While permette di accedere ai contorni fino ad un massimo livello di 7 contorni "innestati" ed è fatta ad hoc per ricreare la struttura da riconoscere.
Come già discusso quando si è parlato della struttura dati CvSeq, i campi fondamentali per l'esplorazione sono i puntatori h_next e v_next che permettono di passare ai contorni successivi sullo stesso livello o sul livello inferiore.
Durante l'esecuzione del programma ogni livello di nidificazione è evidenziato da una pausa (cvWaitKey(0);) che obbliga l'utente a premere un pulsante per passare al livello successivo potendo così osservare come i contorni vengono rilevati e immagazzinati nella struttura ad albero.

Il programma termina con il rilascio della memoria dinamica e la distruzione delle finestre utilizzate per mostrare le immagini elaborate:
cvReleaseImage( &img );
cvReleaseImage( &res );
cvReleaseMemStorage( &storage );
cvDestroyAllWindows();


 

Gioblu Robotics © 2010 - 2012 · Sitemap · privacy

gioscarab@gmail.com

Gioblu BOTServer è online dal 10 Aprile 2010 - 319.232 Visite - 1.027.175 Pagine visualizzate - 182.309 Visitatori unici - 536 utenti attivi