Linux_应用篇(19) V4L2 摄像头应用编程

news2024/11/23 20:15:21

ALPHA/Mini I.MX6U 开发板配套支持多种不同的摄像头,包括正点原子的 ov5640(500W 像素)、ov2640(200W 像素)以及 ov7725(不带 FIFO、 30W 像素)这三款摄像头,在开发板出厂系统上,可以使用这些摄像头;当然,除此之外我们还可以使用 USB 摄像头,直接将 USB 摄像头插入到开发板上的 USB接口即可! 本章我们就来学习 Linux 下的摄像头应用编程。本章将会讨论如下主题内容。
⚫ V4L2 简介;
⚫ V4L2 设备应用编程介绍;
⚫ 摄像头应用编程实战;

V4L2 简介

大家可以看到我们本章的标题叫做“V4L2 摄像头应用编程”,那什么是 V4L2 呢?对 Linux 下摄像头驱动程序开发有过了解的读者,应该知道这是什么意思。V4L2 是 Video for linux two 的简称,是 Linux 内核中视频类设备的一套驱动框架,为视频类设备驱动开发和应用层提供了一套统一的接口规范, 那什么是视频类设备呢?一个非常典型的视频类设备就是视频采集设备,譬如各种摄像头;当然还包括其它类型视频类设备,这里就不再给介绍了。使用 V4L2 设备驱动框架注册的设备会在 Linux 系统/dev/目录下生成对应的设备节点文件,设备节点的名称通常为 videoX(X 标准一个数字编号, 0、 1、 2、 3……),每一个 videoX 设备文件就代表一个视频类设备。应用程序通过对 videoX 设备文件进行 I/O 操作来配置、使用设备类设备,下小节将向大家详细介绍!

V4L2 摄像头应用编程

V4L2 设备驱动框架向应用层提供了一套统一、标准的接口规范,应用程序按照该接口规范来进行应用编程,从而使用摄像头。 对于摄像头设备来说,其编程模式如下所示:
1. 首先是打开摄像头设备;
2. 查询设备的属性或功能;
3. 设置设备的参数,譬如像素格式、 帧大小、 帧率;
4. 申请帧缓冲、 内存映射;
5. 帧缓冲入队;
6. 开启视频采集;
7. 帧缓冲出队、对采集的数据进行处理;
8. 处理完后,再次将帧缓冲入队,往复;
9. 结束采集。
流程图如下所示:

从流程图中可以看到,几乎对摄像头的所有操作都是通过 ioctl()来完成,搭配不同的 V4L2 指令(request参数)请求不同的操作,这些指令定义在头文件 linux/videodev2.h 中,在摄像头应用程序代码中,需要包含头文件 linux/videodev2.h,该头文件中申明了很多与摄像头应用编程相关的数据结构以及宏定义,大家可以打开这个头文件看看。在 videodev2.h 头文件中,定义了很多 ioctl()的指令,以宏定义的形式提供(VIDIOC_XXX),如下所示:

/*
* I O C T L C O D E S F O R V I D E O D E V I C E S
*
*/
#define VIDIOC_QUERYCAP _IOR('V', 0, struct v4l2_capability)
#define VIDIOC_RESERVED _IO('V', 1)
#define VIDIOC_ENUM_FMT _IOWR('V', 2, struct v4l2_fmtdesc)
#define VIDIOC_G_FMT _IOWR('V', 4, struct v4l2_format)
#define VIDIOC_S_FMT _IOWR('V', 5, struct v4l2_format)
#define VIDIOC_REQBUFS _IOWR('V', 8, struct v4l2_requestbuffers)
#define VIDIOC_QUERYBUF _IOWR('V', 9, struct v4l2_buffer)
#define VIDIOC_G_FBUF _IOR('V', 10, struct v4l2_framebuffer)
#define VIDIOC_S_FBUF _IOW('V', 11, struct v4l2_framebuffer)
#define VIDIOC_OVERLAY _IOW('V', 14, int)
#define VIDIOC_QBUF _IOWR('V', 15, struct v4l2_buffer)
#define VIDIOC_EXPBUF _IOWR('V', 16, struct v4l2_exportbuffer)
#define VIDIOC_DQBUF _IOWR('V', 17, struct v4l2_buffer)
#define VIDIOC_STREAMON _IOW('V', 18, int)
#define VIDIOC_STREAMOFF _IOW('V', 19, int)
#define VIDIOC_G_PARM _IOWR('V', 21, struct v4l2_streamparm)
#define VIDIOC_S_PARM _IOWR('V', 22, struct v4l2_streamparm)
#define VIDIOC_G_STD _IOR('V', 23, v4l2_std_id)
#define VIDIOC_S_STD _IOW('V', 24, v4l2_std_id)
#define VIDIOC_ENUMSTD _IOWR('V', 25, struct v4l2_standard)
#define VIDIOC_ENUMINPUT _IOWR('V', 26, struct v4l2_input)
#define VIDIOC_G_CTRL _IOWR('V', 27, struct v4l2_control)
#define VIDIOC_S_CTRL _IOWR('V', 28, struct v4l2_control)
#define VIDIOC_G_TUNER _IOWR('V', 29, struct v4l2_tuner)
#define VIDIOC_S_TUNER _IOW('V', 30, struct v4l2_tuner)
#define VIDIOC_G_AUDIO _IOR('V', 33, struct v4l2_audio)
#define VIDIOC_S_AUDIO _IOW('V', 34, struct v4l2_audio)
#define VIDIOC_QUERYCTRL _IOWR('V', 36, struct v4l2_queryctrl)
#define VIDIOC_QUERYMENU _IOWR('V', 37, struct v4l2_querymenu)
#define VIDIOC_G_INPUT _IOR('V', 38, int)
#define VIDIOC_S_INPUT _IOWR('V', 39, int)
#define VIDIOC_G_EDID _IOWR('V', 40, struct v4l2_edid)
#define VIDIOC_S_EDID _IOWR('V', 41, struct v4l2_edid)
#define VIDIOC_G_OUTPUT _IOR('V', 46, int)
#define VIDIOC_S_OUTPUT _IOWR('V', 47, int)
#define VIDIOC_ENUMOUTPUT _IOWR('V', 48, struct v4l2_output)
#define VIDIOC_G_AUDOUT _IOR('V', 49, struct v4l2_audioout)
#define VIDIOC_S_AUDOUT _IOW('V', 50, struct v4l2_audioout)
#define VIDIOC_G_MODULATOR _IOWR('V', 54, struct v4l2_modulator)
#define VIDIOC_S_MODULATOR _IOW('V', 55, struct v4l2_modulator)
#define VIDIOC_G_FREQUENCY _IOWR('V', 56, struct v4l2_frequency)
#define VIDIOC_S_FREQUENCY _IOW('V', 57, struct v4l2_frequency)
#define VIDIOC_CROPCAP _IOWR('V', 58, struct v4l2_cropcap)
#define VIDIOC_G_CROP _IOWR('V', 59, struct v4l2_crop)
#define VIDIOC_S_CROP _IOW('V', 60, struct v4l2_crop)
#define VIDIOC_G_JPEGCOMP _IOR('V', 61, struct v4l2_jpegcompression)
#define VIDIOC_S_JPEGCOMP _IOW('V', 62, struct v4l2_jpegcompression)
#define VIDIOC_QUERYSTD _IOR('V', 63, v4l2_std_id)
#define VIDIOC_TRY_FMT _IOWR('V', 64, struct v4l2_format)
#define VIDIOC_ENUMAUDIO _IOWR('V', 65, struct v4l2_audio)
#define VIDIOC_ENUMAUDOUT _IOWR('V', 66, struct v4l2_audioout)
#define VIDIOC_G_PRIORITY _IOR('V', 67, __u32) /* enum v4l2_priority */
#define VIDIOC_S_PRIORITY _IOW('V', 68, __u32) /* enum v4l2_priority */
#define VIDIOC_G_SLICED_VBI_CAP _IOWR('V', 69, struct v4l2_sliced_vbi_cap)
#define VIDIOC_LOG_STATUS _IO('V', 70)
#define VIDIOC_G_EXT_CTRLS _IOWR('V', 71, struct v4l2_ext_controls)
#define VIDIOC_S_EXT_CTRLS _IOWR('V', 72, struct v4l2_ext_controls)
#define VIDIOC_TRY_EXT_CTRLS _IOWR('V', 73, struct v4l2_ext_controls)
#define VIDIOC_ENUM_FRAMESIZES _IOWR('V', 74, struct v4l2_frmsizeenum)
#define VIDIOC_ENUM_FRAMEINTERVALS _IOWR('V', 75, struct v4l2_frmivalenum)
#define VIDIOC_G_ENC_INDEX _IOR('V', 76, struct v4l2_enc_idx)
#define VIDIOC_ENCODER_CMD _IOWR('V', 77, struct v4l2_encoder_cmd)
#define VIDIOC_TRY_ENCODER_CMD _IOWR('V', 78, struct v4l2_encoder_cmd)

