基於GTK的USB視頻採集程序

        基於GTK的USB視頻採集程序

        查了幾天的資料,今天終於將USB攝像頭測試程序調試成功了。這個測試程序很簡單,功能就是將USB攝像頭採集的數據顯示在屏幕上。寫這個程序的目的是熟悉usb攝像頭的一些基本操作方法,爲以後在開發板上編寫視頻採集程序打好基礎。本測試程序包括兩部分:一是視頻採集部分,主要通過v4l2接口操作攝像頭,將採集的視頻幀存放在內存緩衝區。二是顯示部分,將視頻緩衝區的數據顯示到屏幕上。因爲攝像頭採集回來的數據幀爲YUV格式,不能直接顯示,需要轉換成RGB格式纔可以顯示在屏幕上。圖形界面採用GNOME桌面環境下的GTK圖形庫。程序主要參考了v4l2視頻採集例程capture.c,以及開源軟件Camorama-0.16的源代碼。程序運行效果如圖:


        本測試程序採用的攝像頭是USB攝像頭,他既屬於USB設備,又屬於視頻輸入設備。USB攝像頭在linux上要工作,首先要安裝它的驅動,還好現在大部分的USB攝像頭都是所謂的免驅攝像頭,實際上就是採用系統內置UVC驅動來工作的,UVC全稱 USB video device class,是USB設備標準的一個子類。所有支持這個標準的USB設備都可以用UVC來驅動,我採用的攝像頭就是這種免驅動攝像頭。
        Linux下對視頻音頻設備操作的接口叫video4linux,現在內核中是它的第二個版本video4linux2(v4l2)。他是內核中的一個模塊,就像input模塊一樣。v4l2對應用程序抽象了操作音頻視頻設備的細節。應用程序只需要調用v4l2接口函數就可以操作設備,無須關心是什麼樣的設備,比如在應用程序層面上,USB攝像頭與其他類型的攝像頭沒有分別。與此同時v4l2簡化了音視頻驅動程序的編寫,底層驅動只需要實現很少一部分功能,把其他的工作交給video4linux層就可以了。video4linux2與video4linux差別還是挺大的,採用video4linux接口的程序基本上是不能在v4l2下工作的。但是v4l2也向下兼容了一部分video4linux的接口。下面就簡要分析一下我的程序。
一. 主要數據結構 struct camera
struct camera {
	char *device_name;
	int fd;
	int width;
	int height;
	int display_depth;
	int image_size;
   	int frame_number;
	struct video_capability video_cap;
	struct v4l2_capability v4l2_cap;
	struct v4l2_cropcap v4l2_cropcap;
	struct v4l2_format v4l2_fmt;
	struct video_window video_win;
	struct video_picture video_pic;
	struct buffer *buffers;
	unsigned char *rgbbuf;
};
        這個結構體是我爲了方便操作攝像頭,自定義的一個結構體,主要包括了攝像頭的一些屬性,這個結構參考了camorama-0.16中相關結構。
devide_name    記錄攝像頭設備的名稱,如"/dev/video0"
fd    是設備打開後返回的文件描述符
width    攝像頭採集視頻的寬度
height    攝像頭採集視頻的高度
display_depth    顯示屏幕的分辨率,以字節爲單位,我的顯示屏爲3,也就是分辨率爲24
image_size    攝像頭採集視頻的大小,爲width*height*display_depth
frame_number    視頻緩衝區標號,在視頻採集的時候需要開闢多個緩衝區,這個表示緩衝區的個數
video_cap    是video_capability結構體,主要定義了視頻設備的一些信息,通過ioctl命令可以從設備讀出這個信息。
v4l2_cap    是v4l2_capability 結構體,同樣定義了一些視頻設備的信息與video_capability不同,他是v4l2接口的。但是我發現他缺少video_capability的一些內容,所以還是定義了video_capability  這樣兩種接口混用了,不過既然v4l2支持設備返回video_capability,這樣也沒什麼不妥。
v4l2_cropcap    是v4l2_cropcap結構體,在操作視頻緩衝區的時候使用
v4l2_fmt     是v4l2_format結構體,主要定義了視頻顯示的一些屬性
video_win    是video_window結構體,主要定義了視頻格式,如高度,寬度等
video_pic    是video_picture結構體,主要定義畫面的屬性,如亮度,灰度,飽和度等
buffers     是自定義的struct buffer結構體,包括是頻緩衝區的開始地址,以及大小
rgbbuf     視頻緩衝區指針,顯示程序就是在這裏讀取數據的。
二. 程序結構
         程序主要分爲三個部分:視頻採集,格式轉換,視頻顯示。視頻採集部分主要是操作v4l2的接口函數,對應v4l2.c以及v4l2.h文件。視頻轉換,主要是將yuv格式轉換成rgb格式,對應於yuv422_rgb.c,以及yuv422_rgb.h文件。視頻顯示,主要是利用GTK圖形庫構建圖形界面,以及將視頻數據顯示在窗口上,對應與main.c文件。程序流程圖如下:

         程序以main函數開始,首先分配struct camera結構體並初始化,然後初始化顯示緩衝區,分配內存。然後打開設備,對設備進行初始化,這部分代碼主要調用v4l2.c中的函數,如:設置設備的採集格式,採集方式,以及對設備進行mmap,初始化幀緩存等。注意v4l2讀取視頻數據有三種方式,一個就是通過普通的read操作,這個比較慢,另外一個就是使用mmap,速度比較塊,第三種方式是用戶指針,在capeture.c中,可以選擇這三種方式,而我的程序中只用了mmap的方式。初始化完設備,設備達到就緒的狀態。然後就可以初始化圖形顯示界面了,主要是建立窗口,設置屬性,定義信號鏈接函數。在這個程序中我是定義了兩個線程分別完成視頻採集和視頻顯示工作的。所以接下來要建立這兩個線程。最後,調用ioctl,打開設備的視頻採集。一切就緒後,進入GTK窗體主循環。然後兩個線程就互不干擾的分別進行視頻採集與視頻顯示了,這裏利用了一個全局變image_ready 進行兩個線程之間的同步。
