最近放假,比较清闲,正好手上有一个USB的免驱摄像头,想了想打算做一个简易的照相机,后期移植到4412的板子上做实时监控。之后在网上找了找参考资料,发现了一个比较好的博客,在此贴出来,链接:http://www.cnblogs.com/surpassal/archive/2012/12/19/zed_webcam_lab1.html
本文是我在参考这个博客和其他很多资料的情况下写作的,其中包含了我遇到的问题及解决办法。
硬件平台:USB免驱摄像头+X86PC
开发环境:WIN7-64bit+VMware11.0+Ubuntu12.04+Qtcreater(Qt5.0)
一.开始前的准备
在正式开始编程前,首先需要对所需要用到的知识点进行了解,在此我们主要需要对V4L2,YUYV转RGB888和BMP编码进行了解。
1.V4L2
Video4linux2(简称V4l2),是linux中关于视频设备的内核驱动。在linux中一切皆文件,我们借由V4l2的api即可把摄像头当作文件进行读写。V4L2原生支持UVC摄像头,我开头说的USB免驱摄像头即在此列。由于V4L2设置非常复杂,所以在编程的时候再去看如何设置。
2.YUYV转RGB888
RGB是常见的像素点表示方法,目前常见于24bit(即真彩色),RGB三个分量分别占8bit,即上文我所提到的RGB888。
YUV(亦称YCrCb)是被欧洲电视系统所采用的一种颜色编码方法(属于PAL)。YUV主要用于优化彩色视频信号的传输,使其向后兼容老式黑白电视。与RGB视频信号传输相比,它最大的优点在于只需占用极少的带宽(RGB要求三个独立的视频信号同时传输)。其中"Y"表示明亮度(Lumina
nce或Luma),也就是灰阶值;是个基带信号,而"U"和"V"表示的则是色度(Chrominance或Chroma),作用是描述影像色彩及饱和度,用于指定像素的颜色。U和V不是基带信号,它俩是被正交调制了的。 "亮度"是通过RGB输入信号来创建的,方法是将RGB信号的特定部分叠加到一起。"色度"则定义了颜色的两个方面-色调与饱和度,分别用Cr和CB来表示。其中,Cr反映了RGB输入信号红色部分与RGB信号亮度值之间的差异。而CB反映的是RGB输入信号蓝色部分与RGB信号亮度值之同的差异。通过运算,YUV三分量可以还原出R(红),G(绿),B(蓝)。
换算公式如下表:
<span style="font-size:18px;">R = Y + 1.042*(V-128); G = Y - 0.34414*(U-128) - 0.71414*(V-128); B = Y + 1.772*(U-128);</span>
YUV同样也有很多标准,不过一般摄像头上会采用422,420等标准,YUYV就是422形式。解释一下就是,假设现在有两个像素点P1和P2,那么本来应该有Y1,U1,V1,Y2,U2,V2六个分量,但是实际上只保留了Y1,U1,Y2,V2,这样在相邻极近的两个像素点之间部分信息共享,减小了数据量,但是又因为变化较小,并不会明显改变图像质量。
3.BMP编码
BMP(全程Bitmap)是Windows中的标准图像文件格式。其编码方式简单,可以完整的保留原始图像信息。
典型的BMP图片包含四个部分:
1.位图头文件数据结构,包含BMP文件的类型,显示内容等信息。
2.位图信息数据结构,包含BMP的宽,高,压缩方法,颜色定义等信息。
3.调色板,可选部分,24bit图像不需要调色板。
4.位图数据,即真实像素数据
相关文件头和位图数据的说明在对BMP进行编码时进行介绍。
二,建立相关工程
首先在虚拟机中安装ubuntu,并且安装Qtcreater。安装教程在网上很多,我就不说了。建议直接下载一体包,方便安装。
Qtcreater官方下载地址:http://www.qt.io/download/
新建一个工程,务必确认好保存路径。
三.修改对应的UI界面
打开mainwindows.ui,在左侧工具栏拖出一个Label,将位置修改为(0,0)大小设置为你摄像头的分辨率,此处我设置为(1280*720)。将Label中的文字删除。修改此Label的obJectName为camera。将主窗口设置成合适大小。拖出一个Label,Line
edit,pushbutton。label文字修改为“路径”,pushbutton文字修改为“拍照”。Line edit的obJectName修改为path,pushbutton的obJectName修改为ok。合理安排所有控件的位置
四.开始程序的编写
1.首先进行头文件包含和定义必要的宏
#include <fcntl.h> #include <stdlib.h> #include <sys/mman.h> #include <linux/videodev2.h> #include <stdio.h> #include <unistd.h> #include <sys/types.h> #include <sys/ioctl.h> #include <errno.h> #include <QtGui> #include "yuyv_to_rgb888.h" #define Video_path "/dev/video0" //usb摄像头挂载路径 #define Aim_path "./test.jpeg" //目标文件存储路径 #define Image_high 720 //目标图片的分辨率720*1280 #define Image_width 1280 #define Video_count <span style="white-space:pre"> </span>2 //缓冲帧数
Video_path是摄像头的挂载路径,一旦V4l2识别摄像头有效后,就会产生这个设备节点。使用宏定义设定目标图片分辨率方便后期适应不同的摄像头。
2.定义需要用的结构体
typedef struct _buffer //定义缓冲区结构体 { void *start; unsigned int length; }buffer; typedef unsigned short WORD; //定义BMP文件头的数据类型 typedef unsigned int DWORD; #pragma pack(2) //修改结构体字节对齐规则 typedef struct _BITMAPFILEHEADER{ WORD bfType; //BMP文件头,必须为BM DWORD bfSize; //文件大小,字节为单位 DWORD bfReserved; //保留字,值为0 DWORD bfOffBits; //位图数据起始位置,此处取54 }BITMAPFILEHEADER; typedef struct _BITMAPINFOHEADER{ DWORD biSize; //本结构所占字节数,此处取40 DWORD biWidth; //图片宽度,像素为单位 DWORD biHeight; //高度,同上 WORD biPlanes; //目标设备级别,此处必须为1 WORD biBitCount; //像素位数,此处取24(真彩色) DWORD biCompression; //位图类型,必须为0 DWORD biSizeImage; //位图大小 DWORD biXPelsPerMeter; //水平像素数,此处取0 DWORD biYPelsPerMeter; //竖直像素数,此处取0 DWORD biClrUsed; //位图实际使用的颜色表中的颜色数,此处取0 DWORD biClrImportant; //位图显示过程中重要的颜色数,此处取0 }BITMAPINFOHEADER; #pragma pack()
buffer是保存原始图像数据的容器。BITMAPFILEHEADER是位图头文件数据结构,BITMAPINFOHEADER是位图信息数据结构。注意在定义结构体的时候需要对结构体对齐规则进行重设,以保证数据之间没有补位空缺,否则会导致BMP图片文件头损坏,致使无法保存正常的BMP图片。
3.定义V4L图像采集的类
class V4l { private: int fd; <span style="white-space:pre"> </span> //驱动文件句柄 bool state; //是否打开成功 buffer *buffers; //原始数据buff struct v4l2_capability <span style="white-space:pre"> </span>cap; //V4l2参数结构体 struct v4l2_fmtdesc fmtdesc; struct v4l2_format fmt; struct v4l2_streamparm setfps; struct v4l2_requestbuffers <span style="white-space:pre"> </span>req; struct v4l2_buffer buf; enum v4l2_buf_type type; BITMAPFILEHEADER bf; //BMP图片头 BITMAPINFOHEADER bi; unsigned char frame_buffer[Image_high*Image_width*3]; //RGB图片buff bool YUYV_to_RGB888(void); //YUYV转RGB888 bool V4l_Init(void); //V4L初始化 bool Get_Frame(void); //获取原始数据 bool Free_Frame(void); //更新原始数据 QPixmap image; public: V4l(); ~V4l(); QPixmap Get_image(void); //获取图片 bool Get_state(void); //获取当前V4L的状态 bool Save_BMP(char *path); //保存BMP图片 };
fd为摄像头的句柄,staate为摄像头的启动状态,后面是一些V4l2的结构体,我们用到一个说一个,定义所有用到的函数。
4.函数的具体实现
V4l::V4l()
V4l::V4l() { bi.biSize = 40; //设定BMP图片头 bi.biWidth = Image_width; bi.biHeight = Image_high; bi.biPlanes = 1; bi.biBitCount = 24; bi.biCompression = 0; bi.biSizeImage = Image_width*Image_high*3; bi.biXPelsPerMeter = 0; bi.biYPelsPerMeter = 0; bi.biClrUsed = 0; bi.biClrImportant = 0; bf.bfType = 0x4d42; bf.bfSize = 54 + bi.biSizeImage; bf.bfReserved = 0; bf.bfOffBits = 54; if(V4l_Init()) //初始化V4L state=true; else state=false; }
代码分析如下:
首先在V4l的构造函数中,对BMP图片的头信息进行了初始化。
下面我们看一下BMP头的具体构造
typedef struct _BITMAPFILEHEADER{ WORD bfType; //BMP文件头,必须为BM DWORD bfSize; //文件大小,字节为单位 DWORD bfReserved; //保留字,值为0 DWORD bfOffBits; //位图数据起始位置,此处取54 }BITMAPFILEHEADER;
bfType是固定的,也就是0x4d42,用ascii翻译过来是MB
bfSize是图片的大小,也就是width*high*3+54(头的大小),至于为什么是乘3,因为我们存储使用的是RGB888的格式,正好是三字节一个像素点。
bfReserved是保留字,设置为0即可
bfOffBits是图片信息偏移量,也就是像素数据起始地址,由于头是54字节,在这里也就是54
typedef struct _BITMAPINFOHEADER{ DWORD biSize; //本结构所占字节数,此处取40 DWORD biWidth; //图片宽度,像素为单位 DWORD biHeight; //高度,同上 WORD biPlanes; //目标设备级别,此处必须为1 WORD biBitCount; //像素位数,此处取24(真彩色) DWORD biCompression; //位图类型,必须为0 DWORD biSizeImage; //位图大小 DWORD biXPelsPerMeter; //水平像素数,此处取0 DWORD biYPelsPerMeter; //竖直像素数,此处取0 DWORD biClrUsed; //位图实际使用的颜色表中的颜色数,此处取0 DWORD biClrImportant; //位图显示过程中重要的颜色数,此处取0 }BITMAPINFOHEADER;
此结构说明注释已经说的很清楚了,就不重复解释了。
V4l::~V4l()
V4l::~V4l() { if(fd != -1) { ioctl(fd, VIDIOC_STREAMOFF, buffers); <span style="white-space:pre"> </span>//结束图像显示 close(fd); //关闭视频设备 } if(buffers!=NULL) <span style="white-space:pre"> </span>//释放申请的内存 { free(buffers); } }
代码分析如下:
检查fd是否打开状态,是的话就关闭设备,并且释放缓冲用的内存。
bool V4l::V4l_Init()
bool V4l::V4l_Init() { if((fd=open(Video_path,O_RDWR)) == -1) //读写方式打开摄像头 { qDebug()<<"Error opening V4L interface"; //send messege return false; } if (ioctl(fd, VIDIOC_QUERYCAP, &cap) == -1) //检查cap的属性 { qDebug()<<"Error opening device "<<Video_path<<": unable to query device."; return false; } else //打印cap信息 { qDebug()<<"driver:\t\t" <<QString::fromLatin1((char *)cap.driver); //驱动名 qDebug()<<"card:\t\t" <<QString::fromLatin1((char *)cap.card); //Device名 qDebug()<<"bus_info:\t\t" <<QString::fromLatin1((char *)cap.bus_info); //在Bus系统中存放位置 qDebug()<<"version:\t\t" <<cap.version; <span style="white-space:pre"> </span>//driver 版本 qDebug()<<"capabilities:\t" <<cap.capabilities; <span style="white-space:pre"> </span>//能力集,通常为:V4L2_CAP_VIDEO_CAPTURE | V4L2_CAP_STREAMING } fmtdesc.index=0; //获取摄像头支持的格式 fmtdesc.type=V4L2_BUF_TYPE_VIDEO_CAPTURE; qDebug()<<"Support format:"; while(ioctl(fd,VIDIOC_ENUM_FMT,&fmtdesc)!=-1) { qDebug()<<"\t\t"<<fmtdesc.index+1<<QString::fromLatin1((char *)fmtdesc.description); fmtdesc.index++; } fmt.type =V4L2_BUF_TYPE_VIDEO_CAPTURE; <span style="white-space:pre"> </span>//设置像素格式 fmt.fmt.pix.pixelformat <span style="white-space:pre"> </span>=V4L2_PIX_FMT_YUYV; <span style="white-space:pre"> </span>//使用YUYV格式输出 fmt.fmt.pix.height =Image_high; //设置图像尺寸 fmt.fmt.pix.width =Image_width; fmt.fmt.pix.field =V4L2_FIELD_INTERLACED; <span style="white-space:pre"> </span>//设置扫描方式 if(ioctl(fd, VIDIOC_S_FMT, &fmt) == -1) { qDebug()<<"Unable to set format"; return false; } if(ioctl(fd, VIDIOC_G_FMT, &fmt) == -1) //重新读取结构体,以确认完成设置 { qDebug()<<"Unable to get format"; return false; } else { qDebug()<<"fmt.type:\t\t" <<fmt.type; qDebug()<<"pix.height:\t" <<fmt.fmt.pix.height; qDebug()<<"pix.width:\t\t" <<fmt.fmt.pix.width; qDebug()<<"pix.field:\t\t" <<fmt.fmt.pix.field; } setfps.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; //设置预期的帧率,实际值不一定能达到 setfps.parm.capture.timeperframe.denominator = 30; //fps=30/1=30 setfps.parm.capture.timeperframe.numerator = 1; if(ioctl(fd, VIDIOC_S_PARM, &setfps)==-1) { qDebug()<<"Unable to set fps"; return false; } if(ioctl(fd, VIDIOC_G_PARM, &setfps)==-1) { qDebug()<<"Unable to get fps"; return false; } else { qDebug()<<"fps:\t\t"<<setfps.parm.capture.timeperframe.denominator/setfps.parm.capture.timeperframe.numerator; } qDebug()<<"init "<<Video_path<<" \t[OK]\n"; req.count=Video_count; //申请2个缓存区 req.type=V4L2_BUF_TYPE_VIDEO_CAPTURE; //Buffer的类型。此处肯定为V4L2_BUF_TYPE_VIDEO_CAPTURE req.memory=V4L2_MEMORY_MMAP; //Memory Mapping模式,则此处设置为:V4L2_MEMORY_MMAP if(ioctl(fd,VIDIOC_REQBUFS,&req)==-1) { qDebug()<<"request for buffers error"; return false; } int n_buffers; buffers = (buffer *)malloc(req.count*sizeof (*buffers)); //malloc缓冲区 if (!buffers) { qDebug()<<"Out of memory"; return false ; } for (n_buffers = 0; n_buffers < Video_count; n_buffers++) <span style="white-space:pre"> </span>//mmap四个缓冲区 { buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; buf.memory = V4L2_MEMORY_MMAP; buf.index = n_buffers; //query buffers if (ioctl (fd, VIDIOC_QUERYBUF, &buf) == -1) { qDebug()<<"query buffer error"; return false; } buffers[n_buffers].length = buf.length; //map buffers[n_buffers].start = mmap(NULL,buf.length,PROT_READ |PROT_WRITE, MAP_SHARED, fd, buf.m.offset); if (buffers[n_buffers].start == MAP_FAILED) { qDebug()<<"buffer map error"; return false; } } for (n_buffers = 0; n_buffers < Video_count; n_buffers++) //更新buff { buf.index = n_buffers; ioctl(fd, VIDIOC_QBUF, &buf); } type = V4L2_BUF_TYPE_VIDEO_CAPTURE; ioctl (fd, VIDIOC_STREAMON, &type); //开始采集 return true; }
代码分析如下:
if((fd=open(Video_path,O_RDWR)) == -1) <span style="white-space:pre"> </span>//读写方式打开摄像头 { qDebug()<<"Error opening V4L interface"; //发送错误信息 return false; }
打开摄像头设备,Video_path就是我们开始的宏定义,表示摄像头的设备节点
if (ioctl(fd, VIDIOC_QUERYCAP, &cap) == -1) //检查cap的属性 { qDebug()<<"Error opening device "<<Video_path<<": unable to query device."; return false; } else //打印cap信息 { qDebug()<<"driver:\t\t" <<QString::fromLatin1((char *)cap.driver); //驱动名 qDebug()<<"card:\t\t" <<QString::fromLatin1((char *)cap.card); //Device名 qDebug()<<"bus_info:\t\t" <<QString::fromLatin1((char *)cap.bus_info); //在Bus系统中存放位置 qDebug()<<"version:\t\t" <<cap.version; //driver 版本 qDebug()<<"capabilities:\t" <<cap.capabilities; //能力集,通常为:V4L2_CAP_VIDEO_CAPTURE | V4L2_CAP_STREAMING }
检查cap的属性,其中包含着摄像头的基础属性。我们在调试信息中把这些打印出来。
fmtdesc.index=0; //获取摄像头支持的格式 fmtdesc.type=V4L2_BUF_TYPE_VIDEO_CAPTURE; qDebug()<<"Support format:"; while(ioctl(fd,VIDIOC_ENUM_FMT,&fmtdesc)!=-1) { qDebug()<<"\t\t"<<fmtdesc.index+1<<QString::fromLatin1((char *)fmtdesc.description); fmtdesc.index++; }
获取摄像头支持的格式,并且在调试信息中打印出来。方便后面对图像信息进行处理。
fmt.type =V4L2_BUF_TYPE_VIDEO_CAPTURE; //设置像素格式 fmt.fmt.pix.pixelformat <span style="white-space:pre"> </span>=V4L2_PIX_FMT_YUYV; //使用YUYV格式输出 fmt.fmt.pix.height =Image_high; //设置图像尺寸 fmt.fmt.pix.width =Image_width; fmt.fmt.pix.field =V4L2_FIELD_INTERLACED; //设置扫描方式 if(ioctl(fd, VIDIOC_S_FMT, &fmt) == -1) { qDebug()<<"Unable to set format"; return false; } if(ioctl(fd, VIDIOC_G_FMT, &fmt) == -1) //重新读取结构体,以确认完成设置 { qDebug()<<"Unable to get format"; return false; } else { qDebug()<<"fmt.type:\t\t" <<fmt.type; qDebug()<<"pix.height:\t" <<fmt.fmt.pix.height; qDebug()<<"pix.width:\t\t" <<fmt.fmt.pix.width; qDebug()<<"pix.field:\t\t" <<fmt.fmt.pix.field; }
设置图形的基础属性,在进行设置后还要重新读取出来以确认设置成功,因为即使ioctl返回成功,设置也是不一定完成的。
setfps.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; //设置预期的帧率,实际值不一定能达到 setfps.parm.capture.timeperframe.denominator = 30; //fps=30/1=30 setfps.parm.capture.timeperframe.numerator = 1; if(ioctl(fd, VIDIOC_S_PARM, &setfps)==-1) { qDebug()<<"Unable to set fps"; return false; } if(ioctl(fd, VIDIOC_G_PARM, &setfps)==-1) { qDebug()<<"Unable to get fps"; return false; } else { qDebug()<<"fps:\t\t"<<setfps.parm.capture.timeperframe.denominator/setfps.parm.capture.timeperframe.numerator; } qDebug()<<"init "<<Video_path<<" \t[OK]\n";
设置预期帧率,此处帧率为目标值,实际值可以通过读取获得,通常在高分辨率下无法达到预期的帧率,帧率设置是通过两个参数进行的,fps=setfps.parm.capture.timeperframe.denominator/setfps.parm.capture.timeperframe.numerator;
即通过分别设置分母和分子的形式来确定帧数。一般设置帧率为30fps。
到此完成所有摄像头基础参数的设置。
req.count=Video_count; //申请2个缓存区 req.type=V4L2_BUF_TYPE_VIDEO_CAPTURE; //Buffer的类型。此处肯定为V4L2_BUF_TYPE_VIDEO_CAPTURE req.memory=V4L2_MEMORY_MMAP; //Memory Mapping模式,则此处设置为:V4L2_MEMORY_MMAP if(ioctl(fd,VIDIOC_REQBUFS,&req)==-1) { qDebug()<<"request for buffers error"; return false; }
申请视频的缓冲区,视频缓冲有两种获取方式,一个是read方式,即直接读取图像信息,适用于静态图像的获取,但是在这里我们需要实时显示图像,所以使用mmap的方式。
int n_buffers; buffers = (buffer *)malloc(req.count*sizeof (*buffers)); //malloc缓冲区 if (!buffers) { qDebug()<<"Out of memory"; return false ; } for (n_buffers = 0; n_buffers < Video_count; n_buffers++) <span style="white-space:pre"> </span>//mmap缓冲区 { buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; buf.memory = V4L2_MEMORY_MMAP; buf.index = n_buffers; //query buffers if (ioctl (fd, VIDIOC_QUERYBUF, &buf) == -1) { qDebug()<<"query buffer error"; return false; } buffers[n_buffers].length = buf.length; //map buffers[n_buffers].start = mmap(NULL,buf.length,PROT_READ |PROT_WRITE, MAP_SHARED, fd, buf.m.offset); if (buffers[n_buffers].start == MAP_FAILED) { qDebug()<<"buffer map error"; return false; } }
为缓冲区申请内存,并将缓冲区mmap到用户空间。
for (n_buffers = 0; n_buffers < Video_count; n_buffers++) //更新buff { buf.index = n_buffers; ioctl(fd, VIDIOC_QBUF, &buf); }
更新所有的buff
type = V4L2_BUF_TYPE_VIDEO_CAPTURE; ioctl (fd, VIDIOC_STREAMON, &type);
启动采集。
bool V4l::Get_Frame(void)
bool V4l::Get_Frame(void) //获取原始数据 { if(ioctl(fd, VIDIOC_DQBUF, &buf) == -1) { return false; } return true; } bool V4l::Free_Frame() //允许更新原始数据 { if(ioctl(fd, VIDIOC_QBUF, &buf) == -1) { return false; } return true; }
代码分析如下:
这两个函数分别是获取视频图像帧和更新视频图像帧的函数。
举个例子帮助理解,在这里我们使用了两帧的缓冲我们命名为i,i+1,内核空间中存储这个两个帧的地址命名为ip,ip+1假设现在两个帧都是有效帧,先使用DQBUF获取到ip的数据,这时的数据i就在用户空间了,而原有的两个帧的空间中ip+1将会空缺出来,i+1转移到ip中去,此时再使用DQBUF获取ip的数据得到的就是i+1,如果继续获取,因为内核空间中ip已经没有数据了,则会导致获取失败。所以每次获取数据后我们都应该对内核空间中的数据进行更新,更新使用QBUF,这样过程就变成了,i+1的数据转移到ip,而i+2将进入ip+1,始终保证ip中有数据,而双帧缓冲将极大的减小摄像头本身获取图像所需要的时间对整个视频获取过程的影响,从而提高帧率。
bool V4l::Get_state(void)
bool V4l::Get_state(void) //获取当前摄像头状态 { return state; }
bool V4l::YUYV_to_RGB888(void)
bool V4l::YUYV_to_RGB888(void) //图片转码 { int i,j; unsigned char y1,y2,u,v,r1,b1,r2,b2; int g1,g2; char *pointer; int high,width; pointer = (char *)buffers[0].start; high=Image_high; width=Image_width/2; for(i=0;i<high;i++) { for(j=0;j<(Image_width/2);j++) { y1 = *( pointer + (i*width+j)*4); u = *( pointer + (i*width+j)*4 + 1); y2 = *( pointer + (i*width+j)*4 + 2); v = *( pointer + (i*width+j)*4 + 3); r1=(int)R_table[y1][v]; //r1 = y1 + 1.042*(v-128); g1=y1-UV_table[u][v]; //g1 = y1 - 0.34414*(u-128)-0.71414*(v-128); b1=(int)B_table[y1][u]; //b1 = y1 + 1.772*(u-128); r2=(int)R_table[y2][v]; //r2 = y2 + 1.042*(v-128); g2=y2-UV_table[u][v]; //g2 = y2-0.34414*(u-128)-0.71414*(v-128); b2=(int)B_table[y2][u]; //b2 = y2 + 1.772*(u-128); if(g1>255) g1 = 255; else if(g1<0) g1 = 0; if(g2>255) g2 = 255; else if(g2<0) g2 = 0; *(frame_buffer + ((high-1-i)*width+j)*6 ) = b1; *(frame_buffer + ((high-1-i)*width+j)*6 + 1) = (unsigned char)g1; *(frame_buffer + ((high-1-i)*width+j)*6 + 2) = r1; *(frame_buffer + ((high-1-i)*width+j)*6 + 3) = b2; *(frame_buffer + ((high-1-i)*width+j)*6 + 4) = (unsigned char)g2; *(frame_buffer + ((high-1-i)*width+j)*6 + 5) = r2; } } return true; }
代码分析如下:
pointer = (char *)buffers[0].start; high=Image_high; width=Image_width/2;
首先获取到用户空间的原始YUYV数据,因为是两个像素点共用一组数据,所以宽度实际上是指定宽度的一半。
y1 = *( pointer + (i*width+j)*4); u = *( pointer + (i*width+j)*4 + 1); y2 = *( pointer + (i*width+j)*4 + 2); v = *( pointer + (i*width+j)*4 + 3);
获取四个对应的分量。
r1=(int)R_table[y1][v]; //r1 = y1 + 1.042*(v-128); g1=y1-UV_table[u][v]; //g1 = y1 - 0.34414*(u-128)-0.71414*(v-128); b1=(int)B_table[y1][u]; //b1 = y1 + 1.772*(u-128); r2=(int)R_table[y2][v]; //r2 = y2 + 1.042*(v-128); g2=y2-UV_table[u][v]; //g2 = y2-0.34414*(u-128)-0.71414*(v-128); b2=(int)B_table[y2][u]; //b2 = y2 + 1.772*(u-128); if(g1>255) g1 = 255; else if(g1<0) g1 = 0; if(g2>255) g2 = 255; else if(g2<0) g2 = 0;
YUYV转换到RGB数据,公式如注释部分,但是可以看到转换过程有大量的浮点运算,而浮点运算非常耗时,所以在此对算法进行优化,通过空间换取时间的方法,我采用了查表法的方式进行转换。建立R_yv,G_yuv,B_yu的换算表,所有的表需要占的空间是256*256*3=192k,而转换效率提升在4倍以上。在高帧率下可以很显著的减少帧延迟。至于两种算法如何取舍,取决于实际应用环境。
<span style="font-size: 18px;">*(frame_buffer + ((high-1-i)*width+j)*6 ) =</span><span style="font-family: Arial; line-height: 21.06px; white-space: pre-wrap;"><span style="font-size: 18px;"> </span><span style="font-size:14px;">b1</span><span style="font-size: 18px;">;</span></span><span style="font-size: 18px;"> *(frame_buffer + ((high-1-i)*width+j)*6 + 1) = (unsigned char)g1; *(frame_buffer + ((high-1-i)*width+j)*6 + 2) = r1; *(frame_buffer + ((high-1-i)*width+j)*6 + 3) = b2; *(frame_buffer + ((high-1-i)*width+j)*6 + 4) = (unsigned char)g2; *(frame_buffer + ((high-1-i)*width+j)*6 + 5) = r2;</span>
将所得到的RGB数据存储起来,需要注意的是,BMP记录像素的顺序在扫描行内是从左到右,扫描行之间是从下到上的,所以存储也需要按照这个格式来做。
转换表如下,被存储在yuyv_to_rgb888.h中
QPixmap V4l::Get_image(void)
QPixmap V4l::Get_image(void) { Get_Frame(); YUYV_to_RGB888(); QByteArray temp; temp.append((char *)&bf,14); temp.append((char *)&bi,40); temp.append((char *)frame_buffer,Image_high*Image_width*3); image.loadFromData(temp); Free_Frame(); return image; }
代码分析如下:
首先获取原始数据并转换成RGB格式,之后定义一个QByteArray用以获取数据,把BMP的头和数据均写入temp,此时temp包含有一张bmp图片的所有信息,将这些信息导入image中,得到可以显示的QPixmap,允许更新原始视频数据,返回此时的QPixmap用于显示。
bool V4l::Save_BMP(char *path)
bool V4l::Save_BMP(char *path) { Get_Frame(); YUYV_to_RGB888(); FILE *fp1; fp1=fopen(path,"wb"); if(fp1==NULL) { qDebug()<<"open file fail"; return false; } fwrite(&bf.bfType, 14, 1, fp1); fwrite(&bi.biSize, 40, 1, fp1); fwrite(frame_buffer, bi.biSizeImage, 1, fp1); fclose(fp1); return true; }
代码分析如下:
同样是先获取RGB数据,之后打开路径,将所有信息写入文件,关闭文件,即可得到一张BMP图片。
在mainwindows的类中声明一个V4l的对象v4l;
private slots: void showtime(); void photo();
同样还需要申明两个槽,一个用于视频数据的刷新,一个用于手动拍摄照片。
改写mainwindows的构造函数为:
ui->setupUi(this); connect(ui->ok,SIGNAL(clicked()),this,SLOT(photo())); QTimer *timer = new QTimer(this); connect(timer, SIGNAL(timeout()), this, SLOT(showtime())); timer->start(33);
代码分析如下:
第一个connect链接pushbutton的clicked()信号到photo(),也就是我们的拍照函数。
定义一个QTimer,超时时间定位33ms,即刷新率为30fps,超时信号连接到showtime()刷新图像。
void MainWindow::photo()
void MainWindow::photo() { char *temp; temp=(char *)ui->path->text().toStdString().data(); if(v4l.Save_BMP(temp)) { QMessageBox::information(this,"成功","拍照成功",QMessageBox::Ok); } else { QMessageBox::warning(this,"失败","拍照失败",QMessageBox::Ok); } }
代码分析如下:
首先获取line edit中的路径,由于获取到的数据类型是QString,先转化为stdString的类型,之后取其中date出来,把路径参数传入Save_BMP中,如果返回成功就发送一个QMessageBox提示拍照成功,反之提示拍照失败。
void MainWindow::showtime()
void MainWindow::showtime() { ui->camera->setPixmap(v4l.Get_image()); }
重设camera的pixmap即可显示当前的图像,每33ms刷新一次,即可当作连续的视频。
五.编译运行
如图是qDebuge输出的调试信息,可以看到摄像头类型是UVC摄像头,支持格式有MJPEG和YUYV,实际上在使用中如果只进行后台拍摄,可以直接使用MJPEG格式,这样可以直接将得到的数据存入文件得到jpeg格式的图片。分辨率是1280*720也就是我们常说的720P的摄像头,需要注意一点的是fps这里,实际上我在设置的时候是设置的30fps,但是对于这个摄像头来说最大只能达到10fps,所以设置后返回的值就是10fps
拍照效果如图。可以看到效果还是不错的
路径下的照片如图bmp.bmp,是可以正常显示的,成像效果如上图。
六.跨平台移植
将X86平台上的Qt程序移植到ARM平台上是很容易的,只需要更改一下编译器设置就可以了,在此就不进行演示了。
七.结语
到此,整个工程的实现就已经结束了,在我开始参考的博客里,博主还进行了avi的压制,但是我暂时并没有这方面的需求,所以没有进行深入的研究,如果还有想录制视频的看官,可以移步我开头给的链接,看看那位博主是怎么做的。
--THE END
--2016-7-12