基于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*240cam->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   2int 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的库。

基于GTK的USB视频采集程序相关推荐

  1. 基于MFC的Basler相机采集程序

    基于MFC的Basler相机采集程序 编程软件VS2015 相机Basler acA4024-8gc 本文采用了opencv3.4.10,并且采用了opencv1.0版本时代的CvvImage类,其对 ...

  2. 关于Linux平台视频采集程序的修改

    在真正开始编写程序前,我作了大量工作,包括了解Linux支持的摄像头类型.Linux摄像头驱动程序.摄像头采集模型,等等,--当然,还包括去买个摄像头.网上流行的是gspca驱动以及一些老式摄像头的程 ...

  3. 源码免费下载!分享一套基于C6678+K7的视频采集处理方案

    1.为什么说DSP+FPGA架构更适合视频采集处理? 高性能的算法处理,使用硬件描述语言去编写算法的话,复杂程度高,工作量大,不易调试,产品的开发周期将非常长.这时,如果为系统添加专为算法而生的DSP ...

  4. FPGA学习——基于zynq的图像视频采集处理

    图1:一个典型的基于zynq系统的图像处理框架 根据系统读取外界 视 频 数 据 信 息 的 数 据 流 向,从 软 硬件功能层面具体划分为: 1)采集 部 分:视 频 通 过 位 于PL 侧 的50 ...

  5. 基于STM32的USB在线下载程序说明

    上位机通过USB下载和更新STM32固件使用说明 一. 实现功能 通过计算机应用程序在线对单片机程序下载和更新. 二. 词汇说明 BootLoader是指实现在线下载的单片机固件程序. 固件主程序是指 ...

  6. 基于嵌入式linux和s32410平台的视频采集

    随着多媒体技术.网络技术的迅猛发展和后PC机时代的到来,利用嵌入式系统实现远程视频监控.可视电话和视频会议等应用已成为可能.为了实现这些应用,实时获得视频数据是一个重要环节.针对这一点,本文在基于嵌入 ...

  7. Video4Linux下USB摄像头驱动和视频采集的实现

    1 引言 多媒体通信技术的发展为信息的获取和传输提供了丰富的手段,视频采集是其中不可缺少的重要组成部分.视频采集的手段多种多样,随着人们对降低系统成本和提高可靠性的迫切需求,基于嵌入式的视频采集系统成 ...

  8. 基于FFMPEG 的跨平台视频编解码研究

    第33卷 第11期 2011年11月 武 汉 理 工 大 学 学 报 JOURNALOF WUHANUNIVERSITYOFTECHNOLOGY Vol.33 No.11 췍췍췍췍췍췍췍췍췍췍췍췍췍췍 ...

  9. 基于Video4Linux 的USB 摄像头图像采集实现

    最近一直在做一个摄像头视频采集的程序,遇到了很多艰辛的问题,下边这是一个好文章,粘过来供大家也供自己能对添加驱动以及摄像头视频采集 程序的书写有一定的帮助,同时谢谢博主能提供这样一份很好的文章. 做了 ...

最新文章

  1. 中国知网PCNI号码
  2. 计算机学机械制图吗,机械制图与计算机绘图(少学时·任务驱动模式)
  3. html5倒计时秒杀怎么做,vue 设计一个倒计时秒杀的组件
  4. java ssl证书生成_java – 使用jdk中提供的keytool生成SSL证书
  5. jQuery控制表格垂直滚动条
  6. 单行文字不断向上滚动特效
  7. 强大的DataGrid组件[7]_自定义DataGrid——Silverlight学习笔记[15]
  8. 《WTM送书活动:向更遥远的星辰大海起航~》
  9. css用hover制作下拉菜单
  10. 面试题 02.03. 删除中间节点
  11. iPhone应用程序编程指南(文本和Web)
  12. idea中.ignore插件的使用
  13. Ubuntu16.04安装QQ机器人
  14. Exchange系列—群集连续复制配置
  15. DIV+CSS网页设计布局应用详解视频教程
  16. 音视频学习系列第(四)篇---视频的采集预览
  17. linux socket 程序被ctrl+c或者异常终止,提示:bind error:Address already in use,解决办法...
  18. linux系统如何备份系统软件,Linux系统如何备份
  19. CDN (Content Delivery Network 内容分发网络)
  20. iPhone所有屏幕分辨率

热门文章

  1. Markdown copy and paste
  2. 收藏的JAVA面试题大全(http://www.blogjava.net/bibi/archive/2006/07/18/58701.html)
  3. 编译qemu-kvm的RPM安装包
  4. html原生js进度条圆形,原生 JavaScript 实现进度条
  5. 普冉PY32系列(二) Ubuntu GCC Toolchain和VSCode开发环境
  6. python抓取qq群消息_Python获取统计自己的qq群成员信息的方法
  7. 课堂作业--密码强度判断
  8. Servlet运行原理图解
  9. 2021-12-09 使用kali生成木马,渗透Windows系统
  10. 使用PostGIS+GeoServer+Openlayer+Vue构建简单的web地图应用