Bu yazımızda kamera üzerinde tıklanan noktaların takibi üzerine yaptığımız ikinci uygulamayı paylaşacağız.

Bu uygulama kapsamında;

* Bir objenin nasıl takip edilebileceği,

* Obje takibi konusunda hangi OpenCV fonksiyonlarının kullanılabileceği

konularında bilgi verilecektir.

 

 

 

 

Obje Takibi 

Obje takibi,takip edilen nesnenin özelliklerinin alınıp daha sonradan bu özelliklerin aranması olarak tanımlanabilir. Özellik dediğimiz kavram çok geniş olduğu için aynı şekilde obje takibi yapma şekilleride çok fazladır. Biz Lucas Kanade algoritması üzerinde optical-flow aracını kullandık. SIFT gibi farklı uygulamalar denemek de aklımızda var fakat ilk aşama için güzel bir sonuç aldık. Lucas Kanade takibinde 3 varsayım kullanılır. Bunlar bir frame ile diğeri arasındaki parlaklığın tutarlılığı, frame ler arasındaki küçük hareketlilikler ve konumsal tutarlılıktır. Fakat daha büyük resimler için resim piramitleri kullanılır. Programımız için faydalandığımız kaynaklardan biri olan optic-flow örneği incelenebilir.

Şekildeki örnekte optik akış rahat bir şekilde gözlenebilir.

Lucas Kanade Algoritması

Bu algoritma piksellerin frame den frame e değişimini gözleyip, değişimin yönünü ve hızını hesaplamamıza yarıyor. Algoritmada akışın başta sabit olduğu düşünülüyor ve önce lokal komşulara bakılıyor. Daha sonra temel optic-flow denklemleri ile bütün piksellerdeki komşularda algoritma koşturuluyor.

Burada bu algoritma ile, girilen ilk pozisyona göre hedefin yerini belirlemeye çalıştık. İlk girdileri manuel olarak dışarıdan bir ara yüz diyebileceğimiz bir ortamda verdik ve Lucas Kanade algoritmasını optic-flow üzerinde koşturduk. Bir çok noktayı tek seferde takip edebilmek ve geniş bir alanda çalışabilmek için piramit yapısını kullandık. Bir çok noktada takip yapmamızın amacı ise kamerayı daha hassas bir hale getirmekti.

Önemli OpenCV Fonksiyonları

IplImage* cvCreateImage(CvSize size, int depth, int channels)

İmaj oluşturma fonksiyonu olarak tanımlanabilir. IplImage tipinde bir değer döndüreceği için daha önceden bu değişkende bir öge tanımlamamız gerekir.

Size parametresi, resmin genişlik ve yüksekliğini ifade eder.

Depth parametresi, resmin derinliğini ifade eder. IplImage değişkenin içine baktığımızda bu derinlik değerlerini görebiliriz;

  • IPL_DEPTH_8U – Unsigned 8-bit integer
  • IPL_DEPTH_8S – Signed 8-bit integer
  • IPL_DEPTH_16U – Unsigned 16-bit integer
  • IPL_DEPTH_16S – Signed 16-bit integer
  • IPL_DEPTH_32S – Signed 32-bit integer
  • IPL_DEPTH_32F – Single-precision floating point
  • IPL_DEPTH_64F – Double-precision floating point
Bu tiplerde derinlik kullanmamıza OpenCV izin veriyor.
Channels parametresi, yine IplImage özelliklerinden olup , bir piksel üzerindeki kanal sayısını ifade eder.  Çoğu OpenCV fonksiyonu 1-4 arası değer alır. Bu kanallar ise renk tiplerini belirler diyebiliriz.

void cvFindCornerSubPix(const CvArr* image, CvPoint2D32f* corners, int count, CvSize win, CvSize zero_zone, CvTermCriteria criteria)

Bu projede obje takibi ile ilgili kullandığımız en önemli 2 fonksiyondan biri bu fonksiyon olmuştur. Objenin özelliklerini alabilmek için gerekli bir fonksiyondur. Objenin köşegenlerinin alt piksellerini ayrıntılandırarak daha sonra bulunabilmesine olanak sağlar. Aslında çok detaylı ve derin konulardır ama temel anlamda bu çıkarım yapılabilir.