三. 代碼分析 
  1. 首先從main()函數開始

int main(int argc, char **argv)
{
	/*
	 * init struct camera 
	 */
	struct camera *cam;
	
	cam = malloc(sizeof(struct camera));
        //分配內存
	if (!cam) { 
		printf("malloc camera failure!\n");
		exit(1);
	}
        cam->device_name = "/dev/video0";
        //在ubuntu下,我的攝像頭對應的就是這個設備
	cam->buffers = NULL;
	cam->width = 320;
	cam->height = 240;
        //我的攝像頭質量比較差,最大分辨率只有320*240
	cam->display_depth = 3;  /* RGB24 */
	cam->rgbbuf = malloc(cam->width * cam->height * cam->display_depth);


	if (!cam->rgbbuf) { 
		printf("malloc rgbbuf failure!\n");
		exit(1);
	}
	open_camera(cam); //打開設備
	get_cam_cap(cam); //得到設備信息,如果定義了DEBUG_CAM,則會打印視頻信息
	get_cam_pic(cam); //得到圖形信息,同樣如果定義了DEBUG_CAM,則會打印信息
	get_cam_win(cam); //得到視頻顯示信息
	cam->video_win.width = cam->width;
	cam->video_win.height = cam->height;
	set_cam_win(cam);  
        //設置圖像大小,視頻顯示信息中包括攝像頭支持的最大分辨率以及最小分辨率,這個可以設置,我設置的是320×240,當然也可以設置成其他,不過只能設置成特定的一些值
	get_cam_win(cam);
	//顯示設置之後的視頻顯示信息,確定設置成功	
        init_camera(cam);
        //初始化設備,這個函數包括很多有關v4l2的操作
        start_capturing (cam);
	//打開視頻採集

	gtk_window_init(argc,argv,cam);
        //初始化圖形顯示

	g_thread_create((GThreadFunc)draw_thread, drawingarea, FALSE, NULL);
	g_thread_create((GThreadFunc)capture_thread, cam, FALSE, NULL);
        //建立線程

	gdk_threads_enter();
	gtk_main();
	gdk_threads_leave();
        //進入主循環之後,兩個線程開始工作
  	return 0;
} 
  2. 視頻採集線程
static void capture_thread(struct camera *cam)
{
	for (;;) {
		g_usleep(10000);
		if (quit_flag == 1) break;
		gdk_threads_enter();
		fd_set fds;
		struct timeval tv;
		int r;

		FD_ZERO (&fds);
		FD_SET (cam->fd, &fds);

		/* Timeout. */
		tv.tv_sec = 2;
		tv.tv_usec = 0;

		r = select (cam->fd + 1, &fds, NULL, NULL, &tv);

		if (-1 == r) {
        		if (EINTR == errno)
                	continue;
        		errno_exit ("select");
		}

		if (0 == r) {
        		fprintf (stderr, "select timeout\n");
        		exit (EXIT_FAILURE);
		}

		if (read_frame (cam))
            		gdk_threads_leave();
		/* EAGAIN - continue select loop. */
	}	
}
        這是一個死循環,當GTK主函數進入以後一直執行,除非檢測到了退出標誌才退出循環。這裏首先用select判斷設備是否可讀,這是非阻塞的讀方式。如果設備可讀那麼select就會返回1,從而執行read_frame(cam),進行視頻數據的讀取。如果設備阻塞了,程序就退出了。這裏的關鍵函數就是read_frame(),定義在v4l2.c裏:
int read_frame(struct camera *cam)
{
        struct v4l2_buffer buf;

	CLEAR (buf);
        //這是自定義的一個宏,調用memset對內存清零

        buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
        buf.memory = V4L2_MEMORY_MMAP;
	if (quit_flag == 0) {
        	if (-1 == xioctl (cam->fd, VIDIOC_DQBUF, &buf)) {
            		switch (errno) {
            		case EAGAIN:
                	return 0;

	        	case EIO:
		/* Could ignore EIO, see spec. */

	        /* fall through */
			default:
			errno_exit ("VIDIOC_DQBUF");
			}
		}
	}

        assert (buf.index < n_buffers);

	process_image (cam->buffers[buf.index].start, cam);

	if (quit_flag == 0) {
		if (-1 == xioctl (cam->fd, VIDIOC_QBUF, &buf))
			errno_exit ("VIDIOC_QBUF");
	}
	return 1;
}
        這裏讀視頻數據採用的方法是mmap方法,就是將用內核空間的內存映射到用戶空間來用,提高了效率。在設備初始化的時候我已經用mmap映射了四塊緩衝區用來存放視頻數據,也就是說設備已經知道了將視頻數據存放到哪裏,應用程序只需要調用VIDIOC_DQBUF ioctl命令,取緩衝隊列數據,設備自然就會將視頻數據放到相應的緩衝區裏,處理完數據後,再調用VIDIOC_QBUF ioctl命令就可以了。這樣設備就會循環處理四塊緩衝區的視頻數據。
        xioctl 函數其實就是ioctl,只不過加了一些錯誤處理。process_image()是mian.c 中的函數,用來處理數據,是直接顯示,還是壓縮後存儲,以及傳輸,這取決於具體應用。這裏我是調用了格式轉換函數,直接顯示在屏幕上。
  3. 視頻顯示線程
static void draw_thread(GtkWidget *widget)
{
        for(;;) {
                g_usleep(10000);
		if (quit_flag == 1) break;
                gdk_threads_enter();
                if(image_ready) {
                        gtk_widget_queue_draw(GTK_WIDGET (widget));
                }
                else {
                        gdk_threads_leave();
                }
                gdk_threads_leave();
        }
}
        這個線程很簡單,判斷image_ready,如果被置1那麼就調用gtk_widget_queue_draw函數,觸發widget的‘expose-event’事件,從而執行相關處理函數。這個widget參數是gtk控件drawingarea,視頻就是顯示在這個控件上。在窗口初始化的時候定義了這個控件,並初始化了控件的'expose-event'事件的處理函數爲on_darea_expose(),這個函數調用了GTK提供的RGB繪圖函數gdk_draw_rgb_image()將緩衝區的內容繪製到屏幕上。
        在設備初始化之後,兩個線程通過image_ready進行同步,視頻採集進程默默的採集數據,每採回一幀數據,都會調用process_image,對數據進行處理,而process_image處理完數據後置位image_ready,然後視頻顯示線程將視頻顯示在屏幕上,同時清零image_ready,準備下一次轉換。
  4. 格式轉換函數
        攝像頭輸出的幀圖像的格式一般都是YUV格式,也就是亮度與色差的格式,如果不進行轉換,顯示到屏幕上圖像就會不對。不僅沒有色彩,而且還會有交叉。不過大體上還是可以分辨出圖像的。所以原始數據也可以作爲驗證是否採集成功。YUV格式有很多中,各種格式差別就是YUV這三個元素在內存中的排列方式,以及所佔比例不同。我的攝像頭輸出格式是YUV422類型,也就是YUYV類型。因爲無論怎麼變,一個像素YUV三個分量必不可少,而YUV422爲了節省數據量,YUV 的比例爲 2:1:1,也就是兩個Y,對應一個U與V,在內存中存放格式就是Y0 U0 Y1 V0 (每個Y,U,V 分別佔用一個字節) 這樣原本六個字節表示的兩個像素,四個字節就是表示了。
        瞭解YUV422的格式後,轉換就很簡單了,因爲YUV與RGB的轉換公式是固定了。只要在你的緩衝區裏,每隔四個字節提取出兩個像素的YUV的值,比如 Y0 U0 Y1 V0 就提取出了Y0 U0 V0 與 Y1 U0 V0 這兩個像素的值,帶入公式就轉化成了相應的RGB的值。
 * R = Y + 1.4075*(V-128)
 * G = Y - 0.3455*(U-128) - 0.7169*(V-128)
 * B = Y +1.779 *(U-128)         
        以上就是轉化公式,不過注意到上面公式都是浮點數運算,在電腦上就不用說了,直接用就可以了,因爲大部分的CPU都支持硬件浮點運算,可是在嵌入式CPU中,不一定包含硬件浮點運算,比如我用的ARM920T就不支持硬浮點運算,除法指令也沒用。所以,軟件模擬的肯定會耗費大量的CPU時間。針對這種情況,就應該用乘法與移位操作代替浮點與除法運算。於是有人開發出瞭如下算法:
 * U' = U -128
 * V' = V - 128
 * R = Y + V' + ((V'*104) >> 8))
 * G = Y - ((U'*89) >> 8) - ((V' * 183) >> 8)
 * B = Y + U' + ((U'*199) >> 8)
        這樣算出來的結果差不多,速度會比前一種算法快。下面就是我寫的採用快速算法的格式轉換程序,如果是YUV其他格式的,只需要修改少部分代碼就可以了。
