基于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的库。

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