查了几天的资料,今天终于将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;
- };
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;
- }
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. */
- }
- }
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;
- }
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();
- }
- }
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;
- }
#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的库。
原文:http://blog.csdn.net/yaozhenguo2006/article/details/6996931