Camera摄像头工作原理及整机架构
摄像头的基本工作原理
如图所示,一个景象的反射光被镜头所捕捉(镜头的光圈可以调节进光量,马达用来调节对焦),最终将聚焦好的图像精准对焦到图片传感器上边(色彩滤波会产生三基色),光信号转化为数字信号,通过模数转换最终得到原始码流数据。
问题:那我们可以直接使用这个包含图像信息和颜色信息的原始码流数据吗?
答案:不能,要根据用户端请求的VC接口携带请求数据流的格式决定。
编码
摄像头软件层,一般会提供多种格式和分辨率的参数,供上层选择,常见的格式如:YUYV、MJPEG、H264、NV12等。其中:
YUYV:原始码流,每个像素点占2个字节。
MJPEG:可以将数据压缩7倍左右,可以是NV12也可以是YUYV
H264编码:主要看配置,其中I帧压缩7倍左右,P帧20倍左右,B帧50倍左右,理论上B帧越多,就可能支持的高分辨率高帧率的码流
NV12:原始码流,每个像素点1.5个字节。
编码的目的:
如果没有编码,我们计算一下1s请求NV12 4K 30HZ的码流需要多大的带宽?
答案:(384021601.5*30)字节 = 373248000字节=356M
如果没有编码,我们计算一下1s请求NV12 4K 30HZ的码流需要多大的带宽?
答案:(3840*2160*1.5*30)字节 = 373248000字节=356M
按照,我们整机常用的camera接口usb 2.0的理论带宽:480Mbps = 60M/s,
无法满足NV12原始码流4K 30HZ的预览要求的,编解码技术,可以有效的压缩数据的体积而不会或较少的影像画质。
传输
作为相机的数据传输协议,肯定是要统一的,广泛的,厂家和广大开发者支持的协议。其中USB协议肯定有一席之地。整机方案,基本采用的都是USB Camera方案。
UVC是USB Video Class的简写,也就是USB接口的视频设备。一个UVC设备,需包含1个VC Interface和1个或多个VS Interface
VC Interface进行配置参数的传递,如启动和关闭自动对焦,白平衡等。
VS Interface进行图片数据流的传输。
USB协议:
UVC协议:
UVC模型:
UVC软件架构
在Linux系统中,应用层和USB相机通过UVC协议进行交互。系统为了兼容不同的交互协议。在kernel层抽象了V4L2驱动,方便上层进程和各个协议对接。
V4L2提供了一系列的命令,如图所示,上层进程通过ioctl和底层kernel交互。
预备
v4l2介绍:
定义:
Video for Linux two(Video4Linux2)简称V4L2,是V4L的改进版。V4L2是linux操作系统下用于采集图片、视频和音频数据的API接口,配合适当的视频采集设备和相应的驱动程序,可以实现图片、视频、音频等的采集。在远程会议、可视电话、视频监控系统和嵌入式多媒体终端中都有广泛的应用。
在Linux下,所有外设都被看成一种特殊的文件,成为“设备文件”,可以象访问普通文件一样对其进行读写。一般来说,采用V4L2驱动的摄像头设备文是/dev/v4l/video0。为了通用,可以建立一个到/dev/video0的链接。V4L2支持两种方式来采集图像:内存映射方式(mmap)和直接读取方式(read)。V4L2在include/linux/videodev.h文件中定义了一些重要的数据结构,在采集图像的过程中,就是通过对这些数据的操作来获得最终的图像数据。Linux系统V4L2的能力可在Linux内核编译阶段配置,默认情况下都有此开发接口。V4L2从Linux 2.5.x版本的内核中开始出现。
V4L2规范中不仅定义了通用API元素(Common API Elements),图像的格式(Image Formats),输入/输出方法(Input/Output),还定义了Linux内核驱动处理视频信息的一系列接口(Interfaces),这些接口主要有:
视频采集接口——Video Capture Interface;
视频输出接口—— Video Output Interface;
视频覆盖/预览接口——Video Overlay Interface;
视频输出覆盖接口——Video Output Overlay Interface;
编解码接口——Codec Interface。
v4l2结构体介绍
常用的结构体在内核目录include/linux/videodev2.h中定义
struct v4l2_requestbuffers //申请帧缓冲,对应命令VIDIOC_REQBUFS
struct v4l2_capability //视频设备的功能,对应命令VIDIOC_QUERYCAP
struct v4l2_input //视频输入信息,对应命令VIDIOC_ENUMINPUT
struct v4l2_standard //视频的制式,比如PAL,NTSC,对应命令VIDIOC_ENUMSTD
struct v4l2_format //帧的格式,对应命令VIDIOC_G_FMT、VIDIOC_S_FMT等
struct v4l2_buffer //驱动中的一帧图像缓存,对应命令VIDIOC_QUERYBUF
struct v4l2_crop //视频信号矩形边框
v4l2_std_id //视频制式
常用结构体
v4l2_capability
/* 其中域 capabilities 代表设备支持的操作模式,常见的值有 V4L2_CAP_VIDEO_CAPTURE | V4L2_CAP_STREAMING 表示是一个视频捕捉设备并且具有数据流控制模式;另外 driver 域需要和 struct video_device 中的 name 匹配。*/
struct v4l2_capability {
__u8 driver[16]; // 驱动名字
__u8 card[32]; // 设备名字
__u8 bus_info[32]; // 设备在系统中的位置
__u32 version; // 驱动版本号
__u32 capabilities; // 设备支持的操作
__u32 device_caps; // 设备的功能或特性。
__u32 reserved[3]; // 保留字段
};
v4l2_format
/**
* struct v4l2_format - 设备格式结构体
*
* 用于描述视频设备的不同数据格式。
*
* @type: 缓冲区类型
* @fmt: 格式联合体,根据缓冲区类型选择具体格式
*/
struct v4l2_format
{
enum v4l2_buf_type type;
union
{
struct v4l2_pix_format pix; /* V4L2_BUF_TYPE_VIDEO_CAPTURE */
struct v4l2_window win; /* V4L2_BUF_TYPE_VIDEO_OVERLAY */
struct v4l2_vbi_format vbi; /* V4L2_BUF_TYPE_VBI_CAPTURE */
struct v4l2_sliced_vbi_format sliced; /* V4L2_BUF_TYPE_SLICED_VBI_CAPTURE */
__u8 raw_data[200]; /* 用户自定义数据 */
} fmt;
};
/**
* enum v4l2_buf_type - 缓冲区类型枚举
*
* 定义了视频设备支持的不同缓冲区类型。
*/
enum v4l2_buf_type
{
V4L2_BUF_TYPE_VIDEO_CAPTURE = 1, /* 视频捕获 */
V4L2_BUF_TYPE_VIDEO_OUTPUT = 2, /* 视频输出 */
V4L2_BUF_TYPE_VIDEO_OVERLAY = 3, /* 视频覆盖 */
... V4L2_BUF_TYPE_PRIVATE = 0x80, /* 私有类型 */
};
/**
* struct v4l2_pix_format - 像素格式结构体
*
* 用于描述视频捕获设备的像素格式。
*
* @width: 图像宽度
* @height: 图像高度
* @pixelformat: 像素格式
* @field: 场序
* @bytesperline: 每行字节数
* @sizeimage: 图像大小
* @colorspace: 色彩空间
* @priv: 私有数据
*/
struct v4l2_pix_format
{
__u32 width; //视频的宽
__u32 height; //视频的高
__u32 pixelformat; //视频数据格式(常见的值有 V4L2_PIX_FMT_YUV422P | V4L2_PIX_FMT_RGB565)
enum v4l2_field field;
__u32 bytesperline; /* 用于填充,未使用时为0 一行图像占用的字节数 */
__u32 sizeimage; //像占用的总字节数
enum v4l2_colorspace colorspace; //指定设备的颜色空间
__u32 priv; /* 私有数据,取决于像素格式 */
};
v4l2_requestbuffers
//struct v4l2_requestbuffers 与 VIDIOC_REQBUFS ,VIDIOC_REQBUFS 命令通过结构 v4l2_requestbuffers 请求驱动申请一片连续的内存用于缓存视频信息:
/* 用于请求视频设备的缓冲区 */
struct v4l2_requestbuffers
{
__u32 count; // 指定需要申请的缓冲区数量
enum v4l2_buf_type type; // 指定缓冲区的类型,如视频捕捉、视频输出等
enum v4l2_memory memory; // 指定内存的类型,决定了如何访问缓冲区
__u32 reserved[2]; // 保留字段,供未来使用或对齐用
};
/* 用于指定视频缓冲区的内存类型 */
enum v4l2_memory
{
V4L2_MEMORY_MMAP = 1, // 通过mmap系统调用将设备内存映射到用户空间
V4L2_MEMORY_USERPTR = 2, // 用户指针,用户空间地址直接作为缓冲区
V4L2_MEMORY_OVERLAY = 3, // 用于X11 Overlay的内存类型
};
v4l2_buffer
/*
* v4l2_buffer 结构体定义,用于描述视频设备的缓冲区属性
*/
struct v4l2_buffer
{
__u32 index; // 缓冲区索引,标识具体的缓冲区
enum v4l2_buf_type type; // 缓冲区类型,如视频捕捉、视频输出等
__u32 bytesused; // 已使用的字节数,表示缓冲区中已填充的数据量
__u32 flags; // 缓冲区标志,用于标记缓冲区的状态,如请求更多数据等
enum v4l2_field field; // 电视场类型,用于视频信号的场同步
struct timeval timestamp; // 时间戳,记录缓冲区数据的捕获或生成时间
struct v4l2_timecode timecode; // 时间码,用于同步记录视频数据的时间信息
__u32 sequence; // 序列号,用于标识数据包的顺序
enum v4l2_memory memory; // 内存位置类型,标识缓冲区数据存储的位置,如内存或用户指针
union
{
__u32 offset; // 内存偏移地址,如果内存类型为mmap
unsigned long userptr; // 用户指针,如果内存类型为userptr
} m;
__u32 length; // 缓冲区长度,表示分配给缓冲区的总字节数
__u32 input; // 输入选择器,用于多路输入设备选择具体的输入通道
__u32 reserved; // 预留字段,用于未来扩展或对齐
};
常用IOCTL
用户空间对V4L2设备的操作基本都是ioctl来实现的,V4L2设备都有大量可操作的功能(配置寄存器),所以V4L2的ioctl也是十分庞大的。
常用的IOCTL接口命令也在include/linux/videodev2.h中定义
VIDIOC_REQBUFS //分配内存
VIDIOC_QUERYBUF //把VIDIOC_REQBUFS中分配的数据缓存转换成物理地址
VIDIOC_QUERYCAP //查询驱动功能
VIDIOC_ENUM_FMT //获取当前驱动支持的视频格式
VIDIOC_S_FMT //设置当前驱动的频捕获格式
VIDIOC_G_FMT //读取当前驱动的频捕获格式
VIDIOC_TRY_FMT //验证当前驱动的显示格式
VIDIOC_CROPCAP //查询驱动的修剪能力
VIDIOC_S_CROP //设置视频信号的矩形边框
VIDIOC_G_CROP //读取视频信号的矩形边框
VIDIOC_QBUF //把数据从缓存中读取出来
VIDIOC_DQBUF //把数据放回缓存队列
VIDIOC_STREAMON //开始视频显示函数
VIDIOC_STREAMOFF //结束视频显示函数
VIDIOC_QUERYSTD //检查当前视频设备支持的标准,例如PAL或NTSC。
工作流程
打开设备-> 检查和设置设备属性-> 设置帧格式-> 设置一种输入输出方法(缓冲 区管理)-> 循环获取数据-> 关闭设备
(1)打开设备文件
打开视频设备非常简单,在V4L2中,视频设备被看做一个文件。使用open函数打开这个设备:
1. 用非阻塞模式打开摄像头设备
int cameraFd;
cameraFd = open("/dev/video0", O_RDWR | O_NONBLOCK);
2. 如果用阻塞模式打开摄像头设备,上述代码变为:
cameraFd = open("/dev/video0", O_RDWR);
关于阻塞模式和非阻塞模式
应用程序能够使用阻塞模式或非阻塞模式打开视频设备,如果使用非阻塞模式调用视频设备,即使尚未捕获到信息,驱动依旧会把缓存(DQBUFF)里的东西返回给应用程序。
(2)取得设备的capability
看看设备具有什么功能,比如是否具有视频输入特性。
struct v4l2_capability capability;
int ret = ioctl(fd, VIDIOC_QUERYCAP, &capability);
struct v4l2_capability cap;
memset(&cap, 0, sizeof(cap));
/* 获取设备支持的操作 */
if (ioctl(dev->fd, VIDIOC_QUERYCAP, &cap) < 0)
{
if (EINVAL == errno)
{
/*EINVAL为返回的错误值*/
printf(stderr, "%s is no V4L2 device\n", dev->dev);
return TFAIL;
}
else
{
printf(stderr,
"%s is not V4L2 device,unknow error\n", dev->dev);
return TFAIL;
}
}
// 获取成功,检查是否有视频捕获功能
if (!(cap.capabilities & V4L2_CAP_VIDEO_CAPTURE))
{
printf(stderr,"%s is no video capture device\n", dev->dev);
return TFAIL;
}
/* streaming I/O ioctls */
if (!(cap.capabilities & V4L2_CAP_STREAMING))
{
printf(stderr,"%s does not support streaming i/o\n", dev->dev);
return TFAIL;
}
(3)选择视频输入
一个视频设备可以有多个视频输入。如果只有一路输入,这个功能可以没有。VIDIOC_G_INPUT 和 VIDIOC_S_INPUT 用来查询和选则当前的 input,一个 video 设备节点可能对应多个视频源,比如 saf7113 可以最多支持四路 cvbs 输入,如果上层想在四个cvbs视频输入间切换,那么就要调用 ioctl(fd, VIDIOC_S_INPUT, &input) 来切换。VIDIOC_G_INPUT and VIDIOC_G_OUTPUT 返回当前的 video input和output的index.
struct v4l2_input input;
……初始化input
int ret = ioctl(fd, VIDIOC_QUERYCAP, &input);
struct v4l2_input
{
__u32 index; /* Which input * /
__u8 name[32]; /* Label */
__u32 type; /* Type of input */
__u32 audioset; /* Associated audios (bitfield) */
__u32 tuner; /* Associated tuner */
v4l2_std_id std;
__u32 status;
__u32 reserved[4];
};
(4)检测视频支持的制式
v4l2_std_id std;
do
{
ret = ioctl(fd, VIDIOC_QUERYSTD, &std);
}while (ret == -1 && errno == EAGAIN);
switch (std)
{
case V4L2_STD_NTSC:
// ……
case V4L2_STD_PAL:
// ……
}
(5)设置视频捕获格式
v4l2_format 结构体用来设置摄像头的视频制式、帧格式等,在设置这个参数时应先填 好 v4l2_format 的各个域
struct v4l2_format fmt;
memset(&fmt, 0, sizeof(fmt));
fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; //(传输流类型)
fmt.fmt.pix.width = g_display_width; // 宽
fmt.fmt.pix.height = g_display_height; // 高
fmt.fmt.pix.pixelformat = g_fmt; // 采样类型,如 YUV4:2:2
fmt.fmt.pix.field = V4L2_FIELD_INTERLACED; // 采样区域,如隔行采样
/* 设置设备捕获视频的格式 */
if(ioctl(dev->fd, VIDIOC_S_FMT, &fmt) < 0)
{
printf(stderr, "%s iformat not supported \n",dev->dev);
close(dev->fd);
return TFAIL;
}
注意:如果该视频设备驱动不支持你所设定的图像格式,视频驱动会重新修改struct v4l2_format结构体变量的值为该视频设备所支持的图像格式,所以在程序设计中,设定完所有的视频格式后,要获取实际的视频格式,要重新读取struct v4l2_format结构体变量。
(6)向驱动申请帧缓存
一般不超过5个,CAP_BUF_NUM = 4
v4l2_requestbuffers结构中定义了缓存的数量,驱动会据此申请对应数量的视频缓存。多个缓存可以用于建立FIFO,来提高视频采集的效率。控制命令VIDIOC_REQBUFS
功能: 请求V4L2驱动分配视频缓冲区(申请V4L2视频驱动分配内存),V4L2是视频设备的驱动层,位于内核空间,所以通过VIDIOC_REQBUFS控制命令字申请的内存位于内核空间,应用程序不能直接访问,需要通过调用mmap内存映射函数把内核空间内存映射到用户空间后,应用程序通过访问用户空间地址来访问内核空间。
参数说明:参数类型为V4L2的申请缓冲区数据结构体类型struct v4l2_requestbuffers ;
返回值说明: 执行成功时,函数返回值为 0;V4L2驱动层分配好了视频缓冲区;
struct v4l2_requestbuffers req;
/* 申请设备的缓存区 */
memset(&req, 0, sizeof(req));
req.count = CAP_BUF_NUM; // 申请一个拥有四个缓冲帧的缓冲区
req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
req.memory = V4L2_MEMORY_MMAP;
if (ioctl(dev->fd, VIDIOC_REQBUFS, &req) < 0)
{
if (EINVAL == errno)
{
printf(stderr, "%s does not support "
"memory mapping\n",
dev->dev);
return TFAIL;
}
else
{
printf(stderr, "%s does not support "
"memory mapping, unknow error\n",
dev->dev);
return TFAIL;
}
}
if (req.count < 2)
{
printf(stderr, "Insufficient buffer memory on %s\n",
dev->dev);
return TFAIL;
}
(7)获取每个缓存的信息,并mmap到用户空间
应用程序和设备有三种交换数据的方法,直接 read/write、内存映射(memory mapping)和用户指针。这里只讨论内存映射(memory mapping)。
typedef struct VideoBuffer // 定义一个结构体来映射每个缓冲帧
{
void *start;
size_t length;
} VideoBuffer;
VideoBuffer *buffers = calloc(req.count, sizeof(*buffers));
struct v4l2_buffer buf;
for (numBufs = 0; numBufs < req.count; numBufs++) // 映射所有的缓存
{
memset(&buf, 0, sizeof(buf));
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
buf.index = numBufs;
if (ioctl(fd, VIDIOC_QUERYBUF, &buf) == -1)
{ // 获取到对应index的缓存信息,此处主要利用length信息及offset信息来完成后面的mmap操作。
return -1;
}
buffers[numBufs].length = buf.length;
// 转换成相对地址
buffers[numBufs].start = mmap(NULL, buf.length,
PROT_READ | PROT_WRITE,
MAP_SHARED,
fd, buf.m.offset);
if (buffers[numBufs].start == MAP_FAILED)
{
return -1;
}
// addr 映射起始地址,一般为NULL ,让内核自动选择
// length 被映射内存块的长度
// prot 标志映射后能否被读写,其值为PROT_EXEC,PROT_READ,PROT_WRITE, PROT_NONE
// flags 确定此内存映射能否被其他进程共享,MAP_SHARED,MAP_PRIVATE
// fd,offset, 确定被映射的内存地址 返回成功映射后的地址,不成功返回MAP_FAILED ((void*)-1)
int munmap(void *addr, size_t length); // 断开映射
// addr 为映射后的地址,length 为映射后的内存长度
(8)开始采集视频 (在缓冲区处理好之后就可以获得视频了 )
在开始之前,还应当把缓冲帧放入缓冲队列,应用程序和设备有三种交换数据的方法,直接 read/write、内存映射(memory mapping)和用户指针。这里只讨论内存映射(memory mapping)。
// 把四个缓冲帧放入队列
for (i = 0; i < CAP_BUF_NUM; i++)
{
memset(&buf, 0, sizeof(buf));
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
buf.index = i;
buf.m.offset = dev->buffer[i].offset;
/* 将空闲的内存加入可捕获视频的队列 */
if (ioctl(dev->fd, VIDIOC_QBUF, &buf) < 0)
{
printf("ERROR: VIDIOC_QBUF[%s], FUNC[%s], LINE[%d]\n", dev->dev, __FUNCTION__, __LINE__);
return TFAIL;
}
}
type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
/* 打开设备视频流 */
if (ioctl(dev->fd, VIDIOC_STREAMON, &type) < 0)
{
printf("ERROR: VIDIOC_STREAMON[%s], FUNC[%s], LINE[%d]\n", dev->dev, __FUNCTION__, __LINE__);
return TFAIL;
}
前期初始化完成后,只是解决了一帧视频数据的格式和大小问题,而连续视频帧数据的采集需要用帧缓冲区队列的方式来解决,即要通过驱动程序在内存中申请几个帧缓冲区来存放视频数据。
应用程序通过API接口提供的方法(VIDIOC_REQBUFS)申请若干个视频数据的帧缓冲区,申请帧缓冲区数量一般不低于3个,每个帧缓冲区存放一帧视频数据,这些帧缓冲区在内核空间。
应用程序通过API接口提供的查询方法(VIDIOC_QUERYBUF)查询到帧缓冲区在内核空间的长度和偏移量地址。
应用程序再通过内存映射方法(mmap),将申请到的内核空间帧缓冲区的地址映射到用户空间地址,这样就可以直接处理帧缓冲区的数据。
(1)将帧缓冲区在视频输入队列排队,并启动视频采集
在驱动程序处理视频的过程中,定义了两个队列:视频采集输入队列(incoming queues)和视频采集输出队列(outgoing queues),前者是等待驱动存放视频数据的队列,后者是驱动程序已经放入了视频数据的队列。如图2所示。
应用程序需要将上述帧缓冲区在视频采集输入队列排队(VIDIOC_QBUF),然后可启动视频采集。
(2)循环往复,采集连续的视频数据
启动视频采集后,驱动程序开始采集一帧数据,把采集的数据放入视频采集输入队列的第一个帧缓冲区,一帧数据采集完成,也就是第一个帧缓冲区存满一帧数据后,驱动程序将该帧缓冲区移至视频采集输出队列,等待应用程序从输出队列取出。驱动程序接下来采集下一帧数据,放入第二个帧缓冲区,同样帧缓冲区存满下一帧数据后,被放入视频采集输出队列。
应用程序从视频采集输出队列中取出含有视频数据的帧缓冲区,处理帧缓冲区中的视频数据,如存储或压缩。
最后,应用程序将处理完数据的帧缓冲区重新放入视频采集输入队列,这样可以循环采集
(9)取出FIFO缓存中已经采样的帧缓存
structv4l2_buffer capture_buf;
memset(&capture_buf, 0,sizeof(capture_buf));
capture_buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
capture_buf.memory = V4L2_MEMORY_MMAP;
/* 将已经捕获好视频的内存拉出已捕获视频的队列 */
if (ioctl(dev.fd, VIDIOC_DQBUF, &capture_buf) < 0)
{
printf("ERROR: VIDIOC_DQBUF[%s], FUNC[%s], LINE[%d]\n", dev, __FUNCTION__, __LINE__);
return TFAIL;
}
image_data_handle(buffer[capture_buf.index].start, capture_buf.bytesused);
(10)将刚刚处理完的缓冲重新入队列尾,这样可以循环采集
if (ioctl(fd, VIDIOC_QBUF, &buf) == -1) {
return -1;
}
(11)停止视频的采集,解除映射
int ret = ioctl(fd, VIDIOC_STREAMOFF, &buf_type);
munmap(buffer[j].start, buffer[j].length);
(12)关闭视频设备
close(fd);
评论