#define Y0	0
#define U	1
#define Y1	2
#define	V	3

#define R	0
#define G	1
#define	B	2

int yuv422_rgb24(unsigned char *yuv_buf, unsigned char *rgb_buf, unsigned int width, unsigned int height)
{
	int yuvdata[4];
	int rgbdata[3];
	unsigned char *rgb_temp;
 	unsigned int i, j;

	rgb_temp = rgb_buf;
	for (i = 0; i < height * 2; i++) {
		for (j = 0; j < width; j+= 4) {
			/* get Y0 U Y1 V */
			yuvdata[Y0] = *(yuv_buf + i * width + j + 0);
			yuvdata[U]  = *(yuv_buf + i * width + j + 1);
			yuvdata[Y1] = *(yuv_buf + i * width + j + 2);
			yuvdata[V]  = *(yuv_buf + i * width + j + 3);

			/* the first pixel */
			rgbdata[R] = yuvdata[Y0] + (yuvdata[V] - 128) + (((yuvdata[V] - 128) * 104 ) >> 8);
			rgbdata[G] = yuvdata[Y0] - (((yuvdata[U] - 128) * 89) >> 8) - (((yuvdata[V] - 128) * 183) >> 8);
			rgbdata[B] = yuvdata[Y0] + (yuvdata[U] - 128) + (((yuvdata[U] - 128) * 199) >> 8);

			if (rgbdata[R] > 255)  rgbdata[R] = 255;		 
			if (rgbdata[R] < 0) rgbdata[R] = 0;			
			if (rgbdata[G] > 255)  rgbdata[G] = 255;		 
			if (rgbdata[G] < 0) rgbdata[G] = 0;			
			if (rgbdata[B] > 255)  rgbdata[B] = 255;		 
			if (rgbdata[B] < 0) rgbdata[B] = 0;			
			
			*(rgb_temp++) = rgbdata[R] ;
			*(rgb_temp++) = rgbdata[G];
			*(rgb_temp++) = rgbdata[B];
			
			/* the second pix */ 
			rgbdata[R] = yuvdata[Y1] + (yuvdata[V] - 128) + (((yuvdata[V] - 128) * 104 ) >> 8);
			rgbdata[G] = yuvdata[Y1] - (((yuvdata[U] - 128) * 89) >> 8) - (((yuvdata[V] - 128) * 183) >> 8);
			rgbdata[B] = yuvdata[Y1] + (yuvdata[U] - 128) + (((yuvdata[U] - 128) * 199) >> 8);
		
			if (rgbdata[R] > 255)  rgbdata[R] = 255;		 
			if (rgbdata[R] < 0) rgbdata[R] = 0;			
			if (rgbdata[G] > 255)  rgbdata[G] = 255;		 
			if (rgbdata[G] < 0) rgbdata[G] = 0;			
			if (rgbdata[B] > 255)  rgbdata[B] = 255;		 
			if (rgbdata[B] < 0) rgbdata[B] = 0;		
	
			*(rgb_temp++) = rgbdata[R];
			*(rgb_temp++) = rgbdata[G];
			*(rgb_temp++) = rgbdata[B];
		
		}
	}
	return 0;
}
四 . 總結

        這個測試程序主要參考了v4l2的例程capture.c,因爲以後要移植到開發板上,所以將操作V4L2接口的函數放到了v4l2.c這個文件中。程序還參考了開源軟件camoram-0.16,這個軟件的這個版本是v4l接口的,但是一些編程方法還是值得借鑑,新版本已經是v4l2接口的了,但是在網上下載不到源代碼,沒有辦法。程序功能比較單一,一些地方沒有優化,寫在這裏一來爲了分享,二來鞏固一下知識,三來希望高手能指點一下。

程序的全部源代碼在我的資源裏:http://download.csdn.net/detail/yaozhenguo2006/3822525 編譯通過的前提是正確安裝了相應的GTK2.0的庫。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章