Image parametresi,  input resmimizdir.

Corners parametresi, input parametremizin köşe koordinatlarıdır. Fonksiyon çıkışında temizlenmiş olarak output verir.

Count parametresi, köşelerin sayılarını ifade eder.

Win parametresi,  başta anlamakta güçlük çektiğimiz bir parametre oldu. Arama penceresinin uzunluğunun yarısını ifade ediyor.  Örnek olarak eğer  win=(5,5) dersek (5*2 + 1)  X  (5*2 + 1) boyutlarında bir arama penceresinde arama yapabilir. Daha uzun alanlarda arama yapmayacak ve sınırların dışına çıktığımızda hata alacağız. O yüzden kameranın görüntüleneceği pencereye göre boyutlandırma yaptık.

Zero_zone Parametresi, Otokorelasyon matrisinin olası hatalarından kaçınmak için kullanılır. Çok detaylı bir konudur.  (-1,-1) değeri böyle bir bölge olmadığını tanımlar, bizde bu avantajı kullanarak bu parametreyi kullanmaktan ilk aşamada kaçındık.

Criteria parametresi,  Köşe ayrıntılandırılmasının ne zaman sona ereceğini bize söyleyen bir kriterdir. Katman katman düşünülebilir. sürekli ayrıntılandırılabilir. Yeterli sayıya(certain number of iterations) ulaştıktan sonra ya da gereken doğruluk(requiered accuracy) elde edildikten sonra köşe ayrıntılandırılması(corner position refinement ) sona erer.

void cvCalcOpticalFlowPyrLK(const CvArr* prev, const CvArr* curr, CvArr* prevPyr, CvArr* currPyr, const CvPoint2D32f* prevFeatures, CvPoint2D32f* currFeatures, int count, CvSize winSize, int level, char* status, float*track_error, CvTermCriteria criteria, int flags)

Bu fonksiyon ile birlikte Lucas- Kanade metodu ile bilikte optical flow dediğimiz obje takibini gerçekleştiriyoruz.

Prev parametresi, ilk frame i temsil ediyor.

Curr parametresi, t zaman sonraki ikinci frame i ifade ediyor.

PrevPyr parametresi, ilk frame i buffer gibi bir yere depoluyor. Bu buffer 1. seviyeden istenilen seviyeye kadar piramid depolayabilecek yeterlilikte olmalıdır. Bu yüzden (image_width+8)*image_height/3 yeterlilik seviyesi olarak belirlenmiştir.

CurrPyr parametresi, PrevPyr gibidir, ikinci frame için geçerlidir.

PrevFeatures parametresi, hangi akışların bulunması gerektiğini söyleyen noktalar dizisini ifade eder.

CurrFeatures parametresi, ikinci image daki özellikerin hesaplandığı noktaları içerir.

Count parametresi, özellik noktalarının sayısını ifade ediyor.

Winsize parametresi, her bir piramid seviyesinde arama yapılan yerin boyutunu ifade ediyor.

Level parametresi, maksimum piramid seviyesini ifade ediyor.

Status parametresi, Akışı takip edilecek olan özelliğin bulunması halinde 1 , aksi durumda 0 veren bir parametredir.

Track_error parametresi,  orjinal ve hareketli noktaar arasındaki hatayı temsil eder. Bu özelliği kullanmamayı tercih ettik.

Criteria parametresi,  ne zaman işlemin durması gerektiğini belirler. Hassasiyet olarak düşünebiliriz.

Flags parametresi, hangi işlemleri bitirdiğine dair bazı alt durumları gözlemlememiz mümkün. bunun için;

     * CV_LKFLOWPyr_A_READY 

     * CV_LKFLOWPyr_B_READY

     * CV_LKFLOW_INITIAL_GUESSES

bayrakları kontrol edilebilir.

Program

Öncelikle yazdığımız program için akış şeması çıkarttık. Daha sonra uygulama kısmına geçtik.