每 一 个 不 同 的 指 令 宏 就 表 示 向 设 备 请 求 不 同 的 操 作 , 从 上 面 可 以 看 到 , 每 一 个 宏 后 面(_IOWR/_IOR/_IOW)还携带了一个 struct 数据结构体,譬如 struct v4l2_capability、 struct v4l2_fmtdesc,这就是调用 ioctl()时需要传入的第三个参数的类型;调用 ioctl()前,定义一个该类型变量,调用 ioctl()时、将变量的指针作为 ioctl()的第三个参数传入,譬如:

struct v4l2_capability cap;
……
ioctl(fd, VIDIOC_QUERYCAP, &cap);

在实际的应用编程中,并不是所有的指令都会用到, 针对视频采集类设备, 以下笔者列出了一些常用的指令:

V4L2 指令描述
VIDIOC_QUERYCAP查询设备的属性/能力/功能
VIDIOC_ENUM_FMT枚举设备支持的像素格式
VIDIOC_G_FMT获取设备当前的帧格式信息
VIDIOC_S_FMT设置帧格式信息
VIDIOC_REQBUFS申请帧缓冲
VIDIOC_QUERYBUF查询帧缓冲
VIDIOC_QBUF帧缓冲入队操作
VIDIOC_DQBUF帧缓冲出队操作
VIDIOC_STREAMON开启视频采集
VIDIOC_STREAMOFF关闭视频采集
VIDIOC_G_PARM获取设备的一些参数
VIDIOC_S_PARM设置参数
VIDIOC_TRY_FMT尝试设置帧格式、用于判断设备是否支持该格式
VIDIOC_ENUM_FRAMESIZES枚举设备支持的视频采集分辨率
VIDIOC_ENUM_FRAMEINTERVALS枚举设备支持的视频采集帧率

打开摄像头

视频类设备对应的设备节点为/dev/videoX, X 为数字编号,通常从 0 开始;摄像头应用编程的第一步便是打开设备,调用 open 打开,得到文件描述符 fd,如下所示:

int fd = -1;
/* 打开摄像头 */
fd = open("/dev/video0", O_RDWR);
if (0 > fd) {
    fprintf(stderr, "open error: %s: %s\n", "/dev/video0", strerror(errno));
    return -1;
}

打开设备文件时,需要使用 O_RDWR 指定读权限和写权限。

查询设备的属性/能力/功能

打开设备之后,接着需要查询设备的属性,确定该设备是否是一个视频采集类设备、以及其它一些属性,怎么查询呢?自然是通过 ioctl()函数来实现, ioctl()对于设备文件来说是一个非常重要的系统调用, 凡是涉及到配置设备、获取设备配置等操作都会使用 ioctl 来完成,在前面章节内容中我们就已经见识过了;但对于普通文件来说, ioctl()几乎没什么用。查询设备的属性, 使用的指令为 VIDIOC_QUERYCAP, 如下所示:

ioctl(int fd, VIDIOC_QUERYCAP, struct v4l2_capability *cap);

此时通过 ioctl()将获取到一个 struct v4l2_capability 类型数据, 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 */
#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 */

这些宏都是在videodev2.h头文件中所定义的,大家可以自己去看。对于摄像头设备来说,它的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;
}

