经过了一周的V4L2(video for linux two)的学习,终于用摄像头采集到视频图像了。接下来整理一下我的学习过程。

首先介绍一下开发环境:

    主机win7 + 虚拟机ubuntu + 广州斯道科技有限公司出品的Icool6410,采用交叉编译的方式,使用命令arm-linux-gcc –ccamara.c –o camtest将编译好的文件复制到win7上,再使用ftp挂载开发板(这个需要把开发板连接路由器,然后autoeth0查看路由器分配给开发板的IP,然后打开win7我的电脑运行ftp://IP,就可以挂载开发板了),把生成的执行文件camtest复制到开发板文件系统中,然后再修改文件权限chmod 777 camtest,再运行./camtest就可以生成一个out.yuv文件,再从开发板文件系统中把文件复制到win7上,然后再使用软件YUV viewer打开out.yuv文件,就可以看到摄像头采集到的图片了。

接下来介绍一下常用的结构体:

structv4l2_requestbuffers //申请帧缓冲,对应命令VIDIOC_REQBUFS
struct v4l2_capability //视频设备的功能,对应命令VIDIOC_QUERYCAP

structv4l2_input   //视频输入信息,对应命令VIDIOC_ENUMINPUT

structv4l2_standard //视频的制式,比如PAL,NTSC,对应命令VIDIOC_ENUMSTD

structv4l2_format    //帧的格式,对应命令VIDIOC_G_FMT、VIDIOC_S_FMT等

structv4l2_buffer   //驱动中的一帧图像缓存,对应命令VIDIOC_QUERYBUF

structv4l2_crop  //视频信号矩形边框

structv4l2_std_id  //视频制式

这些结构在都在include/linux/videodev2.h中定义,想详细了解这些结构体的可以去看看这个头文件。

2、常用的IOCTL接口命令也在include/linux/videodev2.h中定义

VIDIOC_REQBUFS//分配内存

VIDIOC_QUERYBUF//把VIDIOC_REQBUFS中分配的数据缓存转换成物理地址

VIDIOC_QUERYCAP//查询驱动功能

IDIOC_ENUM_FMT//获取当前驱动支持的视频格式

VIDIOC_S_FMT//设置当前驱动的频捕获格式

VIDIOC_G_FMT//读取当前驱动的频捕获格式

VIDIOC_TRY_FMT//验证当前驱动的显示格式

VIDIOC_CROPCAP//查询驱动的修剪能力

VIDIOC_S_CROP //设置视频信号的矩形边框

VIDIOC_G_CROP //读取视频信号的矩形边框

VIDIOC_QBUF //把数据从缓存中读取出来

VIDIOC_DQBUF //把数据放回缓存队列

VIDIOC_STREAMON //开始视频显示函数

VIDIOC_STREAMOFF //结束视频显示函数

VIDIOC_QUERYSTD //检查当前视频设备支持的标准,例如PAL或NTSC

3. V4L2拥有一套标准的API,大家在做应用程序的时候应该多使用官方提供的接口函数。关于视频采集是有一定的顺序的。

(1)首先要打开摄像头设备文件,这里的前提是开发板一定要有摄像头对应的驱动,若没有就需要移植相应的驱动到内核了。然后使用命令查看开发板是否有/dev/video0这个文件,若有就证明可以打开摄像头了。

使用fd = open(dev_name, O_RDWR /* required */| O_NONBLOCK, 0) 可读可写、无阻塞的的方式打开摄像头文件,则当没有可用的视频数据时,不会阻塞,而立刻返回.

(2)检查当前视频设备支持的标准

do {

       structv4l2_std_id std;ê?

       ret =ioctl(fd, VIDIOC_QUERYSTD, &std);

        } while (ret == -1 && errno ==EAGAIN);

    switch (std) {

           caseV4L2_STD_NTSC:

              printf("thedevice support NTSC\n");

           caseV4L2_STD_PAL:

              printf("thedevice support STD_PAL\n");

       }

(3)取得设备的capability 。

structv4l2_capability capability;

int ret =ioctl(fd, VIDIOC_QUERYCAP, &capability);     

if (!(cap.capabilities & V4L2_CAP_VIDEO_CAPTURE))

fprintf(stderr, "%s is no video capture device \n",dev_name);

if (!(cap.capabilities & V4L2_CAP_STREAMING))

fprintf(stderr, "%s does not support streaming i/o \n",dev_name);

看看设备具有什么功能,比如是否具有视频输入特性。

(4)查询驱动的修剪能力

if (0 == xioctl(fd, VIDIOC_CROPCAP, &cropcap)) 

(5)设置视频的捕捉格式。

在使用这些结构体的时候先要清0一下,使用memset(&x,0,sizeof(structx)).

mt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; 

fmt.fmt.pix.width = PAL_WIDTH;   图片的宽度

fmt.fmt.pix.height = PAL_HEIGHT;  //图片的高度

fmt.fmt.pix.priv = 1;

fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV;

fmt.fmt.pix.field = V4L2_FIELD_INTERLACED;

再使用

if (-1 == xioctl(fd, VIDIOC_S_FMT, &fmt)) 设置上面定义的这些格式。

(6) 向驱动申请帧缓存

struct v4l2_requestbuffers req; 

CLEAR (req); 

req.count = 4;   //申请帧缓存的数目为4

req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; 

req.memory = V4L2_MEMORY_MMAP; 

if (-1 == xioctl(fd, VIDIOC_REQBUFS, &req)) { 

        if (EINVAL== errno) { 

           fprintf(stderr, "%s does not support " 

               "memory mapping/n", dev_name); 

           exit(EXIT_FAILURE); 

        } else{ 

           errno_exit("VIDIOC_REQBUFS");

v4l2_requestbuffers结构中定义了缓存的数量,驱动会据此申请对应数量的视频缓存。多个缓存可以用于建立FIFO,来提高视频采集的效率。

(7)获取每个缓存的信息,并mmap到用户空间

struct buffer { 

    void *start; 

    size_tlength;//buffer's length is different from cap_image_size 

 };  //定义用户缓冲区

buffers = calloc(req.count,sizeof(*buffers));  申请count个用户缓冲区

for (n_buffers = 0; n_buffers< req.count; ++n_buffers) { 

 

        struct v4l2_buffer buf; 

        CLEAR (buf); 

        buf.type =V4L2_BUF_TYPE_VIDEO_CAPTURE; 

        buf.memory = V4L2_MEMORY_MMAP;   //定义使用mmap映射方式

        buf.index = n_buffers; 

        if (-1 == xioctl(fd, VIDIOC_QUERYBUF,&buf))   //申请内核缓存区间,获取到对应的index的缓存信息,此处主要利用length信息及offset信息来完成后面的mmap操作。

           errno_exit("VIDIOC_QUERYBUF"); 

        buffers[n_buffers].length =buf.length;  //转换成相对地址

        buffers[n_buffers].start = mmap(NULL /*start anywhere */, buf.length, 

                PROT_READ | PROT_WRITE /*required */, 

                MAP_SHARED /* recommended */,fd, buf.m.offset);   //mmap()返回内核区间映射到用户区间的首地址。

(8)开始视频采集

static void start_capturing(void){

    unsigned int i;

    enum v4l2_buf_type type;

    for(i = 0;i < n_buffers;++i){

       struct v4l2_buffer buf;

       CLEAR(buf);

       buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; 

           buf.memory = V4L2_MEMORY_MMAP; 

           buf.index = i;

           if (-1 == xioctl(fd, VIDIOC_QBUF, &buf)) 

                  errno_exit("VIDIOC_QBUF"); 

           type= V4L2_BUF_TYPE_VIDEO_CAPTURE; 

           if(-1 == xioctl(fd, VIDIOC_STREAMON, &type))  //开始视频采集

                  errno_exit("VIDIOC_STREAMON");   

       }

}

(9)读取缓存中的内容

static int read_frame(void){

    struct v4l2_buffer buf;

    unsigned int i;

    CLEAR(buf);

    buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;   //查询功能

    buf.memory= V4L2_MEMORY_MMAP;

    if(-1 == xioctl(fd,VIDIOC_DQBUF,&buf)){     //将缓冲出队列

       errno_exit("VIDIOC_DQBUF");

    }

    assert(buf.index < n_buffers); //检查下标

    //      printf("length= %d/r", buffers[buf.index].length); 

            process_image(buffers[i].start,cap_image_size );  //视频处理函数,此函数中可以把视频数据取出来。

    printf("image_size = %d, IO_METHOD_MMAP buffer.length=%d\n",cap_image_size, buffers[buf.index].length);

if(-1 ==xioctl(fd,VIDIOC_QBUF,&buf))    //将刚刚处理完的缓冲重新入队列尾,这样可以循环采集{

    errno_exit("VIDIOC_QBUF");

    }

}

视频处理函数

static void process_image(constvoid *p,int len){      

 fwrite(p, len, 1, file_fd);       //从指针p所指的地址写1个len大小的字节到file_fd所指的文件中

     printf("from %p write%d bytes to file_fd\n",p,len);

 }  //这里也可以对视频数据作别的处理。

(10)停止视频的采集

static voidstop_capturing(void){

    enum v4l2_buf_type type;

    type = V4L2_BUF_TYPE_VIDEO_CAPTURE; 

    if (-1 == xioctl(fd, VIDIOC_STREAMOFF, &type))    //停止视频采集

   errno_exit("VIDIOC_STREAMOFF"); 

}

(11) 释放用户区申请的缓存

static voiduninit_device(void){

unsigned int i;

    for(i = 0;i < n_buffers;i++)

       if(-1 == munmap(buffers[i].start,buffers[i].length))  //撤销map映射

       errno_exit("munmap");

    free(buffers);      //释放用户区申请的缓存

}

(12)关闭视频设备

static void close_device(void){ 

    if (-1 == close(fd)) 

        errno_exit("close");  

    fd = -1; 

另外贴一下mainloop()的代码。

static void mainloop(void){

    unsigned int count;

    count = 50;            //由count控制画面的数目

    while(count-- > 0){

       for(;;){

       fd_set fds;

       struct timeval tv;

       int r;

       FD_ZERO(&fds);    //将指定的文件描述符集清空

       FD_SET(fd,&fds)   ;//在文件描述符集合中增加一个新的文件描述符

       /* Timeout. */ 

             tv.tv_sec= 3; 

              tv.tv_usec= 0; 

       r =select(fd + 1, &fds, NULL, NULL, &tv);  //判断是否可读(即摄像头是否准备好),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()) ; //如果可读,执行read_frame()函数,并跳出循环,这个read_frame()在此用的很巧。

                break; 

       }

    }

}

图一 yuv格式图像

另外再自己还实现了yuv格式的图像转换成jpg格式的图像。

首先得先在ubuntu下安装libjpeg,直接上网站下载后,运行./configure--enable-static --enable-shared --prefix=/usr 这个命令让libjpeg在/usr下建立共享和静止的两种库。再make && makeinstall 等待就安装成功了,若在之后的程序中显示lib不匹配错误,多半是由于lib冲突的问题,把老库删除再重新安装下问题一般得到解决,实在不行的上谷歌找找,#defineJPEG_LIB_VERSION  62    /* Version 6b */ 程序运行出现需要62 但是库的版本为80,也就是库新了,另外在makefile里要注意加上-ljpeg选项。还有一个重要的地方,应用程序一定要记得加#include</usr/include/jpeglib.h>,或者是jpeglib的其他安装地方。总之就是要把jpeglib.h添加进去。我就弄了一下午才搞定,汗。

之后就是把yuv格式图像转化成jpg格式图像了,有两种方法,一种是直接转化,另外一种是先转化成rgb格式再转化成yuv格式。

压缩步骤:

1、申请并初始化jpeg压缩对象,同时要指定错误处理器

struct jpeg_compress_structjcs;// 声明错误处理器,并赋值给jcs.err域
struct jpeg_error_mgr jem;
jcs.err = jpeg_std_error(&jem);

jpeg_create_compress(&jcs);

2、指定压缩后的图像所存放的目标文件,注意,目标文件应以二进制模式打开

f=fopen("03.jpg","wb"); //图像保存在03.jpg下

jpeg_stdio_dest(&jcs, f);

3、设置压缩参数,主要参数有图像宽、高、色彩通道数(1:索引图像,3:其他),色彩空间(JCS_GRAYSCALE表示灰度图,JCS_RGB表示彩色图像),压缩质量等,如下:

jcs.image_width = nWidth; // 为图的宽和高,单位为像素
jcs.image_height = nHeight;
jcs.input_components = 3; // 在此为3,表示彩色位图, 如果是灰度图,则为1
jcs.in_color_space = JCS_GRAYSCALE; //JCS_GRAYSCALE表示灰度图,JCS_RGB表示彩色图像

jpeg_set_defaults(&jcs);
jpeg_set_quality (&jcs, 80, true);

需要注意的是,jpeg_set_defaults函数一定要等设置好图像宽、高、色彩通道数计色彩空间四个参数后才能调用,因为这个函数要用到这四个值,调用jpeg_set_defaults函数后,jpeglib库采用默认的设置对图像进行压缩,如果需要改变设置,如压缩质量,调用这个函数后,可以调用其它设置函数,如jpeg_set_quality函数。其实图像压缩时有好多参数可以设置,但大部分我们都用不着设置,只需调用jpeg_set_defaults函数值为默认值即可。

4、上面的工作准备完成后,就可以压缩了,压缩过程非常简单,首先调用jpeg_start_compress,然后可以对每一行进行压缩,也可以对若干行进行压缩,甚至可以对整个的图像进行一次压缩,压缩完成后,记得要调用jpeg_finish_compress函数,如下:

jpeg_start_compress(&jcs,TRUE);

JSAMPROW row_pointer[1]; // 一行位图
int row_stride; // 每一行的字节数

row_stride = jcs.image_width;// 如果不是索引图,此处需要乘以3

// 对每一行进行压缩
while (jcs.next_scanline < jcs.image_height) {
row_pointer[0] = & pDataConv[jcs.next_scanline * row_stride];
jpeg_write_scanlines(&jcs, row_pointer, 1);
}

jpeg_finish_compress(&jcs);

5、最后就是释放压缩工作过程中所申请的资源了,主要就是jpeg压缩对象,由于在本例中我是直接用的局部变量,所以只需调用jpeg_destroy_compress这个函数即可,如下:

jpeg_destroy_compress(&jcs);

YUV转换JPG算法如下:
static void jpegWrite(unsigned char* img)
{
struct jpeg_compress_struct cinfo;
struct jpeg_error_mgr jerr;
JSAMPROW row_pointer[1];
//char name[4]={'a','b','c','x'};
FILE *outfile = fopen( jpegFilename, "wb" );
// try to open file for saving
if (!outfile) {
errno_exit("jpeg");
}
// create jpeg data
cinfo.err = jpeg_std_error( &jerr );
jpeg_create_compress(&cinfo);
jpeg_stdio_dest(&cinfo, outfile);
// set image parameters
cinfo.image_width = width;
cinfo.image_height = height;
cinfo.input_components = 3;
cinfo.in_color_space = JCS_RGB;
// set jpeg compression parameters to default
jpeg_set_defaults(&cinfo);
// and then adjust quality setting
jpeg_set_quality(&cinfo, jpegQuality, TRUE);
// start compress
jpeg_start_compress(&cinfo, TRUE);
// feed data
while (cinfo.next_scanline < cinfo.image_height) {
row_pointer[0] = &img[cinfo.next_scanline * cinfo.image_width *
cinfo.input_components];
jpeg_write_scanlines(&cinfo, row_pointer, 1);
}
// finish compression
jpeg_finish_compress(&cinfo);
// destroy jpeg data
jpeg_destroy_compress(&cinfo);
// close output file
fclose(outfile);
}
/**
process image read
*/
static void imageProcess(const void* p)
{
unsigned char* src = (unsigned char*)p;
unsigned char* dst = malloc(width*height*3*sizeof(char));
// convert from YUV422 to RGB888
YUV422toRGB888(width,height,src,dst);
// write jpeg
jpegWrite(dst);
}
static void YUV422toRGB888(int width, int height, unsigned char *src, unsignedchar *dst)
{
int line, column;
unsigned char *py, *pu, *pv;
unsigned char *tmp = dst;
/* In this format each four bytes is two pixels. Each four bytes is two Y's, aCb
and a Cr.Each Y goes to one of the pixels, and the Cb and Cr belong to bothpixels. */
py = src;
pu = src + 1;
pv = src + 3;
#define CLIP(x) ( (x)>=0xFF ? 0xFF : ( (x) <= 0x00 ? 0x00 : (x) ) )
for (line = 0; line < height; ++line) {
for (column = 0; column < width; ++column) {
*tmp++ = CLIP((double)*py + 1.402*((double)*pv-128.0));
*tmp++ = CLIP((double)*py - 0.344*((double)*pu-128.0) -0.714*((double)*pv-128.0));
*tmp++ = CLIP((double)*py + 1.772*((double)*pu-128.0));
// increase py every time
py += 2;
// increase pu,pv every second time
if ((column & 1)==1) {
pu += 4;
pv += 4;
}
}
}
}

 

图二 转成jpg格式的图片

完整代码有好几百行,就不贴上来了

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