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