本文为学习正点原子的V4L2 摄像头应用编程学习笔记!如有侵权,联系删除!装载请标明出处《正点原子》!
V4L2 摄像头应用编程
我使用的 ALPHA/Mini I.MX6U 开发板, USB 免驱摄像头,插入板子的USB接口即可(笔者用的–方便便宜)!
配套支持多种不同的摄像头:
包括正点原子的 ov5640(500W 像素)、ov2640(200W 像素)以及 ov7725(不带 FIFO、30W 像素)这三款摄像头,在开发板出厂系统上,可以使用这些摄像头(要自己写驱动玩啥的有能力可以买一个,支持开源祖师正点原子!!!但个人觉得驱动框架能看懂就行,出厂的基本上都是已经写好的,具备在原厂协助下更改的能力就很棒!);
重难点:
--设置帧格式、帧率(多且杂、细究是有它自己的架构的-----毕竟驱动框架摆在那);
--帧缓冲队列:读取图像数据的过程其实就是一个不断地出队列和入队列的过程!
本章我们就来学习 Linux 下的摄像头应用编程。
⚫ USB免驱摄像头测试;
⚫ V4L2 简介;
⚫ V4L2 设备应用编程介绍;
⚫ 摄像头应用编程实战;
USB免驱摄像头测试
#video2 是 USB 摄像头节点
ls /dev/video*
#查看 USB 摄像头支持的格式
v4l2-ctl --device=/dev/video2 --list-formats-ext
#使用 gstreamer 采集(不能设置 MJPG 格式采集)
gst-launch-1.0 -v imxv4l2src device=/dev/video2 ! "video/x-raw, format=(string)YUY2, \
width=(int)640, height=(int)480, framerate=(fraction)15/1" ! imxv4l2sink
#保存录像
gst-launch-1.0 -vvv -e v4l2src num-buffers=1000 device=/dev/video2 ! image/jpeg,width=640, \
height=480,framerate=30/1,rate=30 ! matroskamux ! filesink location=video.mkv
#使用 ffmpeg 采集录像
ffmpeg -t 10 -s 320x240 -pix_fmt yuyv422 -i /dev/video2 video.avi
#命令直接播放即可
gst-play-1.0 或者 gplay-1.0 + 视频文件
查看信息:
视频实时显示:
V4L2 简介
V4L2(Video for linux two) 是 Linux 内核中视频类设备的一套驱动框架,为视频类设备驱动开发和应用层提供了一套统一的接口规范;
视频类设备:一个非常典型的视频类设备就是视频采集设备,譬如各种摄像头;当然还包括其它类型视频类设备。
V4L2 摄像头应用程序
V4L2 设备驱动框架向应用层提供了一套统一、标准的接口规范,应用程序按照该接口规范来进行应用
编程,从而使用摄像头。对于摄像头设备来说,其编程模式如下所示:
1. 首先是打开摄像头设备;
2. 查询设备的属性或功能;
3. 设置设备的参数,譬如像素格式、帧大小、帧率;
4. 申请帧缓冲、内存映射;
5. 帧缓冲入队;
6. 开启视频采集;
7. 帧缓冲出队、对采集的数据进行处理;
8. 处理完后,再次将帧缓冲入队,往复;
9. 结束采集。
从流程图可看到,几乎对摄像头所有操作都通过 ioctl()完成,搭配不同 V4L2 指令(request参数)请求不同的操作,这些指令定义在头文件 linux/videodev2.h 中,以宏定义的形式提供(VIDIOC_XXX);
实际的应用编程中,并不是所有的指令都会用到,针对视频采集类设备,以下笔者列出了一些常用的指令:
1. 打开摄像头
视频类设备对应的设备节点为/dev/videoX,X 为数字编号,通常从 0 开始;
摄像头应用编程的第一步便是打开设备,调用 open 打开,得到文件描述符 fd,如下所示:
int fd = -1;
/* 打开摄像头,打开设备文件时,需要使用 O_RDWR 指定读权限和写权限。*/
fd = open("/dev/video0", O_RDWR);
if (0 > fd)
{
fprintf(stderr, "open error: %s: %s\n", "/dev/video0", strerror(errno));
return -1;
}
2. 查询设备属性/能力/功能
查询设备的属性,使用的指令为 VIDIOC_QUERYCAP;
/*通过 ioctl()将获取到一个 struct v4l2_capability 类型数据*/
ioctl(int fd, VIDIOC_QUERYCAP, struct v4l2_capability *cap);
struct v4l2_capability 数据结构描述了设备的一些属性
/*示例代码 25.2.1 struct v4l2_capability 结构体*/
struct v4l2_capability {
__u8 driver[16]; /* 驱动的名字 */
__u8 card[32]; /* 设备的名字 */
__u8 bus_info[32]; /* 总线的名字 */
__u32 version; /* 版本信息 */
__u32 capabilities; /* 设备拥有的能力 */
__u32 device_caps;
__u32 reserved[3]; /* 保留字段 */
};
们重点关注的是 capabilities 字段,该字段描述了设备拥有的能力,该字段的值如下
(可以是以下任意一个值或多个值的位或关系):
/* Values for 'capabilities' field : videodev2.h头文件中所定义的 */
#define V4L2_CAP_VIDEO_CAPTURE 0x00000001 /* Is a video capture device */
#define V4L2_CAP_VIDEO_OUTPUT 0x00000002 /* Is a video output device */
#define V4L2_CAP_VIDEO_OVERLAY 0x00000004 /* Can do video overlay */
#define V4L2_CAP_VBI_CAPTURE 0x00000010 /* Is a raw VBI capture device */
#define V4L2_CAP_VBI_OUTPUT 0x00000020 /* Is a raw VBI output device */
#define V4L2_CAP_SLICED_VBI_CAPTURE 0x00000040 /* Is a sliced VBI capture device */
#define V4L2_CAP_SLICED_VBI_OUTPUT 0x00000080 /* Is a sliced VBI output device */
#define V4L2_CAP_RDS_CAPTURE 0x00000100 /* RDS data capture */
#define V4L2_CAP_VIDEO_OUTPUT_OVERLAY 0x00000200 /* Can do video output overlay */
#define V4L2_CAP_HW_FREQ_SEEK 0x00000400 /* Can do hardware frequency seek */
#define V4L2_CAP_RDS_OUTPUT 0x00000800 /* Is an RDS encoder */
/* Is a video capture device that supports multiplanar formats */
#define V4L2_CAP_VIDEO_CAPTURE_MPLANE 0x00001000
/* Is a video output device that supports multiplanar formats */
#define V4L2_CAP_VIDEO_OUTPUT_MPLANE 0x00002000
/* Is a video mem-to-mem device that supports multiplanar formats */
#define V4L2_CAP_VIDEO_M2M_MPLANE 0x00004000
/* Is a video mem-to-mem device */
#define V4L2_CAP_VIDEO_M2M 0x00008000
#define V4L2_CAP_TUNER 0x00010000 /* has a tuner */
#define V4L2_CAP_AUDIO 0x00020000 /* has audio support */
#define V4L2_CAP_RADIO 0x00040000 /* is a radio device */
#define V4L2_CAP_MODULATOR 0x00080000 /* has a modulator */
#define V4L2_CAP_SDR_CAPTURE 0x00100000 /* Is a SDR capture device */
#define V4L2_CAP_EXT_PIX_FORMAT 0x00200000 /* Supports the extended pixel format */
#define V4L2_CAP_SDR_OUTPUT 0x00400000 /* Is a SDR output device */
#define V4L2_CAP_META_CAPTURE 0x00800000 /* Is a metadata capture device */
#define V4L2_CAP_READWRITE 0x01000000 /* read/write systemcalls */
#define V4L2_CAP_ASYNCIO 0x02000000 /* async I/O */
#define V4L2_CAP_STREAMING 0x04000000 /* streaming I/O ioctls */
#define V4L2_CAP_TOUCH 0x10000000 /* Is a touch device */
#define V4L2_CAP_DEVICE_CAPS 0x80000000 /* sets device capabilities field */
对于摄像头设备来说,它的capabilities字段必须包含 V4L2_CAP_VIDEO_CAPTURE,表示它支持视频采集功能。所以我们可通过判断 capabilities字段是否包含 V4L2_CAP_VIDEO_CAPTURE、来确定它是否是一个摄像头设备
/* 查询设备功能 */
ioctl(fd, VIDIOC_QUERYCAP, &vcap);
/* 判断是否是视频采集设备 */
if (!(V4L2_CAP_VIDEO_CAPTURE & vcap.capabilities))
{
fprintf(stderr, "Error: No capture video device!\n");
return -1;
}
3. 设置帧格式、帧率
a)枚举出摄像头支持的所有像素格式: VIDIOC_ENUM_FMT
b)枚举摄像头所支持的所有视频采集分辨率: VIDIOC_ENUM_FRAMESIZES
c)枚举摄像头所支持的所有视频采集帧率: VIDIOC_ENUM_FRAMEINTERVALS
d)查看或设置当前的格式: VIDIOC_G_FMT、VIDIOC_S_FMT
e)设置或获取当前的流类型相关参数: VIDIOC_G_PARM、VIDIOC_S_PARM
一个摄像头通常会支持多种不同的像素格式,譬如 RGB、YUYV 以及压缩格式 MJPEG 等,
并且还支持多种不同的视频采集分辨率,譬如 640480、320240、1280*720 等,
除此之外,同一分辨率可能还支持多种不同的视频采集帧率(15fps、30fps)
3.1枚举出摄像头支持的所有像素格式:VIDIOC_ENUM_FMT
使用 VIDIOC_ENUM_FMT 指令知道该设备支持哪些像素格式:
/*
使用 VIDIOC_ENUM_FMT 可以枚举出设备所支持的所有像素格式,
调用 ioctl()需要传入一个 struct v4l2_fmtdesc *指针,
ioctl()会将获取到的数据写入到 fmtdesc 指针所指向的对象中
*/
ioctl(int fd, VIDIOC_ENUM_FMT, struct v4l2_fmtdesc *fmtdesc);
struct v4l2_fmtdesc 结构体描述了像素格式相关的信息:
/*示例代码 25.2.2 struct v4l2_fmtdesc 结构体*/
/* F O R M A T E N U M E R A T I O N */
struct v4l2_fmtdesc {
__u32 index; /* Format number */
__u32 type; /* enum v4l2_buf_type */
__u32 flags;
__u8 description[32]; /* Description string */
__u32 pixelformat; /* Format fourcc */
__u32 reserved[4];
};
index 表示编号,在枚举之前,需将其设置为 0,然后每次 ioctl()调用之后将其值加 1。
一次 ioctl()调用只能得到一种像素格式的信息,如果设备支持多种像素格式,则需要循环调用多次,通过 index 来控制,index 从 0 开始,调用一次 ioctl()之后加 1,直到 ioctl()调用失败,表示已经将所有像素格式都枚举出来了;
所以 index 就是一个编号,获取 index 编号指定的像素格式。
description 字段是一个简单地描述性字符串,简单描述 pixelformat 像素格式。
pixelformat 字段则是对应的像素格式编号,这是一个无符号 32 位数据,每一种像素格式都会使用一个u32 类型数据来表示,如下所示:
/*
可以自己查看videodev2.h 头文件:
可以看到后面有一个 v4l2_fourcc 宏,其实就是通过这个宏以及对应的参数合成的一个u32 类型数据。
*/
/* RGB formats */
#define V4L2_PIX_FMT_RGB332 v4l2_fourcc('R', 'G', 'B', '1') /* 8 RGB-3-3-2 */
#define V4L2_PIX_FMT_RGB444 v4l2_fourcc('R', '4', '4', '4') /* 16 xxxxrrrr ggggbbbb */
#define V4L2_PIX_FMT_ARGB444 v4l2_fourcc('A', 'R', '1', '2') /* 16 aaaarrrr ggggbbbb */
#define V4L2_PIX_FMT_XRGB444 v4l2_fourcc('X', 'R', '1', '2') /* 16 xxxxrrrr ggggbbbb */
#define V4L2_PIX_FMT_RGB555 v4l2_fourcc('R', 'G', 'B', 'O') /* 16 RGB-5-5-5 */
#define V4L2_PIX_FMT_ARGB555 v4l2_fourcc('A', 'R', '1', '5') /* 16 ARGB-1-5-5-5 */
#define V4L2_PIX_FMT_XRGB555 v4l2_fourcc('X', 'R', '1', '5') /* 16 XRGB-1-5-5-5 */
#define V4L2_PIX_FMT_RGB565 v4l2_fourcc('R', 'G', 'B', 'P') /* 16 RGB-5-6-5 */
......
/* Grey formats */
#define V4L2_PIX_FMT_GREY v4l2_fourcc('G', 'R', 'E', 'Y') /* 8 Greyscale */
#define V4L2_PIX_FMT_Y4 v4l2_fourcc('Y', '0', '4', ' ') /* 4 Greyscale */
#define V4L2_PIX_FMT_Y6 v4l2_fourcc('Y', '0', '6', ' ') /* 6 Greyscale */
#define V4L2_PIX_FMT_Y10 v4l2_fourcc('Y', '1', '0', ' ') /* 10 Greyscale */
......
/* Luminance+Chrominance formats */
#define V4L2_PIX_FMT_YUYV v4l2_fourcc('Y', 'U', 'Y', 'V') /* 16 YUV 4:2:2 */
#define V4L2_PIX_FMT_YYUV v4l2_fourcc('Y', 'Y', 'U', 'V') /* 16 YUV 4:2:2 */
#define V4L2_PIX_FMT_YVYU v4l2_fourcc('Y', 'V', 'Y', 'U') /* 16 YVU 4:2:2 */
#define V4L2_PIX_FMT_UYVY v4l2_fourcc('U', 'Y', 'V', 'Y') /* 16 YUV 4:2:2 */
......
/* compressed formats */
#define V4L2_PIX_FMT_MJPEG v4l2_fourcc('M', 'J', 'P', 'G') /* Motion-JPEG */
#define V4L2_PIX_FMT_JPEG v4l2_fourcc('J', 'P', 'E', 'G') /* JFIF JPEG */
#define V4L2_PIX_FMT_DV v4l2_fourcc('d', 'v', 's', 'd') /* 1394 */
#define V4L2_PIX_FMT_MPEG v4l2_fourcc('M', 'P', 'E', 'G') /* MPEG-1/2/4 Multiplexed */
type 字段指定类型,表示我们要获取设备的哪种功能对应的像素格式,因为有些设备它可能即支持视
频采集功能、又支持视频输出等其它的功能;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_VBI_CAPTURE = 4,
V4L2_BUF_TYPE_VBI_OUTPUT = 5,
V4L2_BUF_TYPE_SLICED_VBI_CAPTURE = 6,
V4L2_BUF_TYPE_SLICED_VBI_OUTPUT = 7,
V4L2_BUF_TYPE_VIDEO_OUTPUT_OVERLAY = 8,
V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE = 9,
V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE = 10,
V4L2_BUF_TYPE_SDR_CAPTURE = 11,
V4L2_BUF_TYPE_SDR_OUTPUT = 12,
V4L2_BUF_TYPE_META_CAPTURE = 13,
/* Deprecated, do not use */
V4L2_BUF_TYPE_PRIVATE = 0x80,
};
type 字 段 需 要 在 调 用 ioctl() 之 前 设 置 它 的 值 :
对于摄像头,需将type字段设置为V4L2_BUF_TYPE_VIDEO_CAPTURE,指定要获取的是视频采集的像素格式。
struct v4l2_fmtdesc fmtdesc;
/* 枚举出摄像头所支持的所有像素格式以及描述信息 */
fmtdesc.index = 0;
fmtdesc.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
while (0 == ioctl(fd, VIDIOC_ENUM_FMT, &fmtdesc))
{
printf("fmt: %s <0x%x>\n", fmtdesc.description, fmtdesc.pixelformat);
fmtdesc.index++;
}
3.2 枚举摄像头所支持的所有视频采集分辨率:VIDIOC_ENUM_FRAMESIZES
使用 VIDIOC_ENUM_FRAMESIZES 指令可以枚举出设备所支持的所有视频采集分辨率,用法如下所示:
ioctl(int fd, VIDIOC_ENUM_FRAMESIZES, struct v4l2_frmsizeenum *frmsize);
struct v4l2_frmsizeenum 结构体描述了视频帧大小相关的信息:
struct v4l2_frmsizeenum
{
__u32 index; /* Frame size number */
__u32 pixel_format; /* 像素格式 */
__u32 type; /* type */
union
{ /* Frame size */
struct v4l2_frmsize_discrete discrete;
struct v4l2_frmsize_stepwise stepwise;
};
__u32 reserved[2]; /* Reserved space for future use */
};
struct v4l2_frmsize_discrete
{
__u32 width; /* Frame width [pixel] */
__u32 height; /* Frame height [pixel] */
};
index** 字段与 struct v4l2_fmtdesc 结构体的 index 字段意义相同,一个摄像头通常支持多种不同的视频采
集分辨率,一次 ioctl()调用只能得到一种视频帧大小信息,如果设备支持多种视频帧大小,则需要循环调用
多次,通过 index 来控制。
**pixel_format** 字段指定像素格式,而 type 字段与 struct v4l2_fmtdesc 结构体的 type 字段意义相同;在调用 ioctl()之前,需要先设置 type 字段与 pixel_format 字段,确定我们将要枚举的是:设备的哪种功能、哪种像素格式支持的视频帧大小。
可以看到 struct v4l2_frmsizeenum 结构体中有一个union 共用体,**type=V4L2_BUF_TYPE_VIDEO_CAPTURE 情况下,discrete 生效**,这是一个 struct v4l2_frmsize_discrete 类型变量,描述了视频帧大小信息(包括视频帧的宽度和高度),也就是视频采集分辨率大小。
3.3枚举摄像头所支持的所有视频采集帧率:VIDIOC_ENUM_FRAMEINTERVALS
同一种视频帧大小,摄像头可能支持多种视频采集帧率,譬如常见的 15fps、30fps、45fps 以及 60fps 等;
使用 VIDIOC_ENUM_FRAMEINTERVALS 指令可以枚举出设备所支持的所有帧率.
ioctl(int fd, VIDIOC_ENUM_FRAMEINTERVALS, struct v4l2_frmivalenum *frmival);
/*需传入一个 struct v4l2_frmivalenum *指针,ioctl()将获取的数据写入frmival 指针所指向的对象中;
struct v4l2_frmivalenum 结构体描述了视频帧率相关的信息:
struct v4l2_frmivalenum
{
__u32 index; /* Frame format index */
__u32 pixel_format;/* Pixel format */
__u32 width; /* Frame width */
__u32 height; /* Frame height */
__u32 type; /* type */
union
{ /* Frame interval */
struct v4l2_fract discrete;
struct v4l2_frmival_stepwise stepwise;
};
__u32 reserved[2]; /* Reserved space for future use */
};
struct v4l2_fract
{
__u32 numerator; //分子
__u32 denominator; //分母
};
index、type 字段与 struct v4l2_frmsizeenum 结构体的 index、type 字段意义相同。
width、height 字段用于指定视频帧大小,pixel_format 字段指定像素格式。
以上这些字段都是需要在调用 ioctl()之前设置它的值。
可以看到 struct v4l2_frmivalenum 结构体也有一个 union 共用体,当 type=
V4L2_BUF_TYPE_VIDEO_CAPTURE 时,discrete 生效,这是一个 struct v4l2_fract 类型变量,描述了视频
帧率信息(一秒钟采集图像的次数);
struct v4l2_fract 结构体中,numerator 表示分子、denominator 表示分母,使用 numerator / denominator 来表示图像采集的周期(采集一幅图像需要多少秒),所以视频帧率便等
于 denominator / numerator。
3.4查看或设置当前的格式:VIDIOC_G_FMT、VIDIOC_S_FMT
前面介绍的指令是枚举设备支持的像素格式、视频帧大小以及视频采集帧率等这些信息’’
下面将介绍如何设置这些参数:
首先可以使用 VIDIOC_G_FMT 指令查看设备当期的格式,用法如下所示
int ioctl(int fd, VIDIOC_G_FMT, struct v4l2_format *fmt);
/* 调用 ioctl()需要传入一个 struct v4l2_format *指针,
ioctl()将获取到的数据写入 fmt 指针所指向的对象中,struct v4l2_format 结构体描述了格式相关信息。
使用 VIDIOC_S_FMT 指令设置设备的格式,用法如下所示:
int ioctl(int fd, VIDIOC_S_FMT, struct v4l2_format *fmt);
/* ioctl()会使用 fmt 所指对象的数据去设置设备的格式。
struct v4l2_format 结构体:
struct v4l2_format
{
__u32 type;
union
{
struct v4l2_pix_format pix; /* V4L2_BUF_TYPE_VIDEO_CAPTURE */
struct v4l2_pix_format_mplane pix_mp; /* V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE */
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 */
struct v4l2_sdr_format sdr; /* V4L2_BUF_TYPE_SDR_CAPTURE */
struct v4l2_meta_format meta; /* V4L2_BUF_TYPE_META_CAPTURE */
__u8 raw_data[200]; /* user-defined */
} fmt;
};
struct v4l2_pix_format 结构体
struct v4l2_pix_format
{
__u32 width; //视频帧的宽度(单位:像素)
__u32 height; //视频帧的高度(单位:像素)
__u32 pixelformat; //像素格式
__u32 field; /* enum v4l2_field */
__u32 bytesperline; /* for padding, zero if unused */
__u32 sizeimage;
__u32 colorspace; /* enum v4l2_colorspace */
__u32 priv; /* private data, depends on pixelformat */
__u32 flags; /* format flags (V4L2_PIX_FMT_FLAG_*) */
union {
/* enum v4l2_ycbcr_encoding */
__u32 ycbcr_enc;
/* enum v4l2_hsv_encoding */
__u32 hsv_enc;
};
__u32 quantization; /* enum v4l2_quantization */
__u32 xfer_func; /* enum v4l2_xfer_func */
};
3.5设置或获取当前的流类型相关参数:VIDIOC_G_PARM、VIDIOC_S_PARM
使用 VIDIOC_G_PARM 指令可以获取设备的流类型相关参数(Stream type-dependent parameters):
ioctl(int fd, VIDIOC_G_PARM, struct v4l2_streamparm *streamparm);
/*调用 ioctl()会将获取到的数据写入到 传入的 struct v4l2_streamparm *指针所指向的对象中;
使用 VIDIOC_S_PARM 指令设置设备的流类型相关参数,用法如下所示:
ioctl(int fd, VIDIOC_S_PARM, struct v4l2_streamparm *streamparm);
/*ioctl()会使用 streamparm 所指对象的数据去设置设备的流类型相关参数;
struct v4l2_streamparm 结构体描述了流类型相关的信息:
struct v4l2_streamparm
{
__u32 type; /* enum v4l2_buf_type */
union
{
struct v4l2_captureparm capture;
struct v4l2_outputparm output;
__u8 raw_data[200]; /* user-defined */
} parm;
};
struct v4l2_captureparm
{
__u32 capability; /* Supported modes */
__u32 capturemode; /* Current mode */
struct v4l2_fract timeperframe; /* Time per frame in seconds */
__u32 extendedmode; /* Driver-specific extensions */
__u32 readbuffers; /* # of buffers for read */
__u32 reserved[4];
};
struct v4l2_fract
{
__u32 numerator; /* 分子 */
__u32 denominator; /* 分母 */
};
type 字段与前面一样,不再介绍,在调用 ioctl()之前需先设置它的值。
当 type= V4L2_BUF_TYPE_VIDEO_CAPTURE 时,union 共用体中 capture 变量生效,它是一个 struct
v4l2_captureparm 类型变量,struct v4l2_captureparm 结构体描述了摄像头采集相关的一些参数,譬如视频采
集帧率,上面已经给出了该结构体的定义。
struct v4l2_captureparm 结构体中,capability 字段表示设备支持的模式有哪些,可取值如下(以下任意
一个或多个的位或关系):
/* Flags for 'capability' and 'capturemode' fields */
/* High quality imaging mode 高品质成像模式 */
#define V4L2_MODE_HIGHQUALITY 0x0001
/* timeperframe field is supported 支持设置timeperframe 字段 */
#define V4L2_CAP_TIMEPERFRAME 0x1000
capturemode 则表示当前的模式,与 capability 字段的取值相同。
timeperframe 字段是一个 struct v4l2_fract 结构体类型变量,描述了设备视频采集的周期,前面已经给大
家介绍过。使用 VIDIOC_S_PARM 可以设置视频采集的周期,也就是视频采集帧率,但是很多设备并不支
持应用层设置 timeperframe 字段,只有当 capability 字段包含 V4L2_CAP_TIMEPERFRAME 时才表示设备
支持 timeperframe 字段,这样应用层才可以去设置设备的视频采集帧率。
所以,在设置之前,先通过 VIDIOC_G_PARM 命令获取到设备的流类型相关参数,判断 capability 字
段是否包含 V4L2_CAP_TIMEPERFRAME,如下所示:
struct v4l2_streamparm streamparm;
streamparm.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
ioctl(v4l2_fd, VIDIOC_G_PARM, &streamparm);
/** 判断是否支持帧率设置 **/
if (V4L2_CAP_TIMEPERFRAME & streamparm.parm.capture.capability)
{
streamparm.parm.capture.timeperframe.numerator = 1;
streamparm.parm.capture.timeperframe.denominator = 30;//30fps
if (0 > ioctl(v4l2_fd, VIDIOC_S_PARM, &streamparm))
{//设置参数
fprintf(stderr, "ioctl error: VIDIOC_S_PARM: %s\n", strerror(errno));
return -1;
}
}
else
fprintf(stderr, "不支持帧率设置");
4. 申请帧缓冲、内存映射
-
读取摄像头数据的方式有两种:
-
一种是 read 方式,也就是直接通过 read()系统调用读取摄像头采集到的数据;
-
另一种则是 streaming 方式;
-
-
先前介绍了使用 VIDIOC_QUERYCAP 指令查询设备的属性、得到一个 struct v4l2_capability 类型数据,其中 capabilities 字段记录了设备拥有的能力:
-
当该字段包含V4L2_CAP_READWRITE 时,表示设备支持 read I/O 方式读取数据;
-
当该字段包含 V4L2_CAP_STREAMING时,表示设备支持 streaming I/O 方式;
-
事实上,绝大部分设备都支持 streaming I/O 方式读取数据,使用streaming I/O 方式,我们需要向设备申请帧缓冲,并将帧缓冲映射到应用程序进程地址空间中。
4.1使用 VIDIOC_REQBUFS 指令可申请帧缓冲
当完成对设备的配置之后,接下来就可以去申请帧缓冲了,帧缓冲顾名思义就是用于存储一帧图像数据的缓冲区。
使用 VIDIOC_REQBUFS 指令可申请帧缓冲,使用方式如下所示:
ioctl(int fd, VIDIOC_REQBUFS, struct v4l2_requestbuffers *reqbuf);
/*
调用 ioctl()需要传入一个 struct v4l2_requestbuffers *指针,
struct v4l2_requestbuffers 结构体描述了申请帧缓冲的信息,
ioctl()会根据 reqbuf 所指对象填充的信息进行申请。
*/
struct v4l2_requestbuffers 结构体
/*
* M E M O R Y - M A P P I N G B U F F E R S
*/
struct v4l2_requestbuffers
{
__u32 count; //申请帧缓冲的数量
__u32 type; /* enum v4l2_buf_type */
__u32 memory; /* enum v4l2_memory */
__u32 reserved[2];
};
/*
解析:
type 字段与前面所提及到的 type 字段意义相同,不再介绍,在调用 ioctl()之前需先设置它的值。
count 字段用于指定申请帧缓冲的数量。
memory 字段可取值如下:
enum v4l2_memory
{
V4L2_MEMORY_MMAP = 1,
V4L2_MEMORY_USERPTR = 2,
V4L2_MEMORY_OVERLAY = 3,
V4L2_MEMORY_DMABUF = 4,
};
通常将 memory 设置为 V4L2_MEMORY_MMAP 即可!
*/
使用实例:
truct v4l2_requestbuffers reqbuf;
reqbuf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
reqbuf.count = 3; // 申请 3 个帧缓冲
reqbuf.memory = V4L2_MEMORY_MMAP;
if (0 > ioctl(fd, VIDIOC_REQBUFS, &reqbuf))
{
fprintf(stderr, "ioctl error: VIDIOC_REQBUFS: %s\n", strerror(errno));
return -1;
}
4.2 streaming I/O 方式(帧缓冲队列)
streaming I/O 方式会在内核空间中维护一个帧缓冲队列,驱动程序会将从摄像头读取的一帧数据写入到
队列中的一个帧缓冲,接着将下一帧数据写入到队列中的下一个帧缓冲;
当应用程序需要读取一帧数据时,需要从队列中取出一个装满一帧数据的帧缓冲,这个取出过程就叫做出队;
当应用程序处理完这一帧数据后,需要再把这个帧缓冲加入到内核的帧缓冲队列中,这个过程叫做入队!
由此可知,读取图像数据的过程其实就是一个不断地出队列和入队列的过程,
4.3将帧缓冲映射到进程地址空间
使用 VIDIOC_REQBUFS 指令申请帧缓冲,该缓冲区实质上是由内核所维护的;
应用程序不能直接读取该缓冲区数据,需将其映射到用户空间中,这样应用程序读取映射区的数据实际上就是读取内核维护的帧缓冲中的数据。
在映射之前,需要使用VIDIOC_QUERYBUF指令查询帧缓冲的信息,譬如帧缓冲的长度、偏移量等信息;
ioctl(int fd, VIDIOC_QUERYBUF, struct v4l2_buffer *buf);
/*调用 ioctl()需要传入一个 struct v4l2_buffer *指针,
struct v4l2_buffer 结构体描述了帧缓冲的信息,ioctl()会将获取到的数据写入到 buf 指针所指的对象中
struct v4l2_buffer 结构体描述了帧缓冲的信息:
struct v4l2_buffer
{
__u32 index; //buffer 的编号
__u32 type; //type
__u32 bytesused;
__u32 flags;
__u32 field;
struct timeval timestamp;
struct v4l2_timecode timecode;
__u32 sequence;
/* memory location */
__u32 memory;
union {
__u32 offset; //偏移量
unsigned long userptr;
struct v4l2_plane *planes;
__s32 fd;
} m;
__u32 length; //buffer 的长度
__u32 reserved2;
__u32 reserved;
};
详解:
index 字段表示一个编号,申请的多个帧缓冲、每一个帧缓冲都有一个编号,从 0 开始。
一次 ioctl()调用只能获取指定编号对应的帧缓冲的信息,所以要获取多个帧缓冲的信息,需要重复调用多次,每调用一次ioctl()、index 加 1,指向下一个帧缓冲。
type 字段与前面所提及到的 type 字段意义相同,不再介绍,在调用 ioctl()之前需先设置它的值。
memory 字段与 struct v4l2_requestbuffers 结构体的 memory 字段意义相同,需要在调用 ioctl()之前设置
它的值。
length 字段表示帧缓冲的长度,而共同体中的 offset 则表示帧缓冲的偏移量,如何理解这个偏移量?
因为应用程序通过 VIDIOC_REQBUFS 指令申请帧缓冲时,内核会向操作系统申请一块内存空间作为帧缓冲区,这块内存空间的大小就等于申请的帧缓冲数量 * 每一个帧缓冲的大小,每一个帧缓冲对应到这一块内存空间的某一段,所以它们都有一个地址偏移量。
帧缓冲的数量不要太多了,尤其是在一些内存比较吃紧的嵌入式系统中,帧缓冲的数量太多,势必会占用太多的系统内存。
使用示例,申请帧缓冲后、调用 mmap()将帧缓冲映射到用户地址空间:
struct v4l2_requestbuffers reqbuf;
struct v4l2_buffer buf;
void *frm_base[3];
reqbuf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
reqbuf.count = 3; // 申请 3 个帧缓冲
reqbuf.memory = V4L2_MEMORY_MMAP;
/* 申请 3 个帧缓冲 */
if (0 > ioctl(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(fd, VIDIOC_QUERYBUF, &buf);
frm_base[buf.index] = mmap(NULL, buf.length,
PROT_READ | PROT_WRITE, MAP_SHARED,
fd, buf.m.offset);
if (MAP_FAILED == frm_base[buf.index])
{
perror("mmap error");
return -1;
}
}
上述的示例中,我们会将三个帧缓冲映射到用户空间,并将每一个帧缓冲对应的映射区的起始地址保存在 frm_base 数组中,后面读取摄像头采集的数据时,直接读取映射区即可。
5. 入队
使用 VIDIOC_QBUF 指令将帧缓冲放入到内核的帧缓冲队列中,使用方式如下:
ioctl(int fd, VIDIOC_QBUF, struct v4l2_buffer *buf);
调用 ioctl()之前,需要设置 struct v4l2_buffer 类型对象的 memory、type 字段,使用示例如下所示:
/*将三个帧缓冲放入内核的帧缓冲队列(入队操作)中:*/
struct v4l2_buffer buf;
/* 入队操作 */
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
for (buf.index = 0; buf.index < 3; buf.index++)
{
if (0 > ioctl(fd, VIDIOC_QBUF, &buf))
{
perror("ioctl error");
return -1;
}
}
6. 开启视频采集
将三个帧缓冲放入到队列中之后,接着便可以打开摄像头、开启图像采集了;
使用 VIDIOC_DQBUF 指令开启视频采集,使用方式如下所示:
ioctl(int fd, VIDIOC_STREAMON, int *type); //开启视频采集
ioctl(int fd, VIDIOC_STREAMOFF, int *type); //停止视频采集
type 其实一个 enum v4l2_buf_type *指针,通常用法如下:
enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
if (0 > ioctl(fd, VIDIOC_STREAMON, &type))
{
perror("ioctl error");
return -1;
}
7.读取数据、对数据进行处理
开启视频采集之后,接着便可以去读取数据了;
前面说过,直接读取每一个帧缓冲的在用户空间的映射区即可读取到摄像头采集的每一帧图像数据。
在读取数据之前,需要将帧缓冲从内核的帧缓冲队列中取出,这个操作叫做帧缓冲出队(有入队自然就有出队);
使用 VIDIOC_DQBUF 指令执行出队操作,使用方式如下:
ioctl(int fd, VIDIOC_DQBUF, struct v4l2_buffer *buf);
帧缓冲出队之后,接下来便可读取数据,然后对数据进行处理;
譬如将摄像头采集的图像显示到 LCD屏上;
数据处理完成之后,再将帧缓冲入队,将队列中的下一个帧缓冲出队,然后读取数据、处理,这样往复操作。
使用示例如下:
struct v4l2_buffer buf;
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
for ( ; ; )
{
for(buf.index = 0; buf.index < 3; buf.index++)
{
ioctl(fd, VIDIOC_DQBUF, &buf); //出队
// 读取帧缓冲的映射区、获取一帧数据
// 处理这一帧数据
do_something();
// 数据处理完之后、将当前帧缓冲入队、接着读取下一帧数据
ioctl(fd, VIDIOC_QBUF, &buf);
}
}
8. 结束视频采集
如果要结束视频采集,使用 VIDIOC_STREAMOFF 指令,用法前面已经介绍了。使用示例如下所示:
enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
if (0 > ioctl(fd, VIDIOC_STREAMOFF, &type))
{
perror("ioctl error");
return -1;
}
V4L2 摄像头应用编程实战
流程:
打开设备、查询设备、设置格式、申请帧缓冲、内存映射、入队、开启视频采集、出队、对采集到的数据进行处理
每一个步骤基本都是搭配不同请求指令,通过 ioctl()来实现;
/*示例代码 25.3.1 V4L2 摄像头应用编程示例代码*/
/***************************************************************
Copyright © ALIENTEK Co., Ltd. 1998-2021. All rights reserved.
文件名 : v4l2_camera.c
作者 : 邓涛
版本 : V1.0
描述 : V4L2 摄像头应用编程实战
其他 : 无
论坛 : www.openedv.com
日志 : 初版 V1.0 2021/7/09 邓涛创建
***************************************************************/
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <string.h>
#include <errno.h>
#include <sys/mman.h>
#include <linux/videodev2.h>
#include <linux/fb.h>
#define FB_DEV "/dev/fb0" //LCD 设备节点
#define FRAMEBUFFER_COUNT 3 //帧缓冲数量
/*** 摄像头像素格式及其描述信息 ***/
typedef struct camera_format {
unsigned char description[32]; //字符串描述信息
unsigned int pixelformat; //像素格式
} cam_fmt;
/*** 描述一个帧缓冲的信息 ***/
typedef struct cam_buf_info {
unsigned short *start; //帧缓冲起始地址
unsigned long length; //帧缓冲长度
} cam_buf_info;
static int width; //LCD 宽度
static int height; //LCD 高度
static unsigned short *screen_base = NULL;//LCD 显存基地址
static int fb_fd = -1; //LCD 设备文件描述符
static int v4l2_fd = -1; //摄像头设备文件描述符
static cam_buf_info buf_infos[FRAMEBUFFER_COUNT];
static cam_fmt cam_fmts[10];
static int frm_width, frm_height; //视频帧宽度和高度
static int fb_dev_init(void)
{
struct fb_var_screeninfo fb_var = {0};
struct fb_fix_screeninfo fb_fix = {0};
unsigned long screen_size;
/* 打开 framebuffer 设备 */
fb_fd = open(FB_DEV, O_RDWR);
if (0 > fb_fd) {
fprintf(stderr, "open error: %s: %s\n", FB_DEV, strerror(errno));
return -1;
}
/* 获取 framebuffer 设备信息 */
ioctl(fb_fd, FBIOGET_VSCREENINFO, &fb_var);
ioctl(fb_fd, FBIOGET_FSCREENINFO, &fb_fix);
screen_size = fb_fix.line_length * fb_var.yres;
width = fb_var.xres;
height = fb_var.yres;
/* 内存映射 */
screen_base = mmap(NULL, screen_size, PROT_READ | PROT_WRITE, MAP_SHARED, fb_fd, 0);
if (MAP_FAILED == (void *)screen_base) {
perror("mmap error");
close(fb_fd);
return -1;
}
/* LCD 背景刷白 */
memset(screen_base, 0xFF, screen_size);
return 0;
}
static int v4l2_dev_init(const char *device)
{
struct v4l2_capability cap = {0};
/* 打开摄像头 */
v4l2_fd = open(device, O_RDWR);
if (0 > v4l2_fd) {
fprintf(stderr, "open error: %s: %s\n", device, strerror(errno));
return -1;
}
/* 查询设备功能 */
ioctl(v4l2_fd, VIDIOC_QUERYCAP, &cap);
/* 判断是否是视频采集设备 */
if (!(V4L2_CAP_VIDEO_CAPTURE & cap.capabilities)) {
fprintf(stderr, "Error: %s: No capture video device!\n", device);
close(v4l2_fd);
return -1;
}
return 0;
}
static void v4l2_enum_formats(void)
{
struct v4l2_fmtdesc fmtdesc = {0};
/* 枚举摄像头所支持的所有像素格式以及描述信息 */
fmtdesc.index = 0;
fmtdesc.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
while (0 == ioctl(v4l2_fd, VIDIOC_ENUM_FMT, &fmtdesc)) {
// 将枚举出来的格式以及描述信息存放在数组中
cam_fmts[fmtdesc.index].pixelformat = fmtdesc.pixelformat;
strcpy(cam_fmts[fmtdesc.index].description, fmtdesc.description);
fmtdesc.index++;
}
}
static void v4l2_print_formats(void)
{
struct v4l2_frmsizeenum frmsize = {0};
struct v4l2_frmivalenum frmival = {0};
int i;
frmsize.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
frmival.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
for (i = 0; cam_fmts[i].pixelformat; i++) {
printf("format<0x%x>, description<%s>\n", cam_fmts[i].pixelformat,
cam_fmts[i].description);
/* 枚举出摄像头所支持的所有视频采集分辨率 */
frmsize.index = 0;
frmsize.pixel_format = cam_fmts[i].pixelformat;
frmival.pixel_format = cam_fmts[i].pixelformat;
while (0 == ioctl(v4l2_fd, VIDIOC_ENUM_FRAMESIZES, &frmsize)) {
printf("size<%d*%d> ",
frmsize.discrete.width,
frmsize.discrete.height);
frmsize.index++;
/* 获取摄像头视频采集帧率 */
frmival.index = 0;
frmival.width = frmsize.discrete.width;
frmival.height = frmsize.discrete.height;
while (0 == ioctl(v4l2_fd, VIDIOC_ENUM_FRAMEINTERVALS, &frmival)) {
printf("<%dfps>", frmival.discrete.denominator /
frmival.discrete.numerator);
frmival.index++;
}
printf("\n");
}
printf("\n");
}
}
static int v4l2_set_format(void)
{
struct v4l2_format fmt = {0};
struct v4l2_streamparm streamparm = {0};
/* 设置帧格式 */
fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;//type 类型
fmt.fmt.pix.width = width; //视频帧宽度
fmt.fmt.pix.height = height;//视频帧高度
fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_RGB565; //像素格式
if (0 > ioctl(v4l2_fd, VIDIOC_S_FMT, &fmt)) {
fprintf(stderr, "ioctl error: VIDIOC_S_FMT: %s\n", strerror(errno));
return -1;
}
/*** 判断是否已经设置为我们要求的 RGB565 像素格式
如果没有设置成功表示该设备不支持 RGB565 像素格式 */
if (V4L2_PIX_FMT_RGB565 != fmt.fmt.pix.pixelformat) {
fprintf(stderr, "Error: the device does not support RGB565 format!\n");
return -1;
}
frm_width = fmt.fmt.pix.width; //获取实际的帧宽度
frm_height = fmt.fmt.pix.height;//获取实际的帧高度
printf("视频帧大小<%d * %d>\n", frm_width, frm_height);
/* 获取 streamparm */
streamparm.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
ioctl(v4l2_fd, VIDIOC_G_PARM, &streamparm);
/** 判断是否支持帧率设置 **/
if (V4L2_CAP_TIMEPERFRAME & streamparm.parm.capture.capability) {
streamparm.parm.capture.timeperframe.numerator = 1;
streamparm.parm.capture.timeperframe.denominator = 30;//30fps
if (0 > ioctl(v4l2_fd, VIDIOC_S_PARM, &streamparm)) {
fprintf(stderr, "ioctl error: VIDIOC_S_PARM: %s\n", strerror(errno));
return -1;
}
}
return 0;
}
static int v4l2_init_buffer(void)
{
struct v4l2_requestbuffers reqbuf = {0};
struct v4l2_buffer buf = {0};
/* 申请帧缓冲 */
reqbuf.count = FRAMEBUFFER_COUNT; //帧缓冲的数量
reqbuf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
reqbuf.memory = V4L2_MEMORY_MMAP;
if (0 > ioctl(v4l2_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 < FRAMEBUFFER_COUNT; buf.index++) {
ioctl(v4l2_fd, VIDIOC_QUERYBUF, &buf);
buf_infos[buf.index].length = buf.length;
buf_infos[buf.index].start = mmap(NULL, buf.length,
PROT_READ | PROT_WRITE, MAP_SHARED,
v4l2_fd, buf.m.offset);
if (MAP_FAILED == buf_infos[buf.index].start) {
perror("mmap error");
return -1;
}
}
/* 入队 */
for (buf.index = 0; buf.index < FRAMEBUFFER_COUNT; buf.index++) {
if (0 > ioctl(v4l2_fd, VIDIOC_QBUF, &buf)) {
fprintf(stderr, "ioctl error: VIDIOC_QBUF: %s\n", strerror(errno));
return -1;
}
}
return 0;
}
static int v4l2_stream_on(void)
{
/* 打开摄像头、摄像头开始采集数据 */
enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
if (0 > ioctl(v4l2_fd, VIDIOC_STREAMON, &type)) {
fprintf(stderr, "ioctl error: VIDIOC_STREAMON: %s\n", strerror(errno));
return -1;
}
return 0;
}
static void v4l2_read_data(void)
{
struct v4l2_buffer buf = {0};
unsigned short *base;
unsigned short *start;
int min_w, min_h;
int j;
if (width > frm_width)
min_w = frm_width;
else
min_w = width;
if (height > frm_height)
min_h = frm_height;
else
min_h = height;
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
for ( ; ; ) {
for(buf.index = 0; buf.index < FRAMEBUFFER_COUNT; buf.index++) {
ioctl(v4l2_fd, VIDIOC_DQBUF, &buf); //出队
for (j = 0, base=screen_base, start=buf_infos[buf.index].start;
j < min_h; j++) {
memcpy(base, start, min_w * 2); //RGB565 一个像素占 2 个字节
base += width; //LCD 显示指向下一行
start += frm_width;//指向下一行数据
}
// 数据处理完之后、再入队、往复
ioctl(v4l2_fd, VIDIOC_QBUF, &buf);
}
}
}
int main(int argc, char *argv[])
{
if (2 != argc)
{
fprintf(stderr, "Usage: %s <video_dev>\n", argv[0]);
exit(EXIT_FAILURE);
}
/* 初始化 LCD */
if (fb_dev_init())
exit(EXIT_FAILURE);
/* 初始化摄像头 */
if (v4l2_dev_init(argv[1]))
exit(EXIT_FAILURE);
/* 枚举所有格式并打印摄像头支持的分辨率及帧率 */
v4l2_enum_formats();
v4l2_print_formats();
/* 设置格式 */
if (v4l2_set_format())
exit(EXIT_FAILURE);
/* 初始化帧缓冲:申请、内存映射、入队 */
if (v4l2_init_buffer())
exit(EXIT_FAILURE);
/* 开启视频采集 */
if (v4l2_stream_on())
exit(EXIT_FAILURE);
/* 读取数据:出队 */
v4l2_read_data(); //在函数内循环采集数据、将其显示到 LCD 屏
exit(EXIT_SUCCESS);
}