Dikkat: Butona basıldı mı? işleminden sonra Hayır durumundan frame leri güncelle durumuna geçiyoruz. Orada yanlış bir çizim olmuş.
Akış şemasını çizdikten sonra kodumuzu anlatmaya başlayabiliriz.
main   
int main(int argc, char **argv) {
 
	capture = cvCaptureFromCAM(0);
	cvNamedWindow( "Test", CV_WINDOW_AUTOSIZE );
	cvSetMouseCallback( "Test", camera_on_mouse, 0 );
	run_cam();
	return 0;
}
 

Burada kamera görüntüsünü aldıktan sonra ve mouse için interrupt atamasını yaptıktan sonra programı çağırmak için kurulmuş main fonksiyonudur. main fonksiyonunu elimizden geldiğince kısa tutmaya özen gösterdik.

run_cam   
void run_cam(){
	while(1){
		//printf("it is in run_cam function\n");
		frame = cvQueryFrame(capture);
		if(!image)
			init_features();
		cvCopy( frame, image, 0 );
		cvCvtColor( image, grey, CV_BGR2GRAY );
		if(counter){
			start_track();
			create_circle();
		}
		if(add_point){
			counter++;
			get_features();
			add_point = 0;
		}
		save_old_frames();
		show_camera();
	}
}
 

Bu fonksiyon programımızın asıl kemik yapısını oluşturmaktadır. Bu programda neler yapıldığı rahatça anlaşılabilir. Önce görüntüden frame i alıyoruz. Daha sonra eğer daha önceden image oluşturulmadıysa yani özellikler için tanımlama yapılmadıysa tanımlama fonksiyonunu çağırıyor. Diğer durumda frame in o anki görüntüsünü image içine kopyalıyor. Aynı zamanda grey diye daha önceden tanımladığımız piramidlerden bir tanesi için renk değişikliğine gidiyor. Çünkü resimde RGB değerler yerine Grey_scale değerler kullanmak özellik yakalamayı kolaylaştırıyor. Daha sonra Eğer daha önceden takip için nokta belirtildiyse onları takip ediyor. Daha sonra tekrar nokta tıklanıp tıklanmadığının kontrolü için bir kısmımız var. Orada ise eğer tıklandıysa yeni değerler alınıp kaydediliyor. En sonunda kamerada gösteriliyor. Pirsmidleri oluşturmak için eski değerleri kaydedip programa devam ediyoruz. Aslında burada anlatılan her şey bize yukarıda anlattığımız akış şemasını veriyor.

Böylelikle main.cpp dosyamızı tamamlıyoruz. Kod karmaşıklığını giderebilmek için kendimize bir header dosyası oluşturduk. header dosyasındaki fonksiyonlarına göz atalım;

void init_features(){
	//printf("it is in init_features function\n");
	 image = cvCreateImage( cvGetSize(frame), 8, 3 );
	 image->origin = frame->origin;
	 grey = cvCreateImage( cvGetSize(frame), 8, 1 );
	 prev_grey = cvCreateImage( cvGetSize(frame), 8, 1 );
	 pyramid = cvCreateImage( cvGetSize(frame), 8, 1);
	 prev_pyramid = cvCreateImage( cvGetSize(frame), 8, 1 );
	 flags = 0;
     status = (char*)cvAlloc(500);
     points_32f[0] = (CvPoint2D32f*)cvAlloc(500*sizeof(points_32f[0][0]));
     points_32f[1] = (CvPoint2D32f*)cvAlloc(500*sizeof(points_32f[0][0]));
}
 

Bu fonksiyon bir defaya mahsus çalışacak ve başta özelliklerin tanımlanması için kullanılacaktır. Öncelikle frame üzerinde işlem yapamıyoruz çünkü frame kamera görüntülerini alıyor. Ama image diye bir değişken tanımladığımız takdirde onu frame gibi kullanabiliriz. Daha sonra isimlerden de anlaşılacağı gibi obje takibi için gerekli değişkenler tanımlanıyor. Burada status değişkeni için koyabileceğimiz nokta sayısı kadar büyüklükte olmalı ki her birisinin durumunu gösterebilsin. Bizde maksimum 500 nokta koyacağımızı düşünüp status değişkenini tanımlıyoruz. points_32f değişkeni ise daha sonradan köşe belirlemede fonksiyon değişkeni için kullanacağımız basit bir resim tipi değişikliği için gerekli oluyor. aynı zamanda aynı şekilde her bir noktayı her seferinde gösterip aynı zamanda takip etmemiz gerektiği için boyutunu 500 yaptık. Atlanılmaması gereken bir nokta, her seferinde hepsini tekrar takip edip tekrar ekrana bastığımızdır.

