v4l2摄像头数据获取并显⽰到fb上,yuv格式处理
使⽤v4l2采集摄像头数据,将yuv图像解码成rgb,并显⽰到fb上
折腾了⼀个多星期,总算实现了摄像头数据采集并显⽰到屏幕上,整理⼀下嵌⼊式Linux从摄像头获取数据并显⽰到fb屏幕上的过程。使⽤尽量少的依赖,只⽤了v4l2视频设备驱动以及fb驱动,更加深⼊的理解了计算机底层图像处理的原理。
主要流程
整个处理的主要步骤分为以下:
1. Linux v4l2驱动采集摄像头数据。
2. 图像格式转换:由于采集到的图像不是能直接⽤来显⽰的RGB格式,需要进⾏格式转换。
3. 图像缩放:摄像头采集的图⽚分辨率与显⽰屏分辨率不匹配,需要进⾏图像缩放。
4. 图像显⽰:缩放后的数据写到fb的内存映射上。
v4l2驱动采集摄像头数据
v4l2是Linux内置的视频设备驱动,其采集摄像头数据的流程主要分为以下⼏步:
1. 打开视频设备,使⽤open()函数,打开成功返回对应的设备描述符。
//1. open device
char* default_camera = "/dev/video0";
int fd_camera;
if(argc<2)
fd_camera = open(default_camera, O_RDWR);
else
fd_camera = open(argv[1], O_RDWR);
if(fd_camera == -1)
{
printf("fail to open the camera!\n");
return -1;
}
2. 获取,设置摄像头参数
相关结构体有:
v4l2_capability: 摄像头主要信息的结构体,如其名字,记录了摄像头的能⼒。
v4l2_fmtdesc: 存储摄像头数据格式信息的结构体,可查看摄像头⽀持的视频数据格式。
v4l2_format: 帧数据具体格式信息,如图像宽⾼等。可通过此结构体对摄像头传输出来的帧数据进⾏设置( VIDIOC_S_FMT)或者获取( VIDIOC_G_FMT)。这⾥主要是把输出帧图像数据格式设置为YUYV或MJPEG。
这⾥有个坑需要注意,设置后⼀定要再次获取format,因为设置不⼀定成功。⽽且图像宽⾼不能随意设置,我⼀开始简单的设置为跟屏幕分辨率⼀致,虽然设置成功了,但是读取时摄像头却没有采集到数据,
推测摄像头不⽀持我设的分辨率,毕竟摄像头⾥⾯应该⽀持不了任意尺度的图像缩放,需要获取后⾃⼰⽤算法进⾏处理。但是它竟然返回的结果确实设置成功了,导致后⾯在等待buffer出队的时候⼀直没有数据,程序⼀直处于阻塞状态。所以在不了解摄像头的情况下还是谨慎设置。
//2.get camera capture(camera infomation)
v4l2_capability cap;
ioctl(fd_camera, VIDIOC_QUERYCAP, &cap);
describe_cap(cap);
//3.format describe: get cam frame info
v4l2_fmtdesc fmtdesc;
fmtdesc.index = 0;
printf("support format:\n");
while(ioctl(fd_camera, VIDIOC_ENUM_FMT, &fmtdesc) != -1)
{
cout<<fmtdesc.index<<":"<<fmtdesc.description<<endl;
fmtdesc.index++;
}
//4.format: set cam info, YUYV here.
v4l2_format fmt;
fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV;
if(-1 == ioctl(fd_camera, VIDIOC_S_FMT, &fmt))
perror("fail to set fmt");
if(-1 == ioctl(fd_camera, VIDIOC_G_FMT, &fmt))
perror("fail to get fmt");
cout<<"video format is:"<<(fmt.fmt.pix.pixelformat == V4L2_PIX_FMT_YUYV?"YUYV":"UNKNOWN")<<endl;
cout<<"frame size:"<<fmt.fmt.pix.width<<"x"<<fmt.fmt.pix.height<<endl;
void describe_cap(v4l2_capability cap)
{
cout<<"driver:"<<cap.driver<<endl;
cout<<"name:"<<cap.card<<endl;
cout<<"bus info: "<<cap.bus_info<<endl;
cout<<"driver version:"<<(cap.version>>16&0xff)<<"."<<
(cap.version>>8&0xff)<<"."<<
(cap.version&0xff)<<endl;
printf("capabilities: %x\n", cap.capabilities);
printf("is video capture: %s\n", cap.capabilities&V4L2_CAP_VIDEO_CAPTURE? "True":"False");
printf("support io read: %s\n", cap.capabilities&V4L2_CAP_READWRITE? "True":"False");
printf("support IO stream: %s\n", cap.capabilities&V4L2_CAP_STREAMING?"True":"False");
}
3. 申请帧缓冲队列。
v4l2提供帧缓冲队列缓冲摄像头采集到的帧数据,申请到帧缓冲队列后是操作系统⾃动管理的,每次要获取数据需要将buffer出队,⽤完后将buffer⼊队。涉及到的结构体有:
v4l2_requestbuffers:⽤于申请帧缓冲队列;
v4l2_buffer: ⽤于操作buffer的结构体;
整个操作的流程就是:
(1)申请帧缓冲队列(VIDIOC_REQBUFS)
(2) 查询帧缓冲队列(VIDIOC_QUERYBUF),字⾯意思,查询申请的buffer
(3) 缓冲⼊队列( VIDIOC_QBUF)初始化时每次QUERYBUF后需要执⾏⼀次⼊列。后续每次访问数据需要先QBUF, 访问后需要⼊队列VIDIOC_DQBUF
(4) 内存映射 mmap
//5.get frame to buffer
//5.1 request buffers
struct v4l2_requestbuffers req;
< = V4L2_MEMORY_MMAP;
if(ioctl(fd_camera, VIDIOC_REQBUFS, &req)<0)
{
perror("fail to request buffer");
}
cout<<"frame buffer request done"<<endl;
//5.2 map buffer
void* frame_map[2];
v4l2_buffer buf;
for(int i=0;i<2;i++)
{
memset(&buf, 0, sizeof(buf));
< = V4L2_MEMORY_MMAP;
buf.index = i;
if(-1 == ioctl(fd_camera, VIDIOC_QUERYBUF, &buf))
{
printf("query buf%d error!\n", i);
return 1;
}
printf("query buf%d done;\t", i);
//start to map,把缓冲队列映射到frame_map[]指针数组⾥⾯。
frame_map[i] = mmap(0, buf.length, PROT_READ|PROT_WRITE, MAP_SHARED, fd_camera, ffset);
if(MAP_FAILED == frame_map[i])
{
perror("fram buffer map failed\n");
}
printf("map buf%d done;\t", i);
// queue buffer
if(-1 == ioctl(fd_camera, VIDIOC_QBUF, &buf))
{
printf("queue buffer%d failed!\n", i);
return 1;
}
printf("queue buf%d done!\n", i);
}
4. 开启视频流
// stream on
v4l2_buf_type type=V4L2_BUF_TYPE_VIDEO_CAPTURE;
if(-1 == ioctl(fd_camera, VIDIOC_STREAMON, &type))
perror("stream on error\n");
cout<<"stream on done\n"<<endl;
6. 采集图像
准备⼯作都做好后,采集图像只要遵循这⼀个简单的流程就可以了:
VIDIOC_DQBUF buffer出队列==》⽤映射好的内存地址获取图像数据==》VIDIOC_QBUF buffer⼊队列
while(!con.interrupt)
{
//6.1 dequeue buffer;
memset(&buf, 0, sizeof(buf));
< = V4L2_MEMORY_MMAP;
int ret = ioctl(fd_camera, VIDIOC_DQBUF, &buf);
if(ret == -1)
{
perror("dequeue fail");
return 1;
}
/
/6.2 pic process
//图像处理
//6.3 enqueue buffer
if(-1 == ioctl(fd_camera, VIDIOC_QBUF, &buf))
{
perror("enqueue fail");
return 1;
}
usleep(10000);
}
图像处理与显⽰
mmap格式怎么打开图像处理
摄像头采集的图⽚是YUV4:2:2的格式,即像素按照以下顺序排列:
Y00U00, 01Y01V00, 01Y02U02, U03Y03V02, V03 Y10U10, 11Y11V10, 11Y12U12, U13Y13V12, V13……………………
简单理解起来就是每个像素点都有Y亮度值,⽽U跟V的⾊度信号是左右间隔排列。可以把每两个像素看成⼀组,则每组的两个像素各有各的亮度值,但是分享了U、V⾊度值。这样做估计是因为⼈眼对亮度的空间分辨率较为敏感吧。
根据YUV转RGB的公式,可以将图⽚转为RGB。
由于每⼀帧都需要进⾏处理,为了节省解码时间,可以在初始化时将YUV到RGB的映射关系制成⼀个映射表,⽣成⽂件存放起来,这样可以⼤⼤提⾼解码数率。
void init_yuv2bgr()
{
//将YUV转RGB映射输出为⽂件保存,⽅便以后每次运⾏。
FILE* yuv2bgr_table =fopen("","r");
if(yuv2bgr_table ==NULL)
yuv2bgr_table =fopen("","w");
else
{
fread(yuv2r,255*255*255,1, yuv2bgr_table);
fread(yuv2g,255*255*255,1, yuv2bgr_table);
fread(yuv2b,255*255*255,1, yuv2bgr_table);
printf("yuv2bgr table ok\n");
return;
}
if(yuv2bgr_table ==NULL)
printf("fail open yuv2bgr table\n");
printf("yuv2bgr table ok\n");
for(int y=0;y<255;y++)
for(int u=0;u<255;u++)
for(int v=0;v<255;v++)
{
//yuv转rgb公式
int tmp;
tmp =(1.164*(y-16)+1.519*(v-128));
tmp = tmp>250?255:(tmp<0)?0:tmp;
yuv2r[y][u][v]=(char)tmp;
tmp =1.164*(y-16)-0.392*(u-128)-0.813*(v-128);
tmp = tmp>250?255:(tmp<0)?0:tmp;
yuv2g[y][u][v]=(char)tmp;
tmp =1.164*(y-16)+2.018*(u-128);
tmp = tmp>250?255:(tmp<0)?0:tmp;
yuv2b[y][u][v]=(char)tmp;
}
fwrite(yuv2r,255*255*255,1, yuv2bgr_table);
fwrite(yuv2g,255*255*255,1, yuv2bgr_table);
fwrite(yuv2b,255*255*255,1, yuv2bgr_table);
fclose(yuv2bgr_table);
}
图像显⽰
由于摄像头采集图像与显⽰屏的分辨率不⼀致,需要将图像进⾏缩放。这⾥进⾏缩⼩,⽐较简单,可以直接按整数倍下采样。也可以只索引出图⽚的部分区域进⾏显⽰。