video4linux(v4l) 使用摄像头的实例基础教程与体会
作者: d_south<d_south@163.com>
Blog : http://hi.baidu.com/d_south
写作日期: 2009
前言. 写这篇文章的想法和思路
由于毕业设计的关系,本人要做一下在 linux 系统中视频的相关工作比如采集和传输。由于本人是菜鸟一个,所以是需要上网搜一搜看大家都是如何做的,当然开始都是理不出一个头绪,但是很多文章都提到了 video4linux(v4l) ,所以我觉得工作的展开可以先从这里开始,。看了网上的一些文章,其中比较重要的也是比较知名的吧,有戴小鼠写的《基于 Video4Linux 的 USB 摄像头图像采集实现》,有陈俊宏写的《 video stream 初探》的一系列共六篇文章,也找了一些英文的资料,看到过《 video4linux programming 》但是这篇文章偏重于视频设备在 linux 中的驱动实现,所以对像我这种低端的只是使用 v4l 相关系统调用的人来说有些帮助但帮助不大,《 Video4Linux Kernel API Reference 》详细介绍了 v4l 中各个重要的结构体的作用。另外顺着陈俊宏的文章,找到了一个叫 EffecTV 的软件,其中的有关 v4l 的源码部分也很值得一看,在后的文章里也会介绍。翻看了网上的很多文章,多半是使用陈俊宏介绍的相关代码,或者是 EffecTV 中的,大家都是这么用而且也都用的不错。
我写这个文章一是想为自己的毕业论文积累些素材,二是我想可能会给今后想要了解 v4l 相关使用知识的人提供一个学习的路线,因为上一段中提到的几篇文章无论谁读起来肯定都会对他有很大的帮助,三是希望我也写篇文章给想学习的人一点帮助,哪怕只有一点点。
文章就分成三个大部分吧:
第一个部分介绍一些 v4l 的基本概念和基本方法,利用系统 API 完成一系列函数以方便后续应用程序的开发和使用。
第二个部分一些说明如何使用 v4l ,用一个示例程序说明。
第三个部分想简单说一说对获取和处理图像相关问题的思路。在这一章可能会谈一谈我的一些理解和体会。其实网络上的资料很多,我只是稍微整理一下而已。
我的感觉 linux 内核和驱动开发的那些程序员很厉害因为他们留给我们一个很容易使用的接口而使底层复杂的工作对我们很透明,读过上述我提到的文章后会觉得使用 v4l 是 相对容易的(我希望如果有人读了我的文章也会有这种感觉),相对复杂的是采集到图像数据后我们应该怎么办,我想这也可能是很多人当然也包括我所不是特别清 晰和明确的。所以我想在第三个部分里做一些对采集到图像数据后相关问题的探讨,当然我的水平有限,请您指出文中的错误方法和对概念的错误理解,我非常愿意 共同学习和进步。
1. video4linux 基础相关
1.1 v4l 的介绍与一些基础知识的介绍
I. 首先说明一下 video4linux(v4l) 。
它是一些视频系统,视频软件,音频软件的基础,经常使用在需要采集图像的场合,如视频监控, webcam, 可视电话,经常应用在 embedded linux 中是 linux 嵌入式开发中经常使用的系统接口。它是 linux 内核提供给用户空间的编程接口,各种的视频和音频设备开发相应的驱动程序后,就可以通过 v4l 提供的系统 API 来控制视频和音频设备,也就是说 v4l 分为两层,底层为音视频设备在内核中的驱动,上层为系统提供的 API ,而对于我们来说需要的就是使用这些系统的 API 。
II.Linux 系统中的文件操作
有关 Linux 系统中的文件操作不属于本文的内容。但是还是要了解相关系统调用的作用和使用方法。其中包括 open() , read() , close() , ioctl() , mmap() 。详细的使用不作说明。在 Linux 系统中各种设备(当然包括视频设备)也都是用文件的形式来使用的。他们存在与 dev 目录下,所以本质上说,在 Linux 中各种外设的使用(如果它们已经正确的被驱动),与文件操作本质上是没有什么区别的。
1.2 建立一套简单的v4l 函数库
这一节将一边介绍 v4l 的使用方法,一边建立一套简单的函数,应该说是一套很基本的函数,它完成很基本的够能但足够展示如何使用 v4l 。这些函数可以用来被其他程序使用,封装基本的 v4l 功能。本文只介绍一些和摄像头相关的编程方法,并且是最基础 和最简单的 ,所以一些内容并没有介绍,一些与其他视频设备(如视频采集卡)和音频设备有关的内容也没有介绍,本人也不是很理解这方面的内容。
这里先给出接下来将要开发出来函数的一个总览。
相关结构体和函数的定义我们就放到一个名为 v4l.h 的文件中,相关函数的编写就放在一个名为 v4l.c 的文件中把。
对于这个函数库共有如下的定义(也就是大体 v4l.h 中的内容):
#ifndef _V4L_H_
#define _V4L_H_
#include <sys/types.h>
#include <linux/videodev.h> // 使用 v4l 必须包含的头文件
这个头文件可以在 /usr/include/linux 下找到,里面包含了对 v4l 各种结构的定义,以及各种 ioctl 的使用方法,所以在下文中有关 v4l 的相关结构体并不做详细的介绍,可以参看此文件就会得到你想要的内容。
下面是定义的结构体,和相关函数,突然给出这么多的代码很唐突,不过随着一点点解释条理就会很清晰了。
struct _v4l_struct
{
int fd;// 保存打开视频文件的设备描述符
struct video_capability capability;// 该结构及下面的结构为 v4l 所定义可在上述头文件中找到
struct video_picture picture;
struct video_mmap mmap;
struct video_mbuf mbuf;
unsigned char *map;// 用于指向图像数据的指针
int frame_current;
int frame_using[VIDEO_MAXFRAME];// 这两个变量用于双缓冲在后面介绍。
};
typedef struct _v4l_struct v4l_device;
// 上面的定义的结构体,有的文中章有定义 channel 的变量,但对于摄像头来说设置这个变量意义不大通常只有一个 channel ,本文不是为了写出一个大而全且成熟的函数库,只是为了介绍如何使用 v4l ,再加上本人水平也有限,能够给读者一个路线我就很知足了 , 所以并没有设置这个变量同时与 channel 相关的函数也没有给出。
extern int v4l_open(char *, v4l_device *);
extern int v4l_close(v4l_device *);
extern int v4l_get_capability(v4l_device *);
extern int v4l_get_picture(v4l_device *);
extern int v4l_get_mbuf(v4l_device *);
extern int v4l_set_picture(v4l_device *, int, int, int, int, int,);
extern int v4l_grab_picture(v4l_device *, unsigned int);
extern int v4l_mmap_init(v4l_device *);
extern int v4l_grab_init(v4l_device *, int, int);
extern int v4l_grab_frame(v4l_device *, int);
extern int v4l_grab_sync(v4l_device *);
上述函数会在下文中逐渐完成,功能也会逐渐介绍,虽然现在看起来没什么感觉只能从函数名上依稀体会它的功能,或许看起来很烦,不过看完下文就会好了。
前面已经说过使用 v4l 视频编程的流程和对文件操作并没有什么本质的不同,大概的流程如下:
1. 打开视频设备 ( 通常是 /dev/video0)
2. 获得设备信息。
3. 根据需要更改设备的相关设置。
4. 获得采集到的图像数据(在这里 v4l 提供了两种方式,直接通过打开的设备读取数据,使用 mmap 内存映射的方式获取数据)。
5. 对采集到的数据进行操作(如显示到屏幕,图像处理,存储成图片文件)。
6. 关闭视频设备。
知道了流程之后,我们就需要根据流程完成相应的函数。
那么我们首先完成第 1 步打开视频设备 ,需要完成 int v4l_open(char *, v4l_device *);
具体的函数如下
#define DEFAULT_DEVICE “/dev/video0”
int v4l_open(char *dev , v4l_device *vd)
{
if(!dev)dev= DEFAULT_DEVICE;
if((vd-fd=open(dev,O_RDWR))<0){perror(“v4l_open:”);return -1;}
if(v4l_get_capability(vd))return -1;
if(v4l_get_picture(vd))return -1;// 这两个函数就是即将要完成的获取设备信息的函数
return 0
}
同样对于第 6 步也十分简单 ,就是 int v4l_close(v4l_device *); 的作用。
函数如下:
int v4l_close(v4l_device *vd)
{close(vd->fd);return 0;}
现在我们完成第 2 步中获得设备信息的任务 ,下面先给出函数在对函数作出相应的说明。
int v4l_get_capability(v4l_device *vd)
{
if (ioctl(vd->fd, VIDIOCGCAP, &(vd->capability)) < 0) {
perror("v4l_get_capability:");
return -1;
}
return 0;
}
int v4l_get_picture(v4l_device *vd)
{
if (ioctl(vd->fd, VIDIOCGPICT, &(vd->picture)) < 0) {
perror("v4l_get_picture:");
return -1;
}
return 0;
}
对于以上两个函数我们不熟悉的地方可有 vd->capability 和 vd->picture 两个结构体,和这两个函数中最主要的语句 ioctl 。对于 ioctl 的行为它是由驱动程序提供和定义的,在这里当然是由 v4l 所定义的,其中宏 VIDIOCGCAP 和 VIDIOCGPICT 的分别表示获得视频设备的 capability 和 picture 。对于其他的宏功能定义可以在你的 Linux 系统中的 /usr/include/linux/videodev.h 中找到,这个头文件也包含了 capability 和 picture 的定义。例如:
struct video_capability
{
char name[32];
int type;
int channels; /* Num channels */
int audios; /* Num audio devices */
int maxwidth; /* Supported width */
int maxheight; /* And height */
int minwidth; /* Supported width */
int minheight; /* And height */
};capability 结构它包括了视频设备的名称,频道数,音频设备数,支持的最大最小宽度和高度等信息。
struct video_picture
{
__u16 brightness;
__u16 hue;
__u16 colour;
__u16 contrast;
__u16 whiteness; /* Black and white only */
__u16 depth; /* Capture depth */
__u16 palette; /* Palette in use */
}picture 结构包括了亮度,对比度,色深,调色板等等信息。头文件里还列出了 palette 相关的值,这里并没有给出。
了解了以上也就了解了这两个简单函数的作用,现在我们已经获取到了相关视频设备的 capabilty 和 picture 属性。
这里直接给出另外一个函数
int v4l_get_mbuf(v4l_device *vd)
{
if (ioctl(vd->fd, VIDIOCGMBUG ,&(vd->mbuf)) < 0) {
perror("v4l_get_mbuf:");
return -1;
}
return 0;
}
对于结构体 video_mbuf 在 v4l 中的定义如下, video_mbuf 结构体是为了服务使用 mmap 内存映射来获取图像的方法而设置的结构体,通过这个结构体可以获得摄像头设备存储图像的内存大小。具体的定义如下,各变量的使用也会在下文详细说明。
struct video_mbuf
{
int size; 可映射的摄像头内存大小
int frames; 摄像头可同时存储的帧数
int offsets[VIDEO_MAX_FRAME]; 每一帧图像的偏移量
};
下面完成第 3 步按照需要更改设备的相应设置, 事实上可以更改的设置很多,本文以更改 picture 属性为例说明更改属性的一般方法。
那么我们就完成 extern int v4l_set_picture(v4l_device *, int, int, int, int, int,); 这个函数吧
int v4l_set_picture(v4l_device *vd,int br,int hue,int col,int cont,int white)
{
if(br) vd->picture.brightnesss=br;
if(hue) vd->picture.hue=hue;
if(col) vd->picture.color=col;
if(cont) vd->picture.contrast=cont;
if(white) vd->picture.whiteness=white;
if(ioctl(vd->fd,VIDIOCSPICT,&(vd->picture))<0)
{perror("v4l_set_picture: ");return -1;}
return 0;
}
上述函数就是更改 picture 相关属性的例子,其核心还是 v4l 给我们提供的 ioctl 的相关调用,通过这个函数可以修改如亮度,对比度等相关的值。
第 4 步获得采集到的图像数据。
这一步是使用 v4l 比较重要的一步,涉及到几个函数的编写。当然使用 v4l 就是为了要获得图像,所以这一步很关键,但是当你获得了图像数据后,还需要根据你想要达到的目的和具体情况做进一步的处理,也就是第 5 步所做的事情,这些内容将在后面第三部分提到。这里讲如何获得采集到的数据。
如前所述获得图像的方式有两种,分别是直接读取设备 和使用 mmap 内存映射 ,而通常大家使用的方法都是后者。
1 ) . 直接读取设备
直接读设备的方式就是使用 read() 函数,我们先前定义的
extern int v4l_grab_picture(v4l_device *, unsigned int); 函数就是完成这个工作的,它的实现也很简单。
int v4l_grab_picture(v4l_device *vd, unsighed int size)
{
if(read(vd-fd,&(vd->map),size)==0)return -1;
return 0 ;
}
该函数的使用也很简单,就是给出图像数据的大小, vd->map 所指向的数据就是图像数据。而图像数据的大小你要根据设备的属性自己计算获得。
2 ) . 使用 mmap 内存映射来获取图像
在这部分涉及到下面几个函数,它们配合来完成最终图像采集的功能。
extern int v4l_mmap_init(v4l_device *); 该函数把摄像头图像数据映射到进程内存中,也就是只要使用 vd->map 指针就可以使用采集到的图像数据(下文详细说明)
extern int v4l_grab_init(v4l_device *, int, int); 该函数完成图像采集前的初始化工作。
extern int v4l_grab_frame(v4l_device *, int); 该函数是真正完成图像采集的一步,在本文使用了一个通常都会使用的一个小技巧,可以在处理一帧数据时同时采集下一帧的数据,因为通常我们使用的摄像头都可以至少存储两帧的数据。
extern int v4l_grab_sync(v4l_device *); 该函数用来完成截取图像的同步工作,在截取一帧图像后调用,返回表明一帧截取结束。
下面分别介绍这几个函数。
mmap() 系统调用使得进程之间通过映射同一个普通文件实现共享内存。普通文件被映射到进程地址空间后,进程可以像访问普通内存一样对文件进行访问,不必在调用 read() , write() 等操作。两个不同进程 A 、 B 共享内存的意思是,同一块物理内存被映射到进程 A 、 B 各自的进程地址空间。进程 A 可以即时访问进程 B 对共享内存中数据的更新,反之亦然。
采用共享内存通信的一个显而易见的好处是减少 I/O 操作提高读取效率,因为使用 mmap 后进程可以直接读取内存而不需要任何数据的拷贝。
mmap 的函数原型如下
void* mmap ( void * addr , size_t len , int prot , int flags , int fd , off_t offset )
addr :共享内存的起始地址,一般设为 0 ,表示由系统分配。
len: 指定映射内存的大小。在我们这里,该值为摄像头 mbuf 结构体的 size 值,即图像数据的总大小。
port :指定共享内存的访问权限 PROT_READ( 可读 ) , PROT_WRITE (可写)
flags :一般设置为 MAP_SHARED
fd :同享文件的文件描述符。
介绍完了 mmap 的使用,就可以介绍上文中定义的函数 extern int v4l_mmap_init(v4l_device *); 了。先给出这个函数的代码,再做说明。
int v4l_mmap_init(v4l_device *vd)
{
if (v4l_get_mbuf(vd) < 0)
return -1;
if ((vd->map = mmap(0, vd->mbuf.size, PROT_READ|PROT_WRITE, MAP_SHARED, vd->fd, 0)) < 0) {
perror("v4l_mmap_init:mmap");
return -1;
}
return 0;
}
这个函数首先使用 v4l_get_mbuf(vd) 获得一个摄像头重要的参数,就是需要映射内存的大小,即 vd->mbuf.size ,然后调用 mmap ,当我们在编程是调用 v4l_mmap_init 后, vd.map 指针所指向的内存空间即为我们将要采集的图像数据。
获得图像前的初始化工作 v4l_grab_init(); 该函数十分简单直接粘上去,其中将。 vd->frame_using[0] 和 vd->frame_using[1] 都设为 FALSE ,表示两帧的截取都没有开始。
int v4l_grab_init(v4l_device *vd, int width, int height)
{
vd->mmap.width = width;
vd->mmap.height = height;
vd->mmap.format = vd->picture.palette;
vd->frame_current = 0;
vd->frame_using[0] = FALSE;
vd->frame_using[1] = FALSE;
return v4l_grab_frame(vd, 0);
}
真正获得图像的函数 extern int v4l_grab_frame(v4l_device *, int);
int v4l_grab_frame(v4l_device *vd, int frame)
{
if (vd->frame_using[frame]) {
fprintf(stderr, "v4l_grab_frame: frame %d is already used./n", frame);
return -1;
}
vd->mmap.frame = frame;
if (ioctl(vd->fd, VIDIOCMCAPTURE, &(vd->mmap)) < 0) {
perror("v4l_grab_frame");
return -1;
}
vd->frame_using[frame] = TRUE;
vd->frame_current = frame;
return 0;
}
读到这里,应该觉得这个函数也是相当的简单。最关键的一步即为调用 ioctl(vd->fd, VIDIOCMCAPTURE, &(vd->mmap)) ,调用后相应的图像就已经获取完毕。其他的代码是为了完成双缓冲就是截取两帧图像用的,可以自己理解下。
在截取图像后还要进行同步操作,就是调用 extern int v4l_grab_sync(v4l_device *); 函数,该函数如下
int v4l_grab_sync(v4l_device *vd)
{
if (ioctl(vd->fd, VIDIOCSYNC, &(vd->frame_current)) < 0) {
perror("v4l_grab_sync");
}
vd->frame_using[vd->frame_current] = FALSE;
return 0;
}
该函数返回 0 说明你想要获取的图像帧已经获取完毕。
图像存在了哪里?
最终我们使用 v4l 的目的是为了获取设备中的图像,那么图像存在哪里?从上面的文章可以知道, vd.map 指针所指就是你要获得的第一帧图像。图像的位置,存在 vd.map+vd.mbuf.offsets[vd.frame_current] 处。其中 vd.frame_current=0 ,即为第一帧的位置, vd.frame_current=1 ,为第二帧的位置。
2 上述 v4l 库使用的方法
给出了上述的一些代码,这里用一些简单的代码表明如何来使用它。上文中已经说过将相关结构体和函数的定义放到一个名为 v4l.h 的文件中,相关函数的编写放在一个名为 v4l.c 的文件。
现在我们要使用它们。
使用的方法很简单,你创建一个 .c 文件,假设叫 test.c 吧,那么 test.c 如下
//test.c
include “v4l.h”
...
v4l_device vd;
void main()
{
v4l_open(DEFAULT_DEVICE,&vd);
v4l_mmap_init(&vd);
v4l_grab_init(&vd,320,240);
v4l_grab_sync(&vd);// 此时就已经获得了一帧的图像,存在 vd.map 中
while(1)
{
vd.frame_current ^= 1;
v4l_grab_frame(&vd, vd.frame_current);
v4l_grab_sync(&vd);
图像处理函数( vd.map+vd. vd.map+vd.mbuf.offsets[vd.frame_current] );
// 循环采集,调用你设计的图像处理函数来处理图像
// 其中 vd.map+vd. vd.map+vd.mbuf.offsets[vd.frame_current] 就是图像所在位置。
}
}
3 有关获取的图像的一些问题
问 :我获取到的图像究竟长什么样?
答 :每个摄像头获取的图像数据的格式可能都不尽相同,可以通过 picture. palette 获得。获得的图像有黑白的,有 yuv 格式的, RGB 格式的,也有直接为 jpeg 格式的。你要根据实际情况,和你的需要对图像进行处理。比如常见的,如果你要在嵌入式的 LCD 上显示假设 LCD 是 RGB24 的,但是你获得图像是 YUV 格式的那么你就将他转换为 RGB24 的。具体的转换方法可以上网查找,也可参考前面提到过的 effectTV 中的相关代码。
问 :如何显示图像或将图像保存?
答 :假设你采集到的图像为 RGB24 格式的,我接触过的可以使用 SDL 库显示(网络上很流行的叫 spcaview 的软件就是这样的,不过它将图像数据压缩为 jpeg 的格式后显示,这个软件也被经常的移植到一些嵌入式平台使用,如 ARM 的)。当然也可以使用嵌入式 linux 的 Framebuffer 直接写屏显示。将图像保存可以用 libjpeg 将其保存为 jpeg 图片直接存储,相关的使用方法可以上网查找。也可以使用一些视频编码,将其编码保存(我希望学习一下相关的技术因为我对这方面一点不懂,如果你有一些资料可以推荐给我看,我十分想看一看)。
一边写文章一边才发现自己很菜,因为很多都是参考别人的文章,而自己想写出来去一落键盘就写不出什么。就写这么多,因为我只会这么多。高手见笑,新手和我一样我们互相讨论