|
OpenCV e il riconoscimento dei contorni di un'immagine
Per chi volesse discutere l'argomento oppure riscontrasse delle imperfezioni o degli errori
è disponibile un apposito thread sul forum del sito:
vai al forum
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) #define CV_SEQUENCE_FIELDS() \ int flags; /* micsellaneous flags */ \ int header_size; /* size of sequence header */ \ struct CvSeq* h_prev; /* previous sequence */ \ struct CvSeq* h_next; /* next sequence */ \ struct CvSeq* v_prev; /* 2nd previous sequence */ \ struct CvSeq* v_next; /* 2nd next sequence */ \ int total; /* total number of elements */ \ int elem_size;/* size of sequence element in bytes */ \ char* block_max;/* maximal bound of the last block */ \ char* ptr; /* current write pointer */ \ int delta_elems; /* how many elements allocated when the sequence grows (sequence granularity) */ \ CvMemStorage* storage; /* where the seq is stored */ \ CvSeqBlock* free_blocks; /* free blocks list */ \ CvSeqBlock* first; /* pointer to the first sequence block */ typedef struct CvSeq { CV_SEQUENCE_FIELDS() } CvSeq;
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 (dalla documentazione OpenCV) #include "stdio.h" #include "cv.h" #include "highgui.h" int main( int argc, char* argv[] ) { IplImage* img = cvCreateImage( cvSize( 640, 480),8, 1 ); cvSet( img, cvScalarAll(0) ); IplImage* res = cvCreateImage( cvSize(640,480), 8, 3 ); cvSetZero( res ); // -----> Disegno di due bersagli da analizzare contenuti in un rettangolo cvRectangle( img, cvPoint(30,30), cvPoint(630,440), CV_RGB(255,255,255), -1 ); cvCircle( img, cvPoint( 200,200), 120, CV_RGB(150,150,150), -1 ); cvCircle( img, cvPoint( 200,200), 80, CV_RGB(100,100,100), -1 ); cvCircle( img, cvPoint( 200,200), 40, CV_RGB(50,50,50), -1 ); cvCircle( img, cvPoint( 200,200), 10, CV_RGB(10,10,10), -1 ); cvCircle( img, cvPoint( 480,300), 120, CV_RGB(150,150,150), -1 ); cvCircle( img, cvPoint( 480,300), 80, CV_RGB(100,100,100), -1 ); cvCircle( img, cvPoint( 480,300), 40, CV_RGB(50,50,50), -1 ); cvCircle( img, cvPoint( 480,300), 10, CV_RGB(10,10,10), -1 ); // <----- Disegno di due bersagli da analizzare contenuti in un rettangolo cvNamedWindow( "Originale" ); cvShowImage( "Originale", img ); cvNamedWindow( "Risultato" ); // Evidenziazione dei contorni cvCanny( img, img, 5, 90 ); cvNamedWindow( "Canny" ); cvShowImage( "Canny", img ); // Espansione dei contorni in modo da analizzarli in modo "utile" // (Provate a vedere cosa succede commentando la riga successiva) cvDilate( img, img, NULL, 1 ); cvNamedWindow( "Canny dilated" ); cvShowImage( "Canny dilated", img ); CvMemStorage* storage = cvCreateMemStorage( 0); CvSeq* contours=NULL; int contour_num; // Analisi dei contorni contour_num = cvFindContours( img, storage, &contours, sizeof(CvContour), CV_RETR_TREE, CV_CHAIN_APPROX_NONE ); printf( "Trovati %d contorni\r\n", contour_num ); CvSeq* succ = contours; int cont=0; while( succ!=NULL ) { cvDrawContours( res, succ, CV_RGB(255,0,0), CV_RGB(255,0,0), 0, -1 ); cont++; cvShowImage( "Risultato", res ); cvWaitKey(0); CvSeq* hole_0 = succ->v_next; while( hole_0!=NULL ) { cvDrawContours( res, hole_0, CV_RGB(0,255,0), CV_RGB(0,255,0),0, -1 ); cont++; cvShowImage( "Risultato", res ); cvWaitKey(0); CvSeq* hole_1 = hole_0->v_next; while( hole_1!=NULL ) { cvDrawContours( res, hole_1, CV_RGB(0,0,255), CV_RGB(0,0,255), 0, -1 ); cont++; cvShowImage( "Risultato", res ); cvWaitKey(0); CvSeq* hole_2 = hole_1->v_next; while( hole_2!=NULL ) { cvDrawContours( res, hole_2, CV_RGB(0,255,255), CV_RGB(0,255,255), 0, -1 ); cont++; cvShowImage( "Risultato", res ); cvWaitKey(0); CvSeq* hole_3 = hole_2->v_next; while( hole_3!=NULL ) { cvDrawContours( res, hole_3, CV_RGB(255,255,0), CV_RGB(255,255,0), 0, -1 ); cont++; cvShowImage( "Risultato", res ); cvWaitKey(0); CvSeq* hole_4 = hole_3->v_next; while( hole_4!=NULL ) { cvDrawContours( res, hole_4, CV_RGB(255,0,255), CV_RGB(255,0,255), 0, -1 ); cont++; cvShowImage( "Risultato", res ); cvWaitKey(0); CvSeq* hole_5 = hole_4->v_next; while( hole_5!=NULL ) { cvDrawContours( res, hole_5, CV_RGB(255,255,255), CV_RGB(255,255,255), 0, -1 ); cont++; cvShowImage( "Risultato", res ); cvWaitKey(0); CvSeq* hole_6 = hole_5->v_next; while( hole_6!=NULL ) { cvDrawContours( res, hole_6, CV_RGB(100,100,100), CV_RGB(100,100,100), 0, -1 ); cont++; cvShowImage( "Risultato", res ); cvWaitKey(0); CvSeq* hole_7 = hole_6->v_next; while( hole_7!=NULL ) { cvDrawContours( res, hole_7, CV_RGB(0,0,0), CV_RGB(0,0,0), 0, -1 ); cont++; cvShowImage( "Risultato", res ); cvWaitKey(0); CvSeq* hole_8 = hole_7->v_next; while( hole_8!=NULL ) { cvDrawContours( res, hole_8, CV_RGB(200,200,200), CV_RGB(200,200,200), 0, -1 ); cont++; cvShowImage( "Risultato", res ); cvWaitKey(0); hole_8 = hole_8->h_next; } hole_7 = hole_7->h_next; } hole_6 = hole_6->h_next; } hole_5 = hole_5->h_next; } hole_4 = hole_4->h_next; } hole_3 = hole_3->h_next; } hole_2 = hole_2->h_next; } hole_1 = hole_1->h_next; } hole_0 = hole_0->h_next; } succ = succ->h_next; } printf( "Disegnati %d contorni\r\n", cont ); // Alla fine printf( "\r\nPremi un tasto per terminare..." ); cvWaitKey(0); cvReleaseImage( &img ); cvReleaseImage( &res ); cvReleaseMemStorage( &storage ); cvDestroyAllWindows(); return 0; }
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();
E con il programma termina anche questo breve tutorial sulla ricerca dei contorni di un'immagine utilizzando la libreria OpenCV e il linguaggio C++.
Per chi volesse approfondire gli argomenti trattati è consigliatissimo il libro su OpenCV:
Learning OpenCV - O'Reilly
o il sito Wiki di OpenCV:
http://opencv.willowgarage.com/wiki/
Per chi volesse discutere l'argomento oppure riscontrasse delle imperfezioni o degli errori
è disponibile un apposito thread sul forum del sito:
vai al forum
Il tutorial ti è stato utile?
Offrimi un caffè, sarò felice di realizzarne di migliori.
|