设置帧格式、帧率

一个摄像头通常会支持多种不同的像素格式,譬如 RGB、 YUYV 以及压缩格式 MJPEG 等, 并且还支持多种不同的视频采集分辨率,譬如 640*480、 320*240、 1280*720 等,除此之外, 同一分辨率可能还支持多种不同的视频采集帧率(15fps、 30fps) 。 所以,通常在进行视频采集之前、需要在应用程序中去设置这些参数。

a)枚举出摄像头支持的所有像素格式: VIDIOC_ENUM_FMT

要设置像素格式,首先得知道该设备支持哪些像素格式,如何得知呢? 使用 VIDIOC_ENUM_FMT 指令:

ioctl(int fd, VIDIOC_ENUM_FMT, struct v4l2_fmtdesc *fmtdesc);

使用 VIDIOC_ENUM_FMT 可以枚举出设备所支持的所有像素格式,调用 ioctl()需要传入一个 structv4l2_fmtdesc *指针, ioctl()会将获取到的数据写入到 fmtdesc 指针所指向的对象中。 struct v4l2_fmtdesc 结构体描述了像素格式相关的信息,我们来看看 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 类型数据来表示,如下所示:

/* 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 */

以上列举出来的只是其中一部分,篇幅有限、不能将所有的像素格式都列举出来,大家可以自己查看videodev2.h 头文件。可以看到后面有一个 v4l2_fourcc 宏,其实就是通过这个宏以及对应的参数合成的一个u32 类型数据。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++;
}

b)枚举摄像头所支持的所有视频采集分辨率: VIDIOC_ENUM_FRAMESIZES

使用 VIDIOC_ENUM_FRAMESIZES 指令可以枚举出设备所支持的所有视频采集分辨率,用法如下所示:

ioctl(int fd, VIDIOC_ENUM_FRAMESIZES, struct v4l2_frmsizeenum *frmsize);

调用 ioctl()需要传入一个 struct v4l2_frmsizeenum *指针, ioctl()会将获取到的数据写入到 frmsize 指针所指向的对象中。 struct v4l2_frmsizeenum 结构体描述了视频帧大小相关的信息,我们来看看 structv4l2_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 类型变量,描述了视频帧大小信息(包括视频帧的宽度和高度),也就是视频采集分辨率大小。譬如我们要枚举出摄像头 RGB565 像素格式所支持的所有视频帧大小:

struct v4l2_frmsizeenum frmsize;
frmsize.index = 0;
frmsize.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
frmsize.pixel_format = V4L2_PIX_FMT_RGB565;
while (0 == ioctl(fd, VIDIOC_ENUM_FRAMESIZES, &frmsize)) {
    printf("frame_size<%d*%d>\n", frmsize.discrete.width, frmsize.discrete.height);
    frmsize.index++;
}

c)枚举摄像头所支持的所有视频采集帧率: VIDIOC_ENUM_FRAMEINTERVALS

同一种视频帧大小,摄像头可能会支持多种不同的视频采集帧率,譬如常见的 15fps、 30fps、 45fps 以及 60fps 等;使用 VIDIOC_ENUM_FRAMEINTERVALS 指令可以枚举出设备所支持的所有帧率,使用方式如下:

ioctl(int fd, VIDIOC_ENUM_FRAMEINTERVALS, struct v4l2_frmivalenum *frmival);

调用 ioctl()需要传入一个 struct v4l2_frmivalenum *指针, ioctl()会将获取到的数据写入到 frmival 指针所指 向 的 对象 中 。 struct v4l2_frmivalenum 结构 体描 述 了 视频 帧 率相 关的 信 息 ,我 们 来看看 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。
使用示例,譬如,我们要枚举出 RGB565 像素格式下 640*480 帧大小所支持的所有视频采集帧率:

struct v4l2_frmivalenum frmival;
frmival.index = 0;
frmival.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
frmival.pixel_format = V4L2_PIX_FMT_RGB565;
frmival.width = 640;
frmival.height = 480;
while (0 == ioctl(fd, VIDIOC_ENUM_FRAMEINTERVALS, &frmival)) {
printf("Frame interval<%ffps> ", frmival.discrete.denominator / frmival.discrete.numerator);
frmival.index++;
}

