ALPHA/Mini I.MX6U 开发板配套支持多种不同的摄像头,包括正点原子的ov5640(500W 像素)、
ov2640(200W 像素)以及ov7725(不带FIFO、30W 像素)这三款摄像头,在开发板出厂系统上,可以使用这些摄像头;当然,除此之外我们还可以使用USB 摄像头,直接将USB 摄像头插入到开发板上的USB
接口即可!本章我们就来学习Linux 下的摄像头应用编程。
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 设备驱动框架向应用层提供了一套统一、标准的接口规范,应用程序按照该接口规范来进行应用编程,从而使用摄像头。对于摄像头设备来说,其编程模式如下所示:
- 首先是打开摄像头设备;
- 查询设备的属性或功能;
- 设置设备的参数,譬如像素格式、帧大小、帧率;
- 申请帧缓冲、内存映射;
- 帧缓冲入队;
- 开启视频采集;
- 帧缓冲出队、对采集的数据进行处理;
- 处理完后,再次将帧缓冲入队,往复;
- 结束采集。
流程图如下所示:
从流程图中可以看到,几乎对摄像头的所有操作都是通过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);
在实际的应用编程中,并不是所有的指令都会用到,针对视频采集类设备,以下笔者列出了一些常用的指令:
打开摄像头
视频类设备对应的设备节点为/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 等,并且还支持多种不同的视频采集分辨率,譬如640480、320240、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()需要传入一个struct v4l2_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 结构体描述了视频帧大小相关的信息,我们来看看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 类型变量,描述了视频帧大小信息(包括视频帧的宽度和高度),也就是视频采集分辨率大小。
譬如我们要枚举出摄像头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 这种像素格式;通常在这种情况下,底层驱动程序并不会按照我们指定的参数进行设置,它会对这些参数进行修改,譬如,如果摄像头不支持800480,那么底层驱动可能会将其修改为
640480(假设摄像头支持这种分辨率);所以,当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 方式;25.2.2 小节中介绍了使用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_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;
}
读取数据、对数据进行处理
开启视频采集之后,接着便可以去读取数据了,前面我们已经说过,直接读取每一个帧缓冲的在用户空间的映射区即可读取到摄像头采集的每一帧图像数据。在读取数据之前,需要将帧缓冲从内核的帧缓冲队列中取出,这个操作叫做帧缓冲出队(有入队自然就有出队),前面已经给大家详细地介绍了这些理论知识。
使用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 摄像头应用编程实战
通过前面的介绍,我们已经知道如何对摄像头进行应用编程了,摄像头的应用编程其实并不难,基本都是按照那样的一套流程下来即可:打开设备、查询设备、设置格式、申请帧缓冲、内存映射、入队、开启视频采集、出队、对采集到的数据进行处理,虽然步骤很多,但是这些操作步骤都是容易理解的,他并没有让你感觉到很难理解这个步骤,每一个步骤基本都是通过ioctl()来实现,搭配不同请求指令。
本小节我们来编写摄像头应用程序,笔者希望大家能够自己去独立完成,通过前面的介绍,相信大家是能够独立完成的,可以适当地参考下面笔者提供的示例代码:
本例程源码对应的路径为:开发板光盘->11 、Linux C 应用编程例程源码
->25_v4l2_camera->v4l2_camera.c。
#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 上显示采集到的图像。
接下来编译示例代码:
将编译得到的可执行文件拷贝到开发板Linux 系统的用户家目录下:
首先在测试之前,我们的开发板上得插上一个摄像头,这里需要注意一下,前面我们提到开发板出厂系统支持ov5640、ov7725 以及ov2640,这三款摄像头,但是不能同时生效,出厂系统默认配置使能的是ov5640,如果要使用ov7725 或ov2640,则需修改设备树,具体如何修改请大家参考“开发板光盘资料A-基础资料/
【正点原子】I.MX6U 用户快速体验V1.7.3.pdf”文档中的3.16 小节。
这里笔者以ov2640 摄像头为例,笔者的测试板上已经连接了ov2640 摄像头,如下所示:
其它摄像头的安装方式也是如此,头部朝外,注意一定是在启动之前就安装好了、而不是开发板启动之后再安装,切记!如果是USB 摄像头,则可在开发板运行状态下,直接将USB 摄像头插入到开发板上的
USB HOST 接口即可。
接着运行测试程序,我们需要传入一个参数,该参数表示摄像头的对应的设备节点:
程序运行之后,此时开发板LCD 屏上将会显示摄像头所采集到的图像,如下所示:
请大家忽略手机拍摄的问题!
原本运行程序之后,终端会打印出摄像头所支持的像素格式、描述信息以及摄像头所支持的采集分辨率、帧率等信息,但是从图25.3.4 中打印信息可知,程序运行之后只打印了像素格式以及描述信息,并没有打印分辨率和帧率等信息,为什么呢?当然这个不是我们的程序有问题,而是摄像头的驱动功能不够完善,底层驱动并没有去实现这些相关的功能,这里给大家简单地提一下,免得大家以为程序有问题!这里笔者换了一个USB 摄像头,给大家看下它的打印信息,如下所示:
从上图可以看到,程序打印了摄像头所支持的所有采集分辨率大小以及帧率。
好了,本章的内容到此结束了,到此为止,我们已经学习了很多硬件外设的应用编程知识了,大家要学会活学活用,把这些东西用起来,尝试着做一个综合类的好玩的小项目,提高自己的应用编程能力,笔者觉得这是非常重要,你不要跟着笔者的教程一个一个章节往下走,你得停下来思考、多动动手、在教程的基础上多往外扩展,这样你才能进步!大家加油!
实战小项目之视频监控
目前常见的视频监控和视频直播都是使用了RTMP 和RTSP 流媒体传输协议等。
RTSP(Real-Time Stream Protocol)由Real Networks 和Netscape 共同提出的,基于文本的多媒体播放控制协议。RTSP 定义流格式,流数据经由RTP 传输;RTSP 实时效果非常好,适合视频聊天,视频监控等方向。
RTMP(Real Time Message Protocol)由Adobe 公司提出,用来解决多媒体数据传输流的多路复用(Multiplexing)和分包(packetizing)的问题,优势在于低延迟,稳定性高,支持所有摄像头格式,浏览器加载flash 插件就可以直接播放。
RTSP 和RTMP 的区别:
RTSP 虽然实时性最好,但是实现复杂,适合视频聊天和视频监控;RTMP 强在浏览器支持好,加载flash
插件后就能直接播放,所以非常火,相反在浏览器里播放rtsp 就很困难了。
本章我们将向大家介绍如何通过FFmpeg+Nginx、使用RTMP 推流实现视频监控或直播。
视频监控简介
本章我们将使用RTMP 流媒体服务来实现视频监控,RTMP 流媒体服务框架图如下所示:
推流端负责将视频数据通过RTMP 流媒体协议传输给RTMP 流媒体服务器,拉流端可以从流媒体服务器中通过RTMP 协议获取到视频数据;而流媒体服务器负责接收推流端的视频数据、当有客户端(拉流端)想要获取视频数据时再将其发给相应的客户端。
所以从上图可知,要想实现RTMP 视频监控,必须要有这三部分:推流客户端、拉流客户端以及流媒体服务器。那这些需要我们自己去实现吗?当然不需要,譬如推流我们可以使用FFmpeg 来做,拉流则可以实现VLC 播放器来做,而流媒体客户端则使用Nginx 来搭建即可!
Nginx 移植
前面也给大家提到了,我们可以使用Nginx 来搭建RTMP 流媒体服务器,譬如你可以在一台公网IP 主机上搭建流媒体服务器,当然,笔者并没有这个条件;这里我们选择在开发板上搭建流媒体服务器,并且推流端也是开发板,所以在本章的方案中,开发板既是流媒体服务器、也是推断端。
既然要在开发板上搭建流媒体服务器,首先我们需要将Nginx 移植到开发板上,事实上,我们的板子出厂系统中就已经移植好了Nginx,并且板子在启动进入系统时会自动启动Nginx,也就是启动流媒体服务,所以板子启动之后本身就已经是一台流媒体服务器了。当然,这里我们不管出厂系统中已经搭建好的流媒体服务,这里只是给大家说明一下,本章我们要自己动手亲自移植Nginx、然后在板子上搭建流媒体服务。
下载Nginx 源码
进入到Ubuntu 系统的某个目录下,执行下面这条命令下载Nginx 源码:
wget http://nginx.org/download/nginx-1.20.0.tar.gz
这里我们下载的是1.20 版本,这是比较新的版本了。下载完成之后将得到一个名为nginx-1.20.0.tar.gz
的压缩包文件。
这就是Nginx 的源码包。
下载nginx-rtmp-module 模块
事实上,原生的Nginx 并不支持RTMP,我们需要安装第三方模块nginx-rtmp-module 插件使其支持
RTMP。通过下面这条命令下载nginx-rtmp-module。
git clone https://github.com/arut/nginx-rtmp-module.git
下载成功之后将得到nginx-rtmp-module 文件夹。
交叉编译Nginx
将下载得到的nginx-1.20.0.tar.gz 文件进行解压:
tar -xzf nginx-1.20.0.tar.gz
解压之后生成nginx-1.20 文件夹,进入到该目录下。在进行交叉编译之前,先对交叉编译工具进行初始化操作:
source /opt/fsl-imx-x11/4.1.15-2.1.0/environment-setup-cortexa7hf-neon-poky-linux-gnueabi
大家根据自己的实际安装位置填写路径。
首先配置源码、接下执行make 编译源码,最后执行make install 安装即可!总共的步骤就这三个,但事实上在编译的过程中会出现一些问题,我们稍后再看!
配置源码
首先第一步是配置源码,在配置之前,需要进行一个简单的修改,否则配置通不过;首先打开nginx 源码目录下的auto/cc/name 文件,将21 行处的”exit 1”给注释掉!如下所示:
修改完成之后保存退出。接着打开auto/types/sizeof 文件,将15 行处的” ngx_size=”修改为” ngx_size=4”,并且将36 行处的”$CC”修改为”gcc”,如下所示:
同样,修改完成之后保存退出即可!接着执行下面这条命令进行配置:
./configure --prefix=/home/dt/tools/nginx-1.20.0/install \
--with-http_ssl_module \
--with-http_mp4_module \
--with-http_v2_module \
--without-http_upstream_zone_module \
--add-module=/home/dt/tools/nginx-rtmp-module
上述命令中,–prefix 指定了nginx 的安装路径,笔者为了方便直接将其安装到nginx 源码目录下的install
目录中;–add-module 用于添加第三方模块,譬如我们前面下载的nginx-rtmp-module,所以–add-module 需要指向nginx-rtmp-module 源码路径,大家根据自己的实际路径填写。
如下所示:
配置成功打印信息如下所示:
编译源码
配置完成之后,接着我们执行make 编译:
本次编译并不会成功,将会出现如下错误打印信息:
这个时候我们需要修改nginx 源码目录下的objs/ngx_auto_config.h 文件,将如下内容添加到该头文件中:
#ifndef NGX_HAVE_SYSVSHM
#define NGX_HAVE_SYSVSHM 1
#endif
添加完成之后保存退出,再次执行make 编译,这样就会编译成功了。
安装
编译成功之后,接着我们进行安装,执行make install
笔者将nginx 安装到了nginx-1.20.0 目录下的install 目录中,进入到install 目录下:
conf 目录下有很多的配置文件,如下所示:
其中nginx.conf 配置文件很重要,后续我们会对该配置文件进行相应的配置。
sbin 目录下有一个可执行程序nginx:
这个可执行文件是一个“not stripped”文件,说明该文件包含了很多的调试信息,所以导致该文件特别大7.5MB,可以执行下面这条命令去掉调试信息、减少文件的大小:
arm-poky-linux-gnueabi-strip --strip-debug nginx
nginx 可执行程序用于启动流媒体服务。
现在我们需要将安装目录下的这些文件拷贝到开发板Linux 系统上,再进行拷贝之前,需要先将开发板出厂系统中已经移植好的nginx 给移除,进入到开发板Linux 系统中,执行下面这些命令移除出厂系统自带的nginx 程序和相应的配置文件:
rm -rf /usr/sbin/nginx
rm -rf /etc/nginx/*
接下我们将nginx 安装目录下sbin 中的nginx 拷贝到开发板Linux 系统/home/root 目录下,如下所示:
接着再将安装目录下的conf、logs、html 文件夹拷贝到开发板Linux 系统的/etc/nginx 目录下,如下所示:
测试nginx
上小节我们已经将nginx 移植到了开发板上,本小节进行测试、验证,看看nginx 是否能够正常工作。先重启开发板,重启进入系统后,进入到/home/root 目录下,执行nginx 程序。
./nginx -V # 查看版本信息
执行./nginx -h 查看帮助信息:
接下来我们要启动nginx,执行如下命令:
./nginx -p /etc/nginx
此时nginx 服务便在后台运行了,通过ps 命令可查看到:
ps -aux
此时我们可以打开电脑浏览器,输入开发板的IP 地址,如下所示:
按下回车,如下所示:
如果显示出上面这个页面,表示我们的nginx 已经正常工作了。
配置nginx
后续我们会使用FFmpeg 进行推流,将视频流通过RTMP 推给nginx 流媒体服务器,在此之前,我们需要对nginx 进行配置,打开nginx 的配置文件/etc/nginx/conf/nginx.conf,添加如下内容:
rtmp
{
server
{
listen 1935;
#监听1935 端口
chunk_size 4096;
application live
{
allow publish 127.0.0.1;
allow play all;
live on;
#打开直播 record off;
#关闭record meta copy;
}
application hls
{
live on;
hls on;
hls_path / tmp / hls;
hls_fragment 8s;
}
}
}
如下所示:
添加完成之后保存退出即可!
接着执行如下命令重启nginx:
./nginx -p /etc/nginx -s reload
使用FFmpeg 推流
nginx 重启之后,接着我们便可使用FFmpeg 进行推流,将视频流数据通过RTMP 推给nginx 流媒体服务器,执行如下命令进行推流:
ffmpeg -re -i /run/media/mmcblk0p1/testVideo.mp4 -c:av copy -f flv rtmp://127.0.0.1/live/mytest
简单地介绍一下这些参数,首先-i 表示输入视频数据,这里我们使用了一个mp4 视频文件;
rtmp://127.0.0.1/live/mytest 表示将视频流通过RTMP 推给流媒体服务器,这里因为我们的服务器和推流端都是开发板,所以这个IP 地址127.0.0.1 指的就是本机的流媒体服务器。
现在我们可以进行拉流了,可以将我们的Windows 主机作为拉流端,使用VLC 软件进行拉流,VLC 软件大家自己下载、安装好。安装好之后打开VLC,如下所示:
点击左上角“媒体”—>“打开网络串流”:
输入流媒体服务器的IP 地址以及路径,笔者使用的开发板对应的IP 地址为192.168.1.114,点击“播放”即可从RTMP 流媒体服务器拉取视频数据进行播放,如下所示:
既有画面也有声音!
接下来我们再使用摄像头进行测试,使用FFmpeg 采集摄像头视频数据将其发送给nginx 流媒体服务器,执行如下命令:
ffmpeg -f v4l2 -video_size 320x240 -framerate 15 -i /dev/video2 -q 10 -f flv rtmp://127.0.0.1/live/mytest
这里笔者使用了一个USB 摄像头进行测试,命令执行之后,接着在Windows 下使用VLC 拉流播放摄像头采集到的画面,实现视频摄像头监控。
文章来源:https://www.toymoban.com/news/detail-681886.html
测试发现,延迟太高了,导致开发板当前采集到的画面与VLC 播放到的画面并不同步,笔者实测大概有5、6 秒的延迟,为什么会这样呢?笔者认为有两方面的原因:
⚫ 我们的I.MX6U 开发板性能太弱了,虽然我们执行的是一条命令,但是FFmpeg 内部却进行很多的处理,譬如对视频、音频数据的处理,由于I.MX6U 本就没有硬件视频解码,完全依赖于软件处理,导致会耗费相当大的时间。
⚫ 在我们本次实验中,由于服务器和推流端都是开发板,本来性能就弱,还做了这么多的工作,必然就会导致延迟大,你可以使用一款高性能的开发板进行测试对比,你就会发现确实是咋们使用开发板性能太弱了!
好了,本章的内容就到此结束了。文章来源地址https://www.toymoban.com/news/detail-681886.html
到了这里,关于V4L2 摄像头应用编程的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!