序言
本系统教程分为3篇linux内核视频教程,由浅至深地教你在Linux上应用摄像头,为之后在视觉等领域的应用打下基础:
1.本篇为基础学习篇,使用摄像头照相获取.jpg格式图片
2.在ubuntu上获取动态读取摄像头的视频流并显示在pc上
3.在linux嵌入式开发板lcd上显示摄像头拍摄的视频
哪些是V4L2框架
市场上转让着种类繁杂的摄像头,她们的机型不同,厂商不同。这样子的话,每一次换一个摄像头莫非就要我们自己去写一个驱动吗?这样子太麻烦了并且没有必要linux site:infoq.cn嵌入式linux 培训,于是就出现了v4l2框架,现今的摄像头都适配这个主流框架(即使不适配厂商也会想尽办法去适配借此来降低自己产品的畅销性)。这个v4l2适配了多种插口的摄像头。我们只须要学习一个简单的V4L2编程就可以通用所有摄像头了!我们晓得linux的摄像头编程应用在人工智能嵌入式很热门,下边我来手把手教你linux是如何驱动摄像头的。
驱动流程
本次教程在pc的虚拟机中实战,想要在嵌入式设备运行只要使用交叉编译器编译出可执行文件再运行即可。菜鸟建议先再pc端使用usb摄像头实验,等学会了再调试到嵌入式设备中。
1.打开设备
当我们摄像头插入到笔记本后,再/dev目录下会出现相应的摄像头video节点如图:
我们须要找到摄像头对应的节点,可以每位都试一试。
#include
#include
#include
#include
#include
#include
int main(void)
{
int fd = open("/dev/video0",O_RDWR);
if (fd < 0)
{
perror("打开设备失败");
return -1;
}
close(fd);
return 0;
}
2.获取支持格式
在这儿须要用到ioctl(文件描述符,命令,与命令对应的结构体)函数来获取摄像头格式,对应的参数在如右图头文件中查找,具体命令作用请复制到百度搜索,这儿直接告诉你们VIDIOC_ENUM_FMT命令是获取摄像头格式。
以上文档以及要写的程序建议你们在vscode打开,便捷查找以及易于使用智能补全插件检测代码和写代码。
#include
#include
#include
#include
#include
#include
#include
#include
int main(void)
{
int fd = open("/dev/video1",O_RDWR);
if (fd < 0)
{
perror("打开设备失败");
return -1;
}
//获取摄像头支持格式 ioctl(文件描述符,命令,与命令对应的结构体)
struct v4l2_fmtdesc vfmt;
vfmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;//摄像头采集
int i = 0;
while(1) //使用while循环从0开始测试index索引值
{
vfmt.index = i;
i++;
int ret = ioctl(fd,VIDIOC_ENUM_FMT,&vfmt);
if (ret < 0)
{
perror("获取失败");
break;
}
printf("index=%dn",vfmt.index);
printf("flags=%dn",vfmt.flags);
printf("discription=%sn",vfmt.description);
unsigned char *p = (unsigned char *)&vfmt.pixelformat;
printf("pixelformat=%c%c%c%cn",p[0],p[1],p[2],p[3]);
printf("reserved=%dn",vfmt.reserved[0]);
}
close(fd);
return 0;
}
运行结果
说明我的摄像头支持三种格式。
3.配置摄像头
#include
#include
#include
#include
#include
#include
#include
#include
#include
int main(void)
{
int fd = open("/dev/video0",O_RDWR);//根据自己的摄像头设备节点打开
if (fd < 0)
{
perror("打开设备失败");
return -1;
}
//获取摄像头支持格式 ioctl(文件描述符,命令,与命令对应的结构体)
struct v4l2_format vfmt;
vfmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; //摄像头采集
vfmt.fmt.pix.width = 640; //设置摄像头采集参数,不可以任意设置
vfmt.fmt.pix.height = 480;
vfmt.fmt.pix.pixelformat = V4L2_PIX_FMT_YYUV; //设置视频采集格式 ,根据上一步测得,注意格式有yyuv和yuyv不要搞混
int ret = ioctl(fd,VIDIOC_S_FMT,&vfmt); //设置格式命令
if (ret < 0)
{
perror("设置格式失败1");
}
memset(&vfmt,0,sizeof(vfmt));
vfmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
ret = ioctl(fd,VIDIOC_G_FMT,&vfmt);
if (ret < 0)
{
perror("设置格式失败2");
}
if(vfmt.fmt.pix.width == 640 && vfmt.fmt.pix.height == 480 && vfmt.fmt.pix.pixelformat == V4L2_PIX_FMT_YUYV)
{
printf("设置成功!");
}else
{
printf("设置失败3");
}
close(fd);
return 0;
}
复印出:设置成功即可。
4.申请内核缓冲班组列
这儿也是一样,直接告诉你们VIDIOC_REQBUFS操作命令是申请缓冲班组列。
#include
#include
#include
#include
#include
#include
#include
#include
#include
int main(void)
{
int fd = open("/dev/video0",O_RDWR);
if (fd < 0)
{
perror("打开设备失败");
return -1;
}
//获取摄像头支持格式 ioctl(文件描述符,命令,与命令对应的结构体)
struct v4l2_format vfmt;
vfmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; //摄像头采集
vfmt.fmt.pix.width = 640; //设置摄像头采集参数,不可以任意设置
vfmt.fmt.pix.height = 480;
vfmt.fmt.pix.pixelformat = V4L2_PIX_FMT_YYUV; //设置视频采集格式 ,根据上一布测得
int ret = ioctl(fd,VIDIOC_S_FMT,&vfmt);
if (ret < 0)
{
perror("设置格式失败1");
}
//申请内核空间
struct v4l2_requestbuffers reqbuffer;
reqbuffer.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
reqbuffer.count = 4; //申请4个缓冲区
reqbuffer.memory = V4L2_MEMORY_MMAP; //映射方式
ret = ioctl(fd,VIDIOC_REQBUFS,&reqbuffer);
if (ret < 0)
{
perror("申请空间失败");
}
close(fd);
return 0;
}
执行结果如图linux内核视频教程,表示正常(在linux中,没有输出是最好的输出)
5.内核缓冲班组列映射到用户空间
映射步骤用到了两个命令:VIDIOC_QUERYBUF和VIDIOC_QBUF,她们两个使用相同的结构体
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
int main(void)
{
int fd = open("/dev/video0",O_RDWR);
if (fd < 0)
{
perror("打开设备失败");
return -1;
}
//获取摄像头支持格式 ioctl(文件描述符,命令,与命令对应的结构体)
struct v4l2_format vfmt;
vfmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; //摄像头采集
vfmt.fmt.pix.width = 640; //设置摄像头采集参数,不可以任意设置
vfmt.fmt.pix.height = 480;
vfmt.fmt.pix.pixelformat = V4L2_PIX_FMT_YYUV; //设置视频采集格式 ,根据上一布测得
int ret = ioctl(fd,VIDIOC_S_FMT,&vfmt);
if (ret < 0)
{
perror("设置格式失败1");
}
//申请内核空间
struct v4l2_requestbuffers reqbuffer;
reqbuffer.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
reqbuffer.count = 4; //申请4个缓冲区
reqbuffer.memory = V4L2_MEMORY_MMAP; //映射方式
ret = ioctl(fd,VIDIOC_REQBUFS,&reqbuffer);
if (ret < 0)
{
perror("申请空间失败");
}
//映射
unsigned char *mptr[4];//保存映射后用户空间的首地址
struct v4l2_buffer mapbuffer;
//初始化type和index
mapbuffer.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
for(int i = 0; i <4;i++) { //使用for对四个申请的空间进行轮询
mapbuffer.index = i;
ret = ioctl(fd,VIDIOC_QUERYBUF,&mapbuffer); //从内核空间中查询一个空间作映射
if (ret < 0)
{
perror("查询内核空间失败");
}
//映射到用户空间
mptr[i] = (unsigned char *)mmap(NULL,mapbuffer.length,PROT_READ|PROT_WRITE,MAP_SHARED,fd,mapbuffer.m.offset);
//查询后通知内核已经放回
ret = ioctl(fd,VIDIOC_QBUF,&mapbuffer);
if (ret < 0)
{
perror("放回失败");
}
}
close(fd);
return 0;
}
6.采集帧数据
采集有四个命令:
VIDIOC_STREAMON(开始采集写数据到队列中)
VIDIOC_DQBUF(告诉内核我要某一个数据,内核不可以更改我正在采集的地方的数据)
VIDIOC_QBUF(告诉内核我早已使用完毕,内核可以写入了)
VIDIOC_STREAMOFF(停止采集-不在向队列中写数据)
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
int main(void)
{
int fd = open("/dev/video0",O_RDWR);
if (fd < 0)
{
perror("打开设备失败");
return -1;
}
//获取摄像头支持格式 ioctl(文件描述符,命令,与命令对应的结构体)
struct v4l2_format vfmt;
vfmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; //摄像头采集
vfmt.fmt.pix.width = 640; //设置摄像头采集参数,不可以任意设置
vfmt.fmt.pix.height = 480;
vfmt.fmt.pix.pixelformat = V4L2_PIX_FMT_MJPEG; //设置为mjpg格式,则我可以直接写入文件保存,YUYV格式保存后需要转换格式才能查看
int ret = ioctl(fd,VIDIOC_S_FMT,&vfmt);
if (ret < 0)
{
perror("设置格式失败1");
}
//申请内核空间
struct v4l2_requestbuffers reqbuffer;
reqbuffer.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
reqbuffer.count = 4; //申请4个缓冲区
reqbuffer.memory = V4L2_MEMORY_MMAP; //映射方式
ret = ioctl(fd,VIDIOC_REQBUFS,&reqbuffer);
if (ret < 0)
{
perror("申请空间失败");
}
//映射
unsigned char *mptr[4];//保存映射后用户空间的首地址
unsigned int size[4];
struct v4l2_buffer mapbuffer;
//初始化type和index
mapbuffer.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
for(int i = 0; i <4;i++) {
mapbuffer.index = i;
ret = ioctl(fd,VIDIOC_QUERYBUF,&mapbuffer); //从内核空间中查询一个空间作映射
if (ret < 0)
{
perror("查询内核空间失败");
}
//映射到用户空间
mptr[i] = (unsigned char *)mmap(NULL,mapbuffer.length,PROT_READ|PROT_WRITE,MAP_SHARED,fd,mapbuffer.m.offset);
size[i] = mapbuffer.length; //保存映射长度用于后期释放
//查询后通知内核已经放回
ret = ioctl(fd,VIDIOC_QBUF,&mapbuffer);
if (ret < 0)
{
perror("放回失败");
}
}
//开始采集
int type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
ret = ioctl(fd,VIDIOC_STREAMON,&type);
if (ret < 0)
{
perror("开启失败");
}
//从队列中提取一帧数据
struct v4l2_buffer readbuffer;
readbuffer.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; //每个结构体都需要设置type为这个参赛要记住
ret = ioctl(fd,VIDIOC_DQBUF,&readbuffer);
if (ret < 0)
{
perror("读取数据失败");
}
FILE *file=fopen("my.jpg", "w+"); //打开一个文件
fwrite( mptr[readbuffer.index],readbuffer.length,1,file);//写入文件
fclose(file); //写入完成,关闭文件
//通知内核使用完毕
ret = ioctl(fd, VIDIOC_QBUF, &readbuffer);
if(ret < 0)
{
perror("放回队列失败");
}
//停止采集
ret = ioctl(fd,VIDIOC_STREAMOFF,&type);
//释放映射
for(int i=0; i<4; i)munmap(mptr[i], size[i]);
close(fd); //关闭文件
return 0;
}
到了这一步我们就早已是可以采集到帧数据了。
进阶学习
在我们成功驱动摄像头拍取相片然后,下一步其实是连续读取弄成视频啦!接出来我将教你们将生成的jpg格式视频流转化成rgb格式显示在屏幕上:动态显示摄像头数据