折腾了一个多星期,总算实现了摄像头数据采集并显示到屏幕上,整理一下嵌入式Linux从摄像头获取数据并显示到fb屏幕上的过程。使用尽量少的依赖,只用了v4l2视频设备驱动以及fb驱动,更加深入的理解了计算机底层图像处理的原理。
主要流程
整个处理的主要步骤分为以下:
- Linux v4l2驱动采集摄像头数据。
- 图像格式转换:由于采集到的图像不是能直接用来显示的RGB格式,需要进行格式转换。
- 图像缩放:摄像头采集的图片分辨率与显示屏分辨率不匹配,需要进行图像缩放。
- 图像显示:缩放后的数据写到fb的内存映射上。
v4l2驱动采集摄像头数据
v4l2是Linux内置的视频设备驱动,其采集摄像头数据的流程主要分为以下几步:
- 打开视频设备,使用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;
}
- 获取,设置摄像头参数
相关结构体有:
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;
fmtdesc.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
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.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
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");
}
- 申请帧缓冲队列。
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;
req.count = 4;
req.memory = V4L2_MEMORY_MMAP;
req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
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));
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = 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, buf.m.offset);
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);
}
- 开启视频流
// 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;
- 采集图像
准备工作都做好后,采集图像只要遵循这一个简单的流程就可以了:
VIDIOC_DQBUF buffer出队列==》用映射好的内存地址获取图像数据==》VIDIOC_QBUF buffer入队列
while(!con.interrupt)
{
//6.1 dequeue buffer;
memset(&buf, 0, sizeof(buf));
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = 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);
}
图像处理与显示
图像处理
摄像头采集的图片是YUV4:2:2的格式,即像素按照以下顺序排列:
Y00 | U00, 01 | Y01 | V00, 01 | Y02 | U02, U03 | Y03 | V02, V03 |
---|---|---|---|---|---|---|---|
Y10 | U10, 11 | Y11 | V10, 11 | Y12 | U12, U13 | Y13 | V12, V13 |
… | … | … | … | … | … | … | … |
简单理解起来就是每个像素点都有Y亮度值,而U跟V的色度信号是左右间隔排列。可以把每两个像素看成一组,则每组的两个像素各有各的亮度值,但是分享了U、V色度值。这样做估计是因为人眼对亮度的空间分辨率较为敏感吧。
根据YUV转RGB的公式,可以将图片转为RGB。
由于每一帧都需要进行处理,为了节省解码时间,可以在初始化时将YUV到RGB的映射关系制成一个映射表,生成文件存放起来,这样可以大大提高解码数率。
void init_yuv2bgr()
{
//将YUV转RGB映射输出为文件保存,方便以后每次运行。
FILE* yuv2bgr_table = fopen("yuv2bgr_table.txt", "r");
if(yuv2bgr_table == NULL)
yuv2bgr_table = fopen("yuv2bgr_table.txt", "w");
else
{
fread(yuv2r, 255*255*255, 1, yuv2bgr_table);
fread(yuv2g, 255*255*255, 1