void get_features(){
	//printf("it is in get features function\n");
    points_32f[1][counter-1] = cvPointTo32f(point);
    cvFindCornerSubPix(
                		grey,
                		points_32f[1]+counter-1,
                		1,
                		cvSize(15,15),
                		cvSize(-1,-1),
                		cvTermCriteria(CV_TERMCRIT_ITER|CV_TERMCRIT_EPS,20,0.03)
                );
 
}

Burada belirlenen noktanın özellikleri alınıyor. Dikkat ederseniz grey resmi üzerinden özellikler belirleniyor. Diğer noktalar fonksiyon tanımına bakılarak anlaşılabilir. counter konusunda da sürekli 1 çıkarmamızın sebebi ise fonksiyona girmeden önce counter değerini artırmış olmamızdan dolayıdır .

void start_track(){
	//printf("it is in start_track function\n");
	cvCalcOpticalFlowPyrLK(
	        			prev_grey,
	        			grey,
	        			prev_pyramid,
	        			pyramid,
	        			points_32f[0],
	        			points_32f[1],
	        			counter,
	        			cvSize(15,15),
	        			3,
	        			status,
	        			0,
	        			cvTermCriteria(CV_TERMCRIT_ITER|CV_TERMCRIT_EPS,20,0.03),
	        			flags
	        	);
 
}
 

Burada ise belirlediğimiz özellikleri takip ediyoruz. Zaten görüntüyü nasıl işleyeceğimizi anlarsak buralar genelde yap-boz şeklinde geçiyor. Fonksiyon ne istiyorsa ona onu veriyoruz:)

void save_old_frames(){
	CV_SWAP( prev_grey, grey, swap_temp );
	CV_SWAP( prev_pyramid, pyramid, swap_temp );
	CV_SWAP( points_32f[0], points_32f[1], temp_points );
}
 

CV_SWAP fonksiyonu aslında;

(       swap_temp = prev_grey       ,        prev_grey = grey        ,           grey = swap_temp        )

işlemini yapıyor. Değerleri güncelliyor ve biz hep bir önceki değeri görüyoruz aslında.

burada ise güncelleme işlemi olarak düşünülebilir.

main.cpp   
//Author: Tuna AYAN tunayan@gmail.com
 
#include <cv.h>
#include <highgui.h>
#include <stdio.h>
#include <ctype.h>
#include "camera.h"
 
void run_cam();
int main(int argc, char **argv) {
 
	capture = cvCaptureFromCAM(0);					  // Take captures from Cam
	cvNamedWindow( "Test", CV_WINDOW_AUTOSIZE );	  // Creating Test Window
	cvSetMouseCallback( "Test", camera_on_mouse, 0 ); // Mouse interrupt on Test window. interrupt function is camera_on_mouse
	run_cam();
	return 0;
}
void run_cam(){
	while(1){
		//printf("it is in run_cam function\n");
		frame = cvQueryFrame(capture);
		if(!image)
			init_features();
		cvCopy( frame, image, 0 );
		cvCvtColor( image, grey, CV_BGR2GRAY );
		if(counter){
			start_track();
			create_circle();
		}
		if(add_point){
			counter++;
			get_features();
			add_point = 0;
		}
		save_old_frames();
		show_camera();
	}
}
 
camera.h   
/*
 * camera.h
 *
 *  Created on: Oct 1, 2011
 *  Author: Tuna AYAN tunayan@gmail.com
 */
 
#ifndef CAMERA_H_
#define CAMERA_H_
 
CvPoint point;
int add_point=0;
CvCapture *capture;
IplImage* frame,tmp_frame;
CvPoint2D32f *points_32f[2] = {0,0};
CvPoint2D32f *temp_points;
int counter = 0;
 