d)查看或设置当前的格式: 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 所指对象的数据去设置设备的格式。 我们来看看 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;
};

type 字段依然与前面介绍的结构体中的 type 字段意义相同,不管是获取格式、还是设置格式都需要在调用 ioctl()函数之前设置它的值。
接下来是一个 union 共用体,当 type 被设置为 V4L2_BUF_TYPE_VIDEO_CAPTURE 时, pix 变量生效,它是一个 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 */
};

colorspace 字段描述的是一个颜色空间, 可取值如下:

enum v4l2_colorspace {
/*
* Default colorspace, i.e. let the driver figure it out.
* Can only be used with video capture.
*/
V4L2_COLORSPACE_DEFAULT = 0,
/* SMPTE 170M: used for broadcast NTSC/PAL SDTV */
V4L2_COLORSPACE_SMPTE170M = 1,
/* Obsolete pre-1998 SMPTE 240M HDTV standard, superseded by Rec 709 */
V4L2_COLORSPACE_SMPTE240M = 2,
/* Rec.709: used for HDTV */
V4L2_COLORSPACE_REC709 = 3,
/*
* Deprecated, do not use. No driver will ever return this. This was
* based on a misunderstanding of the bt878 datasheet.
*/
V4L2_COLORSPACE_BT878 = 4,
/*
* NTSC 1953 colorspace. This only makes sense when dealing with
* really, really old NTSC recordings. Superseded by SMPTE 170M.
*/
V4L2_COLORSPACE_470_SYSTEM_M = 5,
/*
* EBU Tech 3213 PAL/SECAM colorspace. This only makes sense when
* dealing with really old PAL/SECAM recordings. Superseded by
* SMPTE 170M.
*/
V4L2_COLORSPACE_470_SYSTEM_BG = 6,
/*
* Effectively shorthand for V4L2_COLORSPACE_SRGB, V4L2_YCBCR_ENC_601
* and V4L2_QUANTIZATION_FULL_RANGE. To be used for (Motion-)JPEG.
*/
V4L2_COLORSPACE_JPEG = 7,
/* For RGB colorspaces such as produces by most webcams. */
V4L2_COLORSPACE_SRGB = 8,
/* AdobeRGB colorspace */
V4L2_COLORSPACE_ADOBERGB = 9,
/* BT.2020 colorspace, used for UHDTV. */
V4L2_COLORSPACE_BT2020 = 10,
/* Raw colorspace: for RAW unprocessed images */
V4L2_COLORSPACE_RAW = 11,
/* DCI-P3 colorspace, used by cinema projectors */
V4L2_COLORSPACE_DCI_P3 = 12,
};

使用 VIDIOC_S_FMT 指令设置格式时, 通常不需要用户指定 colorspace,底层驱动会根据像素格式pixelformat 来确定对应的 colorspace。
例子:获取当前的格式、并设置格式

struct v4l2_format fmt;
fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
if (0 > ioctl(fd, VIDIOC_G_FMT, &fmt)) { //获取格式信息
perror("ioctl error");
return -1;
}
printf("width:%d, height:%d format:%d\n", fmt.fmt.pix.width, fmt.fmt.pix.height, fmt.fmt.pix.pixelformat);
fmt.fmt.pix.width = 800;
fmt.fmt.pix.height = 480;
fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_RGB565;
if (0 > ioctl(fd, VIDIOC_S_FMT, &fmt)) { //设置格式
perror("ioctl error");
return -1;
}

使用指令 VIDIOC_S_FMT 设置格式时,实际设置的参数并不一定等于我们指定的参数,譬如上面我们指定视频帧宽度为 800、高度为 480,但这个摄像头不一定支持这种视频帧大小,或者摄像头不支持V4L2_PIX_FMT_RGB565 这种像素格式; 通常在这种情况下, 底层驱动程序并不会按照我们指定的参数进行设置, 它会对这些参数进行修改,譬如,如果摄像头不支持 800*480, 那么底层驱动可能会将其修改为640*480(假设摄像头支持这种分辨率);所以,当 ioctl()调用返回后,我们还需要检查返回的 struct v4l2_format
类型变量,以确定我们指定的参数是否已经生效:

struct v4l2_format fmt;
fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
fmt.fmt.pix.width = 800;
fmt.fmt.pix.height = 480;
fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_RGB565;
if (0 > ioctl(fd, VIDIOC_S_FMT, &fmt)) { //设置格式
perror("ioctl error");
return -1;
}
if (800 != fmt.fmt.pix.width ||
480 != fmt.fmt.pix.height){
do_something();
}
if (V4L2_PIX_FMT_RGB565 != fmt.fmt.pix.pixelformat) {
do_something();
}

e)设置或获取当前的流类型相关参数: 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 *指针, ioctl()会将获取到的数据写入到 streamparm 指针所指向的对象中, 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 */
#define V4L2_MODE_HIGHQUALITY 0x0001 /* High quality imaging mode 高品质成像模式 */
#define V4L2_CAP_TIMEPERFRAME 0x1000 /* timeperframe field is supported 支持设置 timeperframe字段 */

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, "不支持帧率设置");

申请帧缓冲、内存映射

读取摄像头数据的方式有两种,一种是 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 方式,我们需要向设备申请帧缓冲,并将帧缓冲映射到应用程序进程地址空间中。当完成对设备的配置之后,接下来就可以去申请帧缓冲了,帧缓冲顾名思义就是用于存储一帧图像数据的缓冲区, 使用 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 即可! 使用示例如下:

struct 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;
}

streaming I/O 方式会在内核空间中维护一个帧缓冲队列, 驱动程序会将从摄像头读取的一帧数据写入到队列中的一个帧缓冲,接着将下一帧数据写入到队列中的下一个帧缓冲;当应用程序需要读取一帧数据时,需要从队列中取出一个装满一帧数据的帧缓冲,这个取出过程就叫做出队;当应用程序处理完这一帧数据后,需要再把这个帧缓冲加入到内核的帧缓冲队列中,这个过程叫做入队!这个很容易理解,现实当中都有很多这样的例子,这里就不再举例了。所以由此可知, 读取图像数据的过程其实就是一个不断地出队列和入队列的过程,如下图所示

将帧缓冲映射到进程地址空间

使用 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 数组中,后面读取摄像头采集的数据时,直接读取映射区即可。

入队

使用 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;
    }
}

开启视频采集

将三个帧缓冲放入到队列中之后,接着便可以打开摄像头、开启图像采集了,使用 VIDIOC_STREAMON指令开启视频采集,使用方式如下所示:

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;
}

读取数据、对数据进行处理

开启视频采集之后,接着便可以去读取数据了, 前面我们已经说过,直接读取每一个帧缓冲的在用户空间的映射区即可读取到摄像头采集的每一帧图像数据。在读取数据之前,需要将帧缓冲从内核的帧缓冲队列中取出,这个操作叫做帧缓冲出队(有入队自然就有出队),前面已经给大家详细地介绍了这些理论知识。使用 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);
    }
}

结束视频采集

如果要结束视频采集,使用 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 摄像头应用编程实践

#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);
}

上述示例代码中, 会将摄像头采集到的图像数据显示到开发板 LCD 屏上, 我们将摄像头的像素格式设置为 RGB565,开发板出厂系统支持正点原子的 ov5640、 ov7725(无 FIFO)以及 ov2640 这几款摄像头,这几款摄像头都支持 RGB565 像素格式; 当然除此之外,还可以板子上使用 UVC USB 摄像头,如果大家身边有这种摄像头,也可以进行测试,但是这种 USB 摄像头通常不支持 RGB565 格式,而更多是 YUYV 格式,上述代码并不支持 YUYV 格式的处理, 需要大家进行修改,你得将采集到的 YUYV 数据转为RGB565 数据,才能在LCD 上显示采集到的图像。
接着运行测试程序,我们需要传入一个参数,该参数表示摄像头的对应的设备节点:

程序运行之后,此时开发板 LCD 屏上将会显示摄像头所采集到的图像。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1831158.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Jupyter Notebook简介

