一、摄像头信息初始化
①配置USB摄像头驱动:
如果插上摄像头在/dev中只有video0那说明没有配置成功,需要先配置上再进行后续操作,可以在menuconfig中按照如下配置:
原链接。
②创建结构体以及宏定义
创建出两个结构体,里面包含的是各自后续多需要用到的信息,便于后续代码中区分:
#include <linux/videodev2.h>
#include <stdio.h>
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <linux/ioctl.h>
#include <sys/mman.h>
#include <linux/fb.h>
#include <stdlib.h>
#define FRAME_WIDTH 640
#define FRAME_HEIGHT 480
#define FRAME_RATE 15
struct screen_info{
unsigned int *screen_base;//显示缓冲区在进程地址空间的映射
int width;
int height;
int fb_fd;//屏幕文件描述符
};
struct frame_info{
int fd;
int width;
int height;
float frame_rate;
unsigned char *frm_base[3];//帧缓冲区
};
struct screen_info screen;
struct frame_info frame;
③摄像头相关信息查看以及初始化:
先去检查此摄像头可以输出的所有数据信息,主要包括输出视频格式、分辨率大小、对应分辨率下的可选帧率。不同的视频格式有不同的分辨率,不同的分辨率也能输出不同的可选帧率,建议是多换换搭配,看到觉得合适的搭配选择。USB接口的摄像头通常只有MJPEG格式和YUV422(YUYV)两种格式,这两种格式的具体描述可以自行查找不多赘述,这里我选用的YUV格式。而LCD屏的格式通常是RGB的,也就是说需要进行格式转换。
struct v4l2_capability vcap;
struct v4l2_fmtdesc fmtdesc;
struct v4l2_frmsizeenum frmsize;
struct v4l2_frmivalenum frmival;
struct v4l2_format fmt;
frame.fd = open("/dev/video1",O_RDWR);
if(frame.fd < 0){
fprintf(stderr,"open error %s : %s\r\n","/dev/video1",strerror(errno));
return -1;
}
//检测该设备是否是摄像头设备
ioctl(frame.fd,VIDIOC_QUERYCAP,&vcap);
if(!(V4L2_CAP_VIDEO_CAPTURE & vcap.capabilities)){
fprintf(stderr, "Error: No capture video device!\n");
return -1;
}
//枚举出所有的像素格式
fmtdesc.index = 0;
fmtdesc.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
while (0 == ioctl(frame.fd,VIDIOC_ENUM_FMT,&fmtdesc)){
printf("fmt: %s <0x%x>\r\n",fmtdesc.description,fmtdesc.pixelformat);
fmtdesc.index++;
}
//枚举所有的视频分辨率
frmsize.index = 0;
frmsize.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
//frmsize.pixel_format = V4L2_PIX_FMT_MJPEG;
frmsize.pixel_format = V4L2_PIX_FMT_YUYV;
while (0 == ioctl(frame.fd,VIDIOC_ENUM_FRAMESIZES,&frmsize))
{
printf("frame_size<%d*%d>\r\n", frmsize.discrete.width, frmsize.discrete.height);
frmsize.index++;
}
//获取可采集的视频帧率
frmival.index = 0;
frmival.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
//frmival.pixel_format = V4L2_PIX_FMT_MJPEG;//fps:30
frmival.pixel_format = V4L2_PIX_FMT_YUYV;//fps:15 10 5
frmival.width = FRAME_WIDTH;
frmival.height = FRAME_HEIGHT;
while (0 == ioctl(frame.fd,VIDIOC_ENUM_FRAMEINTERVALS,&frmival))
{
printf("Frame interval<%.2f fps>\r\n", (float)frmival.discrete.denominator / frmival.discrete.numerator);
frmival.index++;
}
//查看当前可视频格式
fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
if(0 > ioctl(frame.fd,VIDIOC_G_FMT,&fmt)){
perror("get frame error!\r\n");
return -1;
}
printf("Raw : width:%d, height:%d format:%d\r\n", fmt.fmt.pix.width, fmt.fmt.pix.height, fmt.fmt.pix.pixelformat);
④设置摄像头配置:
对摄像头设置完后需要对信息再确认一遍,因为如果你所配置的信息摄像头不支持的话那么程序会自己选择一个临近并且合适的配置。
struct v4l2_streamparm streamparm;
fmt.fmt.pix.width = FRAME_WIDTH;
fmt.fmt.pix.height = FRAME_HEIGHT;
//fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_MJPEG;
fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV;
if(0 > ioctl(frame.fd,VIDIOC_S_FMT,&fmt)){
perror("set frame error!\r\n");
return -1;
}
frame.width = fmt.fmt.pix.width;
frame.height = fmt.fmt.pix.height;
printf("Changed : width:%d, height:%d format:%d\r\n", frame.width, frame.height, fmt.fmt.pix.pixelformat);
//获取并设置视频流相关参数
streamparm.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
ioctl(frame.fd,VIDIOC_G_PARM,&streamparm);
if(V4L2_CAP_TIMEPERFRAME & streamparm.parm.capture.capability){
streamparm.parm.capture.timeperframe.numerator = 1;
streamparm.parm.capture.timeperframe.denominator = FRAME_RATE;
if (0 > ioctl(frame.fd, VIDIOC_S_PARM, &streamparm)) {
fprintf(stderr, "ioctl error: VIDIOC_S_PARM: %s\n", strerror(errno));
return -1;
}
}else
fprintf(stderr,"can't set frame\r\n");
frame.frame_rate = (float)streamparm.parm.capture.timeperframe.denominator / streamparm.parm.capture.timeperframe.numerator;
⑤获取输出信息:
关于帧缓冲队列可以参考以下信息:
struct v4l2_requestbuffers reqbuf;
struct v4l2_buffer buf;
enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
//申请帧缓冲
reqbuf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
reqbuf.count = 3;
reqbuf.memory = V4L2_MEMORY_MMAP;
if (0 > ioctl(frame.fd,VIDIOC_REQBUFS,&reqbuf)){
fprintf(stderr, "ioctl error: VIDIOC_REQBUFS: %s\n", strerror(errno));
return -1;
}
//内存映射
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
for (buf.index = 0;buf.index < 3; buf.index++)
{
ioctl(frame.fd,VIDIOC_QUERYBUF,&buf);
frame.frm_base[buf.index] = mmap(NULL,buf.length,PROT_READ|PROT_WRITE,MAP_SHARED,frame.fd,buf.m.offset);
if(MAP_FAILED == frame.frm_base[buf.index]){
perror("mmap error!\r\n");
return -1;
}
}
//入队
for(buf.index=0;buf.index<3;buf.index++){
if(0>ioctl(frame.fd,VIDIOC_QBUF,&buf)){
perror("qbuf erroe\r\n");
return -1;
}
}
//开启视频采集
if(0>ioctl(frame.fd,VIDIOC_STREAMON,&type)){
perror("get video start\r\n");
return -1;
}
printf("size: %d\r\n",buf.length);
二、屏幕初始化:
可以将屏幕初始化这一步最先进行,因为要查看LCD屏的像素结构组成,这将绝对后面的数据格式以及转换、填充格式。比方说我这个屏幕一个像素点占四个字节(32bit),偏移方式是红绿蓝,以此其一个像素点的结构也就是ARGB,此外分辨率大小决定的摄像头分辨率可选择的范围。
static int fb_dev_init(void)
{
struct fb_var_screeninfo fb_var = {0};
struct fb_fix_screeninfo fb_fix = {0};
unsigned long screen_size;
screen.fb_fd = open("/dev/fb0",O_RDWR);
if(0>screen.fb_fd){
fprintf(stderr,"open screen fail:%s\r\n",strerror(errno));
return -1;
}
ioctl(screen.fb_fd,FBIOGET_VSCREENINFO,&fb_var);
ioctl(screen.fb_fd,FBIOGET_FSCREENINFO,&fb_fix);
printf("分辨率: %d*%d\n"
"像素深度 bpp: %d\n"
"一行的字节数: %d\n"
"像素格式: R<%d %d> G<%d %d> B<%d %d>\n",
fb_var.xres, fb_var.yres, fb_var.bits_per_pixel,
fb_fix.line_length,
fb_var.red.offset, fb_var.red.length,
fb_var.green.offset, fb_var.green.length,
fb_var.blue.offset, fb_var.blue.length);
screen_size = fb_fix.line_length * fb_var.yres;
screen.width = fb_var.xres;
screen.height = fb_var.yres;
//显示缓冲区映射到进程地址空间
screen.screen_base = mmap(NULL,screen_size,PROT_READ|PROT_WRITE,MAP_SHARED,screen.fb_fd,0);
if(MAP_FAILED == (void*)screen.screen_base){
perror("screen mmap fail\r\n");
close(screen.fb_fd);
return -1;
}
//将LCD背景刷白
memset(screen.screen_base,0xFF,screen_size);
return 0;
}
三、数据类型转换并显示(重点):
①v4l2的输出数据格式分析:
前面说过,我选的是YUY422这个类型,在videodev2.h中是这样表示的:
#define V4L2_PIX_FMT_YUYV v4l2_fourcc('Y', 'U', 'Y', 'V') /* 16 YUV 4:2:2 */
但是其具体的组成结构一定要查看v4l2的官方手册,否则会导致数据转换是出现差错,链接如下:
从下图可以看到,手册中以4x4的像素图片为例子展示了数据的排列方式,
每一个符号占一个字节,每四个字节表示两个像素点,Cb=U,Cr=V,每两个Y共同使用一组UV,因此,完整的一帧中所输出的信息量大小是分辨率的宽*高*2个字节。
②YUV422转RGB888:
基于以上分析就可以分割出所需的对应数据,这里有个建议就是在前面用于收集yuv的帧缓冲区尽量用unsigned char类型的指针,而不是short或者是int类型指针,因为这样刚好对应所输出的类型格式并且方便取值,不用再去进行位操作,其数据大小也就是分辨率的宽度*2。而用于存入LCD的rgb数据缓冲区则用unsigned int格式,这样能刚好对应上LCD的一个像素大小。
void yuyv_to_rgb32(unsigned char *yuyv,unsigned int *rgb32)
{
unsigned int i;
unsigned int *rgb32_t = rgb32;
unsigned char* y0 = yuyv + 0;
unsigned char* u0 = yuyv + 1;
unsigned char* y1 = yuyv + 2;
unsigned char* v0 = yuyv + 3;
unsigned char r0, g0, b0, r1, g1, b1;
float rt0 = 0, gt0 = 0, bt0 = 0, rt1 = 0, gt1 = 0, bt1 = 0;
for (i=0;i<frame.width/2;i++)
{
//yuv转rgb计算公式
bt0 = 1.164 * (*y0 - 16) + 2.018 * (*u0 - 128);
gt0 = 1.164 * (*y0 - 16) - 0.813 * (*v0 - 128) - 0.394 * (*u0 - 128);
rt0 = 1.164 * (*y0 - 16) + 1.596 * (*v0 - 128);
bt1 = 1.164 * (*y1 - 16) + 2.018 * (*u0 - 128);
gt1 = 1.164 * (*y1 - 16) - 0.813 * (*v0 - 128) - 0.394 * (*u0 - 128);
rt1 = 1.164 * (*y1 - 16) + 1.596 * (*v0 - 128);
//对计算出来的结果进行限幅操作
if(rt0 > 255) rt0 = 255;
else if(rt0< 0) rt0 = 0;
if(gt0 > 255) gt0 = 255;
else if(gt0 < 0) gt0 = 0;
if(bt0 > 255) bt0 = 255;
else if(bt0 < 0) bt0 = 0;
if(rt1 > 255) rt1 = 255;
else if(rt1 < 0) rt1 = 0;
if(gt1 > 255) gt1 = 255;
else if(gt1 < 0) gt1 = 0;
if(bt1 > 255) bt1 = 255;
else if(bt1 < 0) bt1 = 0;
//数据存入
r0=rt0;g0=gt0;b0=gt0;r1=rt1;g1=gt1;b1=bt1;
*(rgb32_t + 0) = r0<<16 | g0<<8 | b0;
*(rgb32_t + 1) = r1<<16 | g1<<8 | b1;
//地址偏移
yuyv = yuyv + 4;
rgb32_t = rgb32_t + 2;
y0 = yuyv;
u0 = yuyv + 1;
y1 = yuyv + 2;
v0 = yuyv + 3;
}
}
③数据写入LCD:
在为rgb缓冲区申请内存时可以用calloc函数来申请,这样申请出来的结果会把里面的数据直接初始化为0(因为是ARGB,所以一定要让前面的A为0,否则可能会影响成像),可以省省去再memset的清零操作,申请的内存大小是视频一行的像素尺寸*4的大小。
在后续的转换中也是转换一行写入一行,如果是将整个一帧的数据全部转换完再写入的话会非常影响最终的观感,视觉帧率会显著降低。
int j = 0;
unsigned int *base;
unsigned char *start;
unsigned int *rgb = NULL;
//内存申请
rgb = (int *)calloc(frame.width,sizeof(int));
if(rgb == NULL){
printf("calloc fail\r\n");
return -1;
}
//循环读取
for(;;){
for(buf.index = 0;buf.index < 3;buf.index++){
//出队
ioctl(frame.fd,VIDIOC_DQBUF,&buf);
for(j = 0,base=screen.screen_base,start=frame.frm_base[buf.index];j<frame.height;j++){
yuyv_to_rgb32(start,rgb);
memcpy(base,rgb,frame.width*4);
base += screen.width;//切换至屏幕的下一行显示
start += frame.width * 2;//切换至视频的下一行输出
}
//入队
ioctl(frame.fd,VIDIOC_QBUF,&buf);
}
四、效果展示:
图中会存在一些三原色的色斑,这个就是类型转换计算的问题了