/*****Features*****/
IplImage *image = 0,
		 *grey = 0,
		 *prev_grey = 0,
		 *pyramid = 0,
		 *prev_pyramid = 0,
		 *swap_temp;
/*****END*********/
 
/****Features Control****/
char* status = 0;
int flags;
/******END***************/
 
void camera_on_mouse( int event, int x, int y, int flags, void* param ){
 
	if( event == CV_EVENT_LBUTTONDOWN )
	    {
		//printf("it is in camera on mouse function\n");
			point = cvPoint(x,y);
	        add_point = 1;
	    }
 
}
 
void create_circle()
{
	//printf("it is in create_circle function\n");
	int i=0,k=0;
	for(;i< counter; i++){
		if( !status[i] )
			continue;
		points_32f[1][k++] = points_32f[1][i];
		cvCircle(image, cvPointFrom32f(points_32f[1][i]), 3, CV_RGB(255,0,0), -1, 8,0);
	}
}
void show_camera(){
	//printf("it is in show_camera function\n");
	cvShowImage( "Test", image );
	char c = cvWaitKey(33);
	if(c== 27)exit(0);
 
}
void init_features(){
	//printf("it is in init_features function\n");
	 image = cvCreateImage( cvGetSize(frame), 8, 3 );
	 image->origin = frame->origin;
	 grey = cvCreateImage( cvGetSize(frame), 8, 1 );
	 prev_grey = cvCreateImage( cvGetSize(frame), 8, 1 );
	 pyramid = cvCreateImage( cvGetSize(frame), 8, 1);
	 prev_pyramid = cvCreateImage( cvGetSize(frame), 8, 1 );
	 flags = 0; //flag for track accomplished
     status = (char*)cvAlloc(500);
     points_32f[0] = (CvPoint2D32f*)cvAlloc(500*sizeof(points_32f[0][0]));
     points_32f[1] = (CvPoint2D32f*)cvAlloc(500*sizeof(points_32f[0][0]));
}
 
void get_features(){
	//printf("it is in get features function\n");
    points_32f[1][counter-1] = cvPointTo32f(point);
    cvFindCornerSubPix(
                		grey,
                		points_32f[1]+counter-1,
                		1,
                		cvSize(15,15),
                		cvSize(-1,-1),
                		cvTermCriteria(CV_TERMCRIT_ITER|CV_TERMCRIT_EPS,20,0.03)
                );
 
}
void start_track(){
	//printf("it is in start_track function\n");
	cvCalcOpticalFlowPyrLK(
	        			prev_grey,
	        			grey,
	        			prev_pyramid,
	        			pyramid,
	        			points_32f[0],//prev one
	        			points_32f[1],//current one
	        			counter,
	        			cvSize(15,15),
	        			3,
	        			status,
	        			0,
	        			cvTermCriteria(CV_TERMCRIT_ITER|CV_TERMCRIT_EPS,20,0.03),
	        			flags
	        	);
	//flags |= CV_LKFLOW_PYR_A_READY;
 
}
void save_old_frames(){
	CV_SWAP( prev_grey, grey, swap_temp );
	CV_SWAP( prev_pyramid, pyramid, swap_temp );
	CV_SWAP( points_32f[0], points_32f[1], temp_points );
}
 
#endif /* CAMERA_H_ */
 

Belirtmekte fayda var bu kodları yazarken OpenCV sample kodlarından da faydalandık. OpenCV kurduğunuz takdirde dizinde samples klasörü altından örnek uygulamalara erişilebilir. Bazı fonksiyonların nasıl çalıştığını örnek uygulamalarla öğrenmek gerçekten faydalı oluyor.

Kamerayı hareketli hale getirmek istediğimizi belirtmiştik peki kamera nereyi takip edecek? nokta yoğunluklarının ortalamasını alıp o noktayı takip ettirerek hata payını azaltmayı düşünüyoruz. Nokta kayıpları olacaktır. Bir daha ki yazımız bir aksilik olmazsa kamerayı hareketli hale getirmek üzerine olacaktır. Bu konu için ise Stellaris LM3S811 kitini kullanmayı düşünüyoruz.