目录 1.概述 2.诞生背景 3.历史版本 4.安装 5.卸载 6.如何使用 7.菜单和菜单项 8.示例 9.未来展望 10.总结 1.概述 Jupyter Notebook是一种基于Web的交互式计算环境&#xff0c;主要用于数据分析、数据科学、机器学习以及探索性编程等领域。允许用户在单个文档中编写…

3.华为trunk和access接口配置

目的&#xff1a;PC1 连通三层交换机LSW1 LSW1配置 [Huawei]vlan batch 10 [Huawei]interface Vlanif 10 [Huawei-Vlanif10]ip address 10.10.10.10 24 [Huawei]int g0/0/1 [Huawei-GigabitEthernet0/0/1]port link-type trunk [Huawei-GigabitEthernet0/0/1]port trunk allow…

嵌入式操作系统_2.嵌入式操作系统的一般架构

1.嵌入式操作系统的概念 嵌入式操作系统通常由硬件驱动程序、调式代理、操作系统内核、文件系统和可配置组件等功能组成&#xff0c;并为应用软件提供标准的API&#xff08;Application Programming Interface&#xff09;接口服务。 2.一般嵌入式操作系统的体系结构 从嵌入…

C#|Maui|BootstrapBlazor|Bootstrap Blazor 组件库改模板 | Bootstrap Blazor 组件库改布局,该怎么改?

先copy一个项目下来&#xff1a;Bootstrap Blazor 组件库 一套基于 Bootstrap 和 Blazor 的企业级组件库 发现不是很满足我的需求&#xff0c;我要把右下角的admin移动到左边去&#xff0c;该怎么移动&#xff1f; 先改代码 点进去到Layout.razor 文档&#xff0c;改成如下&am…

“专业敏捷教练课程” 8月31-9月1日 · CSP-SM认证周末班【晋升高阶享多重福利】

点击标题阅读&#xff1a; 《数字时代下敏捷教练专业技能CSP-SM框架解析》 为什么“模块化分时段”单元教学&#xff1f; ☆ 有脑科学研究资料揭示: 成人学习者持续3.5小时已经达到极限&#xff0c;新模式教学&#xff0c;给学习者留有一些时间和空间去消化吸收&#xff0c;…

清华停招土木,新增地球科学引热议

早在今年2月26日&#xff0c;多个自媒体平台上有人发布消息称“清华大学停止土木工程等专业招生”&#xff0c;引发广泛关注。 在清华大学的官网可以看到下图的公告。 可以看到&#xff0c;清华大学停招土木工程等专业&#xff0c;新增地球系统科学等专业。这一举措引起全网热…

收银系统源码-连锁店收银系统,支持二次开发

千呼新零售2.0系统是零售行业连锁店一体化收银系统&#xff0c;包括线下收银线上商城连锁店管理ERP管理商品管理供应商管理会员营销等功能为一体&#xff0c;线上线下数据全部打通。 适用于商超、便利店、水果、生鲜、母婴、服装、零食、百货等连锁店使用。 一、收银端 支持Wi…

vim 的 map+noremap

经常在 vim 的配置文件中&#xff0c;看到对于改键的设置。 他们的区别主要有两种 1 用于哪种模式。 2 是否用于递归。

一文快速认识环形光源——CCS光源

机器视觉系统中&#xff0c;光源起着重要作用&#xff0c;不同类型的光源应用也不同&#xff0c;选择合适的光源成像效果非常明显。今天我们一起来看看CCS光源——工业用环形光源LDR2系列。 LDR2系列是标准的环形光源&#xff0c;通过采用柔性基板&#xff0c;可创造任意角度。…

每日一题——Python实现PAT甲级1132 Cut Integer(举一反三+思想解读+逐步优化)五千字好文

一个认为一切根源都是“自己不够强”的INTJ 个人主页&#xff1a;用哲学编程-CSDN博客专栏&#xff1a;每日一题——举一反三Python编程学习Python内置函数 Python-3.12.0文档解读 目录 我的写法 正确性和功能性 时间复杂度 空间复杂度 其他点评 总结 我要更强 优化后…

【每日LeetCode】递归、记忆化搜索

递归、记忆化搜索 【leetcode70 爬楼梯】 class Solution {public int climbStairs(int n) {int[] memo new int[n 1];return dfs(n, memo);}private int dfs(int i, int[] memo){if(i < 1){return 1;}if(memo[i] ! 0){return memo[i];}return memo[i] dfs(i-1,memo) d…

成功秘诀曝光:老阳分享选品师赚钱攻略

当谈论老阳分享的外海拼多多选品师项目时&#xff0c;人们往往对其真实性和可行性存有疑问。这个项目被宣传为一个能够稳定创收的机会&#xff0c;但在决定是否投身其中之前&#xff0c;了解其具体运作和实际效果至关重要。 老阳分享作为电商培训平台&#xff0c;旨在培养和支持…

充电学习——0、电源管理

一、设备电源管理&#xff1a; 两种类型 1、系统睡眠模型&#xff1a; 设备驱动作为系统一部分&#xff0c;会跟随系统进入低功耗状态&#xff0c;suspend &#xff08;suspend-to-ram&#xff09; 一些驱动程序可以管理硬件的唤醒事件&#xff0c; 这一特性通过/sys/device/…

GaussDB技术解读——GaussDB架构介绍(四)

目录 11 GaussDB云原生架构 11.1 云原生关键技术架构 11.2 关键技术方案 11.2.1 通信组件 11.2.2 集群管理组件 11.2.3 多租组件 GaussDB架构介绍&#xff08;三&#xff09;从智能关键技术方案、驱动接口关键技术方案等方面对GaussDB架构进行了解读&#xff0c;本篇将…

【CTF Web】CTFShow 探针泄露 Writeup(PHP+探针泄露+信息收集)

探针泄露 10 对于测试用的探针&#xff0c;使用完毕后要及时删除&#xff0c;可能会造成信息泄露 解法 查看网页源代码。 view-source:https://11170dfe-84c7-4fde-b1ca-5d1ec3dd7570.challenge.ctf.show/没有找到有用的信息。 用 dirsearch 扫描。 dirsearch -u https://1…

FPGA学习最好的2个网站?

自学FPGA最好的两个网站: Xilinx官方网站: ​网址链接&#xff1a; https://www.amd.com/zh-cn.html Xilinx Wiki - Confluence (http://atlassian.net) Xilinx GitHub&#xff08;https://github.com/Xilinx&#xff09; 电子创新网赛灵思社区 | 电子创新网 (http://eet…

原生APP开发的技术优势

原生应用程序&#xff08;Native App&#xff09;是直接使用特定操作系统的编程语言和开发工具为特定平台&#xff08;如iOS、Android等&#xff09;开发的应用程序。原生APP开发具有以下优势。北京木奇移动技术有限公司&#xff0c;专业的软件外包开发公司&#xff0c;欢迎交流…

深度学习推理显卡设置

深度学习推理显卡设置 进入NVIDIA控制面板&#xff0c;选择 “管理3D设置”设置 "低延时模式"为 "“超高”"设置 “电源管理模式” 为 “最高性能优先” 使用锁频来获得稳定的推理 法一&#xff1a;命令行操作 以管理员身份打开CMD查看GPU核心可用频率&…

服务器数据恢复—KVM虚拟机被误删除如何恢复虚拟磁盘文件?

服务器数据恢复环境&故障&#xff1a; 1台服务器&#xff0c;Linux操作系统EXT4文件系统&#xff0c;部署了数台KVM虚拟机&#xff0c;每台虚拟机包含一个qcow2格式的磁盘文件&#xff0c;和一个raw格式的磁盘文件。 工作人员操作失误删除了3台服务器上的KVM虚拟机&#xf…

【递归、搜索与回溯】综合练习三

综合练习三 1.优美的排列3.N 皇后3.有效的数独4.解数独 点赞&#x1f44d;&#x1f44d;收藏&#x1f31f;&#x1f31f;关注&#x1f496;&#x1f496; 你的支持是对我最大的鼓励&#xff0c;我们一起努力吧!&#x1f603;&#x1f603; 1.优美的排列 题目链接&#xff1a;5…