由于是第一次接触UVC,所以内容会比较杂。文章内容多为参考整合。
参考链接:
【1】添加内核支持部分:https://blog.csdn.net/u010034969/article/details/115210890
【2】100ask-摄像头V4L2编程应用开发:http://100ask.org/pages/cfba84/#_7-1-v4l2%E7%AE%80%E4%BB%8B
【3】完整代码来源:https://download.csdn.net/download/luckywang1103/6707987
之前在淘宝上买了个USB摄像头,最近想着在我的imx6ull开发板上应用试试,在这里做个记录。
下面是从淘宝商品详情找到的产品参数:
附上目录:
文章目录
- 1.相关概念
- 1.1 UVC
- UVC设备的PID和VID简介
- 1.2 Linux下V4L2简介
- 1.3 V4L2视频采集原理
- 1.4 ==V4L2应用程序实现流程==
- 1.4.* 一些常用的命令表示符
- 2.环境搭建
- 2.1 内核添加UVC支持
- 2.1.1 查看PID与VID
- 2.1.2 内核图形配置界面添加UVC设备支持
- 2.1.3 内核中增加摄像头的PID和VID
- 2.2 环境安装
- 2.2.1 JPEGLIB
- 3. 各步骤详细编程实现
- 3.1. 打开设备
- 3.2. 查询设备属性
- 3.3. 获取视频格式
- 3.4. 设置图像帧格式
- 3.5. 申请缓冲区
- 3.6. 对申请的帧缓冲内存映射
- 3.7. 将申请的缓冲帧放入队列,并启动数据流
- 3.8. 启动捕捉图像数据
- 3.9. 出列采集的帧缓冲,并处理图像数据,然后再将数据帧入列
- 3.10. 停止捕捉图像数据
- 3.11 对数据进行处理
- 4. 应用代码实现
1.相关概念
1.1 UVC
UVC全称为USB Video Class,即:USB视频类,是一种为USB视频捕获设备定义的协议标准。UVC是Microsoft与另外几家设备厂商联合推出的为USB视频捕获设备定义的协议标准,已成为USB org标准之一。支持 USB Video Class (UVC) standard 1.1可以让相机在所有的作业系统以及平台中使用(Windows, Linux, Mac etc.)。用户只需连接相机便可进行图像传输,而无需安装任何驱动程序 。
简单点说,就是只要USB摄像头是UVC摄像头,那这个摄像头的驱动就遵循一个通用的格式,可以实现免驱的操作。在Linux系统中,UVC驱动的支持在Linux Kernel 2.4之后被增加到内核中。但是为了让内核识别到这款摄像头,还要告诉内核这个USB的ID是UVC设备才行。
UVC设备的PID和VID简介
每个USB设备都有VID(Vender ID,供应商识别码)和PID(Product ID,产品识别码),两者的长度均为2Byte。PID和VID是主机识别USB设备时使用。
主机检测到USB设备后,首先会通过USB Class查询插入的是什么设备。检测到插入设备的类型后,通过读取和检索VID和PID,主机就能知道当前连接的设备的类型,并能了解应该给这个USB设备加载什么样的驱动程序进行通信。
例如,我们插入了U盘
- 系统首先会检测插入USB设备的Class。
- U盘的Class是0x08 —— Mass Storage。那么就会按照USB大容量存储设备的方式对USB设备进行操作。
- 同时,系统还会查看设备的VID和PID,识别该U盘的制造商和型号,并查看是否需要加载特殊的驱动。
1.2 Linux下V4L2简介
V4L2是Video for linux2的简称,是linux中关于视频设备的内核驱动。在Linux中,视频设备是设备文件,可以像访问普通文件一样对其进行读写或是MMAP。
摄像头的设备节点在/dev/video下,如果只有一个视频设备,通过为/dev/video0,也有可能是/dev/uvcvideo。
1.3 V4L2视频采集原理
参考100ask-摄像头V4L2编程应用开发
使用V4L2采集图像,需要对收集到的图像进行如下步骤:
- 分配帧缓冲区
- 将分配到的帧缓冲区从内核空间映射到用户空间
- 将申请到的帧缓冲区在视频采集输入队列中排队
- 等待数据的到来
数据传输流程:
- 首先驱动程序采集一帧图像数据,并放入第一个帧缓冲区
- 第一个帧缓冲区存满一帧图像数据后,将该缓冲区放入视频采集输出队列,等待应用程序取出
- 应用程序取出图像数据后可以对图像数据进行处理或存储操作,然后将该帧缓冲区放入视频采集输入队列的尾部
- 驱动程序采集下一帧数据,放入第二个缓冲区,存满一帧数据后放入输出队列
循环上面几步,就可以实现循环采集。流程图如下:
1.4 V4L2应用程序实现流程
V4L2视频采集一般分为以下5步:
- 打开设备,进行初始化参数设置,通过V4L2接口设置视频图像的采集窗口、采集的点阵大小和格式;
- 申请图像帧缓冲,并进行内存映射,将这些帧缓冲从内核空间映射到用户空间,便于应用程序读取、处理图像数据;
- 将帧缓冲进行入队操作,启动视频采集;
- 驱动开始视频数据的采集,应用程序从视频采集输出队列取出帧缓冲区,处理完后,将帧缓冲区重新放入视频采集输入队列,循环往复采集连续的视频数据;
- 释放资源,停止采集工作。
1.4.* 一些常用的命令表示符
(1)VIDIOC_REQBUFS:分配内存;(2)VIDIOC_QUERYBUF:把VIDIOC_REQBUFS中分配的数据缓存转换成物理地址;(3)VIDIOC_QUERYCAP:查询驱动功能;(4)VIDIOC_ENUM_FMT:获取当前驱动支持的视频格式;(5)VIDIOC_S_FMT:设置当前驱动的视频捕获格式;(6)VIDIOC_G_FMT:读取当前驱动的视频捕获格式;(7)VIDIOC_TRY_FMT:验证当前驱动的显示格式;(8)VIDIOC_CROPCAP:查询驱动的修剪功能;(9)VIDIOC_S_CROP:设置视频信号的边框;(10)VIDIOC_G_CROP:读取视频信号的边框;(11)VIDIOC_QBUF:把数据从缓存中读取出来;(12)VIDIOC_DQBUF:把数据放回缓存队列;(13)VIDIOC_STREAMOP:开始视频显示函数;(14)VIDIOC_STREAMOFF:结束视频显示函数;(15)VIDIOC_QUERYSTD:检查当前视频设备支持的标准,例如PAL或NTSC;
2.环境搭建
本部分参考https://blog.csdn.net/u010034969/article/details/115210890
首先需要在内核中添加UVC支持,为了让这个板子正常识别到这个摄像头,需要在内核中打开UVC摄像头的编译,并将摄像头的PID和VID加入到代码中。
2.1 内核添加UVC支持
2.1.1 查看PID与VID
将摄像头插入电脑USB接口,借助windows的设备管理器,查看对应摄像头的属性。并在详细信息中找到硬件ID。
得到其vid和pid:
- VID:0x0C45
- PID:0x64AB
保存PID和VID备用。
2.1.2 内核图形配置界面添加UVC设备支持
进入内核根目录,make menuconfig
进入内核编译配置。打开如下几项
Device Drives---> Multimedia support---> <*>Media USB Adapters---> V4L platform devices---> <*>SoC camera support---> <*>platform camera support
下面直接引用参考链接【1】的图。
- 进入Device Drivers → Multimedia support → Media USB Adapters,找到并开启USB Video Class(UVC)如下图:
如果找不到这项,说明是相关的依赖项未打开。
可以搜索关键词USB_VIDEO_CLASS,能看到这项的依赖项:
将所有依赖项设置为Y(编译进内核)即可。- 同样,Device Drivers → Multimedia support → V4L platform devices下,开启如下两项:
2.1.3 内核中增加摄像头的PID和VID
打开内核代码driver/media/usb/uvc/uvc_driver.c,找到usb_device_id结构体uvc_ids。
仿照其他摄像头的代码,添加摄像头的PID和VID:
static struct usb_device_id uvc_ids[] = {
//下面是新增的摄像头的id信息,VID是0x05A3,PID是0x9601{.match_flags = USB_DEVICE_ID_MATCH_DEVICE| USB_DEVICE_ID_MATCH_INT_INFO,.idVendor = 0x0C45, //摄像头的VID.idProduct = 0x64AB,.bInterfaceClass = USB_CLASS_VIDEO,.bInterfaceSubClass = 1,.bInterfaceProtocol = 0,.driver_info = UVC_QUIRK_RESTRICT_FRAME_RATE},
//下面是原有的信息/* LogiLink Wireless Webcam */{.match_flags = USB_DEVICE_ID_MATCH_DEVICE| USB_DEVICE_ID_MATCH_INT_INFO,.idVendor = 0x0416,.idProduct = 0xa91a,.bInterfaceClass = USB_CLASS_VIDEO,.bInterfaceSubClass = 1,.bInterfaceProtocol = 0,.driver_info = UVC_QUIRK_PROBE_MINMAX},//省略...
}
完成后make整个内核,并将内核烧写入硬件平台中。
启动平台后插入摄像头,如果出现如下uvc的打印信息,显示出了摄像头的PID&VID并挂在了input下,则说明添加成功。
[ 55.563704] usb 2-1.3: new high-speed USB device number 3 using ci_hdrc
[ 55.703344] uvcvideo: Found UVC 1.00 device Integrated_Webcam_HD (0c45:64ab)
[ 55.733330] input: Integrated_Webcam_HD as /devices/platform/soc/2100000.aips-bus/2184200.usb/ci_hdrc.1/usb2/2-1/2-1.3/2-1.3:1.0/input/input4
可以通过如下指令查看新增的设备节点。
/lib/modules # ls /dev/
video0 video1
在这里新增的是/dev/video1。
2.2 环境安装
2.2.1 JPEGLIB
摄像头输出数据为MJPEG格式(怎么查看支持的输出格式在后面有写到(VIDIOC_G_FMT指令)),需要使用jpeglib库将数据转化为rbg的格式。
官网下载安装包,解压。
./configure CC=arm-linux-gnueabihf-gcc LD=arm-linux-gnueabihf-ld --host=arm-linux-gnueabihf --prefix=/home/book/linux_study/IMX6ULL/Project/Downloads/jpeg-9e/temp --exec-prefix=/home/book/linux_study/IMX6ULL/Project/Downloads/jpeg-9e/temp --enable-shared --enable-staticmake
make install
这里的CC、LD用于指定编译器与链接器,host指定运行主机,–prefix指定安装目录,安装完成后文件都在–prefix指定的目录中,–enable-shared 选项开启动态库支持。
安装完成之后,需要把对应的头文件和库文件移植到运行环境中,通过下面的语句查看交叉编译链的库文件和头文件所在目录。
echo 'main(){}' | arm-linux-gnueabihf-gcc -E -v -
进入上面指定的目录/home/book/linux_study/IMX6ULL/Project/Downloads/jpeg-9e/temp
,复制lib与include下的文件到对应的目录。
cp lib/* -rfd /home/book/linux_study/100ask_imx6ull-sdk/ToolChain/gcc-linaro-6.2.1-2016.11-x86_64_arm-linux-gnueabihf/bin/../arm-linux-gnueabihf/libc/usr/lib/
cp include/* /home/book/linux_study/100ask_imx6ull-sdk/ToolChain/gcc-linaro-6.2.1-2016.11-x86_64_arm-linux-gnueabihf/bin/../arm-linux-gnueabihf/libc/usr/include
以及同样需要复制到开发板的文件系统中,对应/usr/lib
与/usr/include
至此jpeglib移植完成,在对程序进行交叉编译时,需要加上链接选项--ljpeg
。
头文件包含:#include <jpeglib.h>
,可在测试程序中包含头文件并编译检查是否移植成功。
3. 各步骤详细编程实现
V4L2的代码主要在video2lcd/video/v4l2.c
文件中。
本章内容参考搬运自100ask-摄像头V4L2编程应用开发
3.1. 打开设备
devname = "/dev/video1";
if(argc==2) devname = argv[1];int video_fd = open(devname, O_RDWR, 0); //打开摄像头设备,使用阻塞方式打开
if (video_fd <0)
{printf("open device error\n");return -1;
}
3.2. 查询设备属性
通过VIDIOC_QUERYCAP
命令来查询driver是否合乎规范。因为V4L2要求所有driver和device都支持这个ioctl。所以,通过VIDIOC_QUERYCAP
命令是否成功来判断当前device和driver是否符合V4L2规范。当然,这个命令执行成功的同时还能够得到设备足够的信息,如struct v4l2_capability
结构体所示内容。19~22行代码检查当前设备是否为capture设备,并检查使用内存映射还是直接读的方式获取图像数据。
1 struct v4l2_capability tV4l2Cap;
2 iError = ioctl(video_fd , VIDIOC_QUERYCAP, &tV4l2Cap);
3 memset(&tV4l2Cap, 0, sizeof(struct v4l2_capability));
4 iError = ioctl(video_fd , VIDIOC_QUERYCAP, &tV4l2Cap);
5 if (iError) {
6 DBG_PRINTF("Error opening device %s: unable to query device.\n", strDevName);
7 goto err_exit;
8 }
9
10 if (!(tV4l2Cap.capabilities & V4L2_CAP_VIDEO_CAPTURE))//检测是否为capture设备
11 {
12 DBG_PRINTF("%s is not a video capture device\n", strDevName);
13 goto err_exit;
14 }
15
16 if (tV4l2Cap.capabilities & V4L2_CAP_STREAMING) {
17 DBG_PRINTF("%s supports streaming i/o\n", strDevName);
18 }
19
20 if (tV4l2Cap.capabilities & V4L2_CAP_READWRITE) {
21 DBG_PRINTF("%s supports read i/o\n", strDevName);
22 }
下面是结构体的具体内容
01 struct v4l2_capability 02 { 03 __u8 driver[16]; // 驱动名字 04 __u8 card[32]; // 设备名字 05 __u8 bus_info[32]; // 设备在系统中的位置 06 __u32 version; // 驱动版本号 07 __u32 capabilities; // 设备支持的操作 08 __u32 reserved[4]; // 保留字段 09 };
3.3. 获取视频格式
结构体
v4l2_fmtdesc
的内容如下,该结构体描述当前camera支持的格式信息。01 struct v4l2_fmtdesc 02 { 03 __u32 index; // 要查询的格式序号,应用程序设置 04 enum v4l2_buf_type type; // 帧类型,应用程序设置 05 __u32 flags; // 是否为压缩格式 06 __u8 description[32]; // 格式名称 07 __u32 pixelformat; //所支持的格式 08 __u32 reserved[4]; // 保留 09 };
使用VIDIOC_ENUM_FMT
命令查询当前camera支持的所有格式。
struct v4l2_fmtdes
c结构体中index
要设置,从0开始;
enum v4l2_buf_type type
也要设置,如果使用的是camera设备,则enum v4l2_buf_type type
要设置为V4L2_BUF_TYPE_VIDEO_CAPTURE
,因为camera是CAPTURE设备。
结构体中的其他内容driver会填充。其中__u32 pixelformat
参数在设置图像帧格式时需要使用。
struct v4l2_fmtdesc tFmtDesc;
memset(&tFmtDesc, 0, sizeof(tFmtDesc));
tFmtDesc.index = 0;
tFmtDesc.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
while ((iError = ioctl(iFd, VIDIOC_ENUM_FMT, &tFmtDesc)) == 0) {//查询当前camera支持的所有格式。if (isSupportThisFormat(tFmtDesc.pixelformat)){ptVideoDevice->iPixelFormat = tFmtDesc.pixelformat;break;}printf("enum fmt desc\n");printf("%d.%s\n",tFmtDesc.index,tFmtDesc.description);tFmtDesc.index++;
}//经查询,买的摄像头只支持MJPEG
3.4. 设置图像帧格式
设置图像帧格式需要用到
struct v4l2_format
结构体,该结构体描述每帧图像的具体格式,包括帧类型以及图像的长、宽等信息。01 struct v4l2_format 02 { 03 enum v4l2_buf_type type; // 帧类型,应用程序设置 04 union fmt 05 { 06 struct v4l2_pix_format pix; // 视频设备使用 07 structv 4l2_window win; 08 struct v4l2_vbi_format vbi; 09 struct v4l2_sliced_vbi_format sliced; 10 __u8 raw_data[200]; 11 }; 12 };
struct v4l2_format
结构体需要设置enum v4l2_buf_type type
==和union fmt
中的struct v4l2_pix_format pix
。
enum v4l2_buf_type type
因为使用的是camera设备,camera是CAPTURE设备,所以设置成V4L2_BUF_TYPE_VIDEO_CAPTURE。
struct v4l2_pix_format pix
设置一帧图像的长、宽和格式等,由于要适配LCD输出,所以长、宽设置为LCD支持的长、宽,如124~125行所示。
struct v4l2_format tV4l2Fmt; //设置获取视频的格式
memset( &tV4l2Fmt, 0, sizeof(tV4l2Fmt));
tV4l2Fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; //视频数据流类型,永远都是V4L2_BUF_TYPE_VIDEO_CAPTURE
tV4l2Fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_MJPEG;//视频源的格式为JPEG或YUN4:2:2或RGB
tV4l2Fmt.fmt.pix.width = iLcdWidth; //设置视频宽度
tV4l2Fmt.fmt.pix.height = iLcdHeight; //设置视频高度
tV4l2Fmt.fmt.pix.field = V4L2_FIELD_ANY;
if (ioctl(fd, VIDIOC_S_FMT, &tV4l2Fmt) < 0) //使配置生效
{printf("set format failed\n");return -1;
}
设置后可通过VIDIOC_G_FMT
读取格式查看是否设置成功。
3.5. 申请缓冲区
相关结构体如下,该结构体描述申请的缓冲区的基本信息。
01 struct v4l2_requestbuffers 02 { 03 __u32 count; // 缓冲区内缓冲帧的数目 04 enum v4l2_buf_type type; // 缓冲帧数据格式 05 enum v4l2_memorymemory; // 区别是内存映射还是用户指针方式 06 __u32 reserved[2]; 07 };
- 申请一个拥有四个缓冲帧的缓冲区,
__u32 count
为缓冲帧的数目; enum v4l2_buf_type type
和前文一样,同样设置成V4L2_BUF_TYPE_VIDEO_CAPTURE
;enum v4l2_memorymemory
用来区分是内存映射还是用户指针,我们使用内存映射的方式,所以设置成V4L2_MEMORY_MMAP
。
struct v4l2_requestbuffers tV4l2ReqBuffs; //申请帧缓冲
/* request buffers */
memset(&tV4l2ReqBuffs, 0, sizeof(struct v4l2_requestbuffers));
tV4l2ReqBuffs.count = NB_BUFFER; //4,缓存数量,即可保存的图片数量
tV4l2ReqBuffs.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; //数据流类型,永远都是V4L2_BUF_TYPE_VIDEO_CAPTURE
tV4l2ReqBuffs.memory = V4L2_MEMORY_MMAP; //存储类型:V4L2_MEMORY_MMAP或V4L2_MEMORY_USERPTRiError = ioctl(iFd, VIDIOC_REQBUFS, &tV4l2ReqBuffs);//使配置生效
if (iError)
{DBG_PRINTF("Unable to allocate buffers.\n");goto err_exit;
}
3.6. 对申请的帧缓冲内存映射
相关结构体如下,该结构体表示一帧图像数据的基本信息,包含序号、缓冲帧长度和缓冲帧地址等信息。
01 struct v4l2_buffer 02 { 03 __u32 index; //buffer 序号 04 enum v4l2_buf_type type; //buffer 类型 05 __u32 byteused; //buffer 中已使用的字节数 06 __u32 flags; // 区分是MMAP 还是USERPTR 07 enum v4l2_field field; 08 struct timeval timestamp; // 获取第一个字节时的系统时间 09 struct v4l2_timecode timecode; 10 __u32 sequence; // 队列中的序号 11 enum v4l2_memory memory; //IO 方式,被应用程序设置 12 union m 13 { 14 __u32 offset; // 缓冲帧地址,只对MMAP 有效 15 unsigned long userptr; 16 }; 17 __u32 length; // 缓冲帧长度 18 __u32 input; 19 __u32 reserved; 20 };
将内核空间的帧缓冲映射到用户空间,需要两个数据接收帧缓冲的长度和地址,
这里我们需要自己定义一个结构体,其中iVideoBufMaxLen接收帧缓冲的长度,pucVideBuf接收帧缓冲地址。
16 struct VideoDevice {
17 int iFd; //设备文件句柄
18 int iPixelFormat; //支持的像素格式
19 int iWidth; //视频宽度与高度
20 int iHeight;
21
22 int iVideoBufCnt; //表示缓冲区个数
23 int iVideoBufMaxLen; //表示接收帧缓冲区长度
24 int iVideoBufCurIndex;//表示当前缓冲区索引
25 unsigned char *pucVideBuf[NB_BUFFER]; //用于存储各个接收缓冲区的地址。
26
27 /* 函数 */
28 PT_VideoOpr ptOPr;
29 };
mmap原型:
void *mmap(void*addr, size_t length, int prot, int flags, int fd, off_t offset);
参数具体的含义:
addr:映射起始地址,一般为NULL,让内核自动选择;
length:被映射内存块的长度;
prot:标志映射后能否被读写,其值为PROT_EXEC,PROT_READ,PROT_WRITE,PROT_NONE;
flags:确定此内存映射能否被其他进程共享,可设置为MAP_SHARED或MAP_PRIVATE;
fd:设备文件句柄;
offset:确定映射后的内存地址
以下代码使用VIDIOC_QUERYBUF
命令和mmap
函数将内核空间的缓冲区映射到用户空间。
VIDIOC_QUERYBUF
命令的使用需要参数struct v4l2_buffer
结构体- 结构体中的
type
、memory
和index
参数需要设置type
和memory
和前文中的设置一样,分别设置成V4L2_BUF_TYPE_VIDEO_CAPTURE
和V4L2_MEMORY_MMAP
index
参数表示申请的缓冲帧的标号,从0开始,包含申请的所有缓冲帧。
- 结构体中的
struct v4l2_buffer tV4l2Buf;
/* map the buffers */
for (i = 0; i < ptVideoDevice->iVideoBufCnt; i++)
{memset(&tV4l2Buf, 0, sizeof(struct v4l2_buffer));tV4l2Buf.index = i; //申请的缓冲帧的标号tV4l2Buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;tV4l2Buf.memory = V4L2_MEMORY_MMAP; //MMAP方式访问iError = ioctl(iFd, VIDIOC_QUERYBUF, &tV4l2Buf); //把VIDIOC_REQBUFS中分配的数据缓存转换成物理地址;if (iError){DBG_PRINTF("Unable to query buffer.\n");goto err_exit;}ptVideoDevice->iVideoBufMaxLen = tV4l2Buf.length;ptVideoDevice->pucVideBuf[i] = mmap(0 /* start anywhere */ ,tV4l2Buf.length, PROT_READ, MAP_SHARED, iFd,tV4l2Buf.m.offset);//将内核空间的缓冲区映射到用户空间。if (ptVideoDevice->pucVideBuf[i] == MAP_FAILED){DBG_PRINTF("Unable to map buffer\n");goto err_exit;}
}
3.7. 将申请的缓冲帧放入队列,并启动数据流
使用VIDIOC_QBUF命令,将申请的缓冲帧依次放入缓冲帧输入队列,等待被图像采集设备依次填满。
/* Queue the buffers. */
for (i = 0; i < ptVideoDevice->iVideoBufCnt; i++)
{memset(&tV4l2Buf, 0, sizeof(struct v4l2_buffer));tV4l2Buf.index = i;tV4l2Buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;tV4l2Buf.memory = V4L2_MEMORY_MMAP;iError = ioctl(iFd, VIDIOC_QBUF, &tV4l2Buf);//把数据从缓存中读取出来;if (iError){DBG_PRINTF("Unable to queue buffer.\n");goto err_exit;}
}
3.8. 启动捕捉图像数据
启动捕捉图像数据使用VIDIOC_STREAMON命令,当该命令执行成功后,便可以等待图像数据的到来。
/**********************************************************************
* 函数名称:V4l2StartDevice
* 功能描述:开始捕捉图像数据
* 输入参数:ptVideoDevice
* 输出参数:无
* 返 回 值:无
* 修改日期 版本号 修改人 修改内容
* -----------------------------------------------
* 2020/02/16 V1.0 zhenhua 创建
***********************************************************************/
static int V4l2StartDevice(PT_VideoDevice ptVideoDevice)
{int iType = V4L2_BUF_TYPE_VIDEO_CAPTURE;//数据流类型,永远都是V4L2_BUF_TYPE_VIDEO_CAPTUREint iError;iError = ioctl(ptVideoDevice->iFd, VIDIOC_STREAMON, &iType);//启动捕捉图像数据if (iError){DBG_PRINTF("Unable to start capture.\n");return -1;}return 0;
}
3.9. 出列采集的帧缓冲,并处理图像数据,然后再将数据帧入列
我们可以使用VIDIOC_DQBUF
命令,等待缓冲帧的到来,当有缓冲帧被放入视频输出缓冲队列,我们便可以采到一帧图像。接收到图像我们可以对图像进行操作,例如保存、压缩或者LCD输出等。
/*********************************************************************** 函数名称:V4l2GetFrameForStreaming* 功能描述:从图像数据流中获取一帧图像数据* 输入参数:ptVideoDeviceptVideoBuf* 输出参数:无* 返 回 值:无* 修改日期 版本号 修改人 修改内容* -----------------------------------------------* 2020/02/16 V1.0 zhenhua 创建***********************************************************************/
static int V4l2GetFrameForStreaming(PT_VideoDevice ptVideoDevice, PT_VideoBuf ptVideoBuf)
{struct pollfd tFds[1];int iRet;struct v4l2_buffer tV4l2Buf;/* poll */tFds[0].fd = ptVideoDevice->iFd;tFds[0].events = POLLIN;iRet = poll(tFds, 1, -1);if (iRet <= 0){DBG_PRINTF("poll error!\n");return -1;}/* VIDIOC_DQBUF */memset(&tV4l2Buf, 0, sizeof(struct v4l2_buffer));tV4l2Buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;tV4l2Buf.memory = V4L2_MEMORY_MMAP;iRet = ioctl(ptVideoDevice->iFd, VIDIOC_DQBUF, &tV4l2Buf);//VIDIOC_DQBUF把数据放回缓存队列;if (iRet < 0){DBG_PRINTF("Unable to dequeue buffer.\n");return -1;}ptVideoDevice->iVideoBufCurIndex = tV4l2Buf.index;ptVideoBuf->iPixelFormat = ptVideoDevice->iPixelFormat;ptVideoBuf->tPixelDatas.iWidth = ptVideoDevice->iWidth;ptVideoBuf->tPixelDatas.iHeight = ptVideoDevice->iHeight;ptVideoBuf->tPixelDatas.iBpp = (ptVideoDevice->iPixelFormat == V4L2_PIX_FMT_YUYV) ? 16 : \(ptVideoDevice->iPixelFormat == V4L2_PIX_FMT_MJPEG) ? 0 : \(ptVideoDevice->iPixelFormat == V4L2_PIX_FMT_RGB565) ? 16 : \0;ptVideoBuf->tPixelDatas.iLineBytes = ptVideoDevice->iWidth * ptVideoBuf->tPixelDatas.iBpp / 8;ptVideoBuf->tPixelDatas.iTotalBytes = tV4l2Buf.bytesused;ptVideoBuf->tPixelDatas.aucPixelDatas = ptVideoDevice->pucVideBuf[tV4l2Buf.index];return 0;
}
当我们从缓冲帧输出队列取出一个缓冲帧,取出图像数据后我们需要将缓冲帧重新放回到视频输入缓冲队列,该操作还是使用VIDIOC_QBUF命令,放回缓冲帧输入队列后继续等待被填满。
296 /**********************************************************************
297 * 函数名称:V4l2PutFrameForStreaming
298 * 功能描述:将取出的帧缓冲重新放回图像输入队列
299 * 输入参数:ptVideoDevice
300 ptVideoBuf
301 * 输出参数:无
302 * 返 回 值:无
303 * 修改日期 版本号 修改人 修改内容
304 * -----------------------------------------------
305 * 2020/02/16 V1.0 zhenhua 创建
306 ***********************************************************************/
307 static int V4l2PutFrameForStreaming(PT_VideoDevice ptVideoDevice, PT_VideoBuf ptVideoBuf)
308 {
309 /* VIDIOC_QBUF */
310 struct v4l2_buffer tV4l2Buf;
311 int iError;
312
313 memset(&tV4l2Buf, 0, sizeof(struct v4l2_buffer));
314 tV4l2Buf.index = ptVideoDevice->iVideoBufCurIndex;
315 tV4l2Buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
316 tV4l2Buf.memory = V4L2_MEMORY_MMAP;
317 iError = ioctl(ptVideoDevice->iFd, VIDIOC_QBUF, &tV4l2Buf);
318 if (iError)
319 {
320 DBG_PRINTF("Unable to queue buffer.\n");
321 return -1;
322 }
323 return 0;
324 }
3.10. 停止捕捉图像数据
停止采集图像数据,首先使用VIDIOC_STREAMOFF命令,关闭捕获图像数据。同时要注意取消内存映射和关闭句柄,防止不必要的内存泄漏。代码390407行为停止捕捉图像数据命令;代码227241行为取消内存映射和关闭句柄。
380 /**********************************************************************
381 * 函数名称:V4l2StopDevice
382 * 功能描述:停止捕捉图像数据
383 * 输入参数:ptVideoDevice
384 * 输出参数:无
385 * 返 回 值:无
386 * 修改日期 版本号 修改人 修改内容
387 * -----------------------------------------------
388 * 2020/02/16 V1.0 zhenhua 创建
389 ***********************************************************************/
390 static int V4l2StopDevice(PT_VideoDevice ptVideoDevice)
391 {
392 int iType = V4L2_BUF_TYPE_VIDEO_CAPTURE;
393 int iError;
394
395 iError = ioctl(ptVideoDevice->iFd, VIDIOC_STREAMOFF, &iType);
396 if (iError)
397 {
398 DBG_PRINTF("Unable to stop capture.\n");
399 return -1;
400 }
401 return 0;
402 }
403
404 static int V4l2GetFormat(PT_VideoDevice ptVideoDevice)
405 {
406 return ptVideoDevice->iPixelFormat;
407 }217 /**********************************************************************
218 * 函数名称:V4l2ExitDevice
219 * 功能描述:退出采集设备,取消帧缓冲映射和关闭句柄
220 * 输入参数:ptVideoDevice
221 * 输出参数:无
222 * 返 回 值:无
223 * 修改日期 版本号 修改人 修改内容
224 * -----------------------------------------------
225 * 2020/02/16 V1.0 zhenhua 创建
226 ***********************************************************************/
227 static int V4l2ExitDevice(PT_VideoDevice ptVideoDevice)
228 {
229 int i;
230 for (i = 0; i < ptVideoDevice->iVideoBufCnt; i++)
231 {
232 if (ptVideoDevice->pucVideBuf[i])
233 {
234 munmap(ptVideoDevice->pucVideBuf[i], ptVideoDevice->iVideoBufMaxLen);
235 ptVideoDevice->pucVideBuf[i] = NULL;
236 }
237 }
238
239 close(ptVideoDevice->iFd);
240 return 0;
241 }
3.11 对数据进行处理
这里因为使用的摄像头仅支持mjpeg格式,所以还需要对摄像头输出的数据帧进行处理。
这一部分代码参考:
//预览采集到的图像while (1){//如果把处理JPEG格式的数据和显示程序分离,把处理JPEG部分的数据作成一个新的线程,预览时会更加流畅。for (numBufs = 0; numBufs < req.count; numBufs++){ if ((fd_y_file = fopen(s, "wb")) < 0){printf("Unable to create y frame recording file\n");return -1;}memset(&buf, 0, sizeof(buf));buf.index = numBufs;buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; //取得原始采集数据buf.memory = V4L2_MEMORY_MMAP; //存储类型:V4L2_MEMORY_MMAP(内存映射)或V4L2_MEMORY_USERPTR(用户指针)if (ioctl(fd, VIDIOC_DQBUF, &buf) < 0) //从缓冲队列中取出数据{perror("VIDIOC_DQBUF failed.\n");return -1;}unsigned char *ptcur = buffers[numBufs].start; //开始霍夫曼解码int i1;//check huffman table,these code are optionalfor (i1=0; i1<buf.bytesused; i1++){if ((buffers[numBufs].start[i1] == 0x000000FF) && (buffers[numBufs].start[i1+1] == 0x000000C4)){break;}}if (i1 == buf.bytesused){printf("huffman table don't exist! \n");goto next_frame;}int i;//SOI = Start Of Image = "FFD8", EOI = End Of Image = "FFD9"for (i=0; i<buf.bytesused; i++){if ((buffers[numBufs].start[i] == 0x000000FF) && (buffers[numBufs].start[i+1] == 0x000000D8))break;ptcur++;}int imagesize = buf.bytesused - i;fwrite(ptcur, imagesize, 1, fd_y_file); //开始向LCD发送数据显示采集到的图像fclose(fd_y_file);if ((infile = fopen(s, "rb")) == NULL){fprintf(stderr, "open %s failed\n", s);exit(-1);}cinfo.err = jpeg_std_error(&jerr);jpeg_create_decompress(&cinfo);//导入要解压的Jpeg文件infilejpeg_stdio_src(&cinfo, infile);//读取jpeg文件的文件头jpeg_read_header(&cinfo, TRUE);//开始解压Jpeg文件,解压后将分配给scanline缓冲区,jpeg_start_decompress(&cinfo);buffer = (unsigned char *) malloc(cinfo.output_width * cinfo.output_components);y = 0;while (cinfo.output_scanline < cinfo.output_height){jpeg_read_scanlines(&cinfo, &buffer, 1);if (fbdev.fb_bpp == 16){unsigned short color;for (x = 0; x < cinfo.output_width; x++){color = RGB888toRGB565(buffer[x * 3],buffer[x * 3 + 1], buffer[x * 3 + 2]);fb_pixel(fbdev.fb_mem, fbdev.fb_width, fbdev.fb_height, x, y, color);///}}else if (fbdev.fb_bpp == 24||fbdev.fb_bpp == 32){memcpy((unsigned char *)fbdev.fb_mem + y * fbdev.fb_width * fbdev.fb_bpp / 8, buffer,cinfo.output_width * cinfo.output_components);}y++;//下一个scanline}//完成Jpeg解码,释放Jpeg文件jpeg_finish_decompress(&cinfo);jpeg_destroy_decompress(&cinfo);//释放帧缓冲区free(buffer);//关闭Jpeg输入文件fclose(infile);next_frame://将取出的图像放回缓冲区memset(&buf, 0 ,sizeof(buf));buf.index = numBufs;buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;buf.memory = V4L2_MEMORY_MMAP;if (ioctl(fd, VIDIOC_QBUF, &buf) < 0){printf("VIDIOC_QBUF error\n");return -1;}}//printf("start the next frame\n");if(stop_flag) break;}
4. 应用代码实现
由于今天改写还没完成,所以先将可以运行的代码贴上来,等后面重构成功后,贴上最终代码。
以下代码部分搬运自文件usb_camera.c
/*************************************NAME:usb_camera.c
COPYRIGHT:www.embedsky.net*************************************/#include <errno.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <asm/types.h>
#include <linux/videodev2.h>
#include <sys/mman.h>
#include <string.h>
#include <malloc.h>
#include <linux/fb.h>
#include <jpeglib.h>
#include <jerror.h>#include <signal.h>typedef struct VideoBuffer
{unsigned char *start;size_t offset;size_t length;
} VideoBuffer;struct fb_dev
{//for frame bufferint fb;void *fb_mem; //frame buffer mmapint fb_width, fb_height, fb_line_len, fb_size;int fb_bpp;
} fbdev;//得到framebuffer的长、宽和位宽,成功则返回0,失败返回-1
int fb_stat(int fd)
{struct fb_fix_screeninfo fb_finfo;struct fb_var_screeninfo fb_vinfo;if (ioctl(fd, FBIOGET_FSCREENINFO, &fb_finfo)){perror(__func__);return (-1);}if (ioctl(fd, FBIOGET_VSCREENINFO, &fb_vinfo)){perror(__func__);return (-1);}fbdev.fb_width = fb_vinfo.xres;fbdev.fb_height = fb_vinfo.yres;fbdev.fb_bpp = fb_vinfo.bits_per_pixel;fbdev.fb_line_len = fb_finfo.line_length;fbdev.fb_size = fb_finfo.smem_len;return (0);
}//转换RGB888为RGB565(因为当前LCD是采用的RGB565显示的)
unsigned short RGB888toRGB565(unsigned char red, unsigned char green, unsigned char blue)
{unsigned short B = (blue >> 3) & 0x001F;unsigned short G = ((green >> 2) << 5) & 0x07E0;unsigned short R = ((red >> 3) << 11) & 0xF800;return (unsigned short) (R | G | B);
}
//释放framebuffer的映射
int fb_munmap(void *start, size_t length)
{return (munmap(start, length));
}//显示一个像素点的图像到framebuffer上
int fb_pixel(void *fbmem, int width, int height, int x, int y, unsigned short color)
{if ((x > width) || (y > height))return (-1);unsigned short *dst = ((unsigned short *) fbmem + y * width + x);*dst = color;return 0;
}unsigned char stop_flag=0;
void stop_function()
{printf("usb camera stop\n");stop_flag = 1;
}int main(int argc, char** argv)
{int numBufs;int ret;char *devname;printf("USB Camera Test\n");signal(SIGINT,stop_function);//安装信号处理函数 devname = "/dev/video1";if(argc==2) devname = argv[1];int fd = open(devname, O_RDWR, 0); //打开摄像头设备,使用阻塞方式打开if (fd<0){printf("open error\n");return -1;}struct v4l2_format fmt; //设置获取视频的格式memset( &fmt, 0, sizeof(fmt));fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; //视频数据流类型,永远都是V4L2_BUF_TYPE_VIDEO_CAPTUREfmt.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV;//视频源的格式为JPEG或YUN4:2:2或RGBfmt.fmt.pix.width = 800; //设置视频宽度fmt.fmt.pix.height = 600; //设置视频高度//fmt.fmt.pix.field = V4L2_FIELD_ANY; if (ioctl(fd, VIDIOC_S_FMT, &fmt) < 0) //使配置生效{printf("set format failed\n");return -1;}struct v4l2_fmtdesc fmtdesc;fmtdesc.index = 0;fmtdesc.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;while(ioctl(fd,VIDIOC_ENUM_FMT,&fmtdesc) != -1){printf("enum fmt desc\n");printf("%d.%s\n",fmtdesc.index,fmtdesc.description);fmtdesc.index++;}//经查询,天敏minicam只支持MJPEG//设置camera的亮度值struct v4l2_queryctrl queryctrl;struct v4l2_control control;memset(&queryctrl, 0, sizeof(queryctrl));queryctrl.id = V4L2_CID_BRIGHTNESS;if(-1 == ioctl(fd, VIDIOC_QUERYCTRL, &queryctrl)){if(errno != EINVAL){perror("VIDIOC_QUERYCTRL\n");exit(-1);}else{printf("V4L2_CID_BRIGHTNESS is not support\n");}}else if(queryctrl.flags & V4L2_CTRL_FLAG_DISABLED){printf("V4L2_CID_BRIGHTNESS is not support\n");}else{memset(&control, 0, sizeof(control));control.id = V4L2_CID_BRIGHTNESS;control.value = queryctrl.default_value;//queryctrl.default_value;if(-1 == ioctl(fd, VIDIOC_S_CTRL, &control)){perror("VIDIOC_S_CTRL\n");exit(-1);}}struct v4l2_requestbuffers req; //申请帧缓冲memset(&req, 0, sizeof (req));req.count = 4; //缓存数量,即可保存的图片数量req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; //数据流类型,永远都是V4L2_BUF_TYPE_VIDEO_CAPTUREreq.memory = V4L2_MEMORY_MMAP; //存储类型:V4L2_MEMORY_MMAP或V4L2_MEMORY_USERPTRif (ioctl(fd, VIDIOC_REQBUFS, &req) == -1) //使配置生效{perror("request buffer error \n");return -1;}VideoBuffer *buffers = calloc(req.count, sizeof(VideoBuffer)); //将已申请到的缓冲帧映射到应用程序
// printf("sizeof(VideoBuffer) is %d\n", sizeof(VideoBuffer));
//sizeof(VideoBUffer)=12,因为start指针指向unsigned char类型数据,但是本身是4位长度struct v4l2_buffer buf;for (numBufs = 0; numBufs < req.count; numBufs++){memset( &buf, 0, sizeof(buf));buf.index = numBufs;buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; //数据流类型,永远都是V4L2_BUF_TYPE_VIDEO_CAPTUREbuf.memory = V4L2_MEMORY_MMAP; //存储类型:V4L2_MEMORY_MMAP(内存映射)或V4L2_MEMORY_USERPTR(用户指针) if (ioctl(fd, VIDIOC_QUERYBUF, &buf) < 0) //使配置生效{printf("VIDIOC_QUERYBUF error\n");return -1;}// printf("buf len is %d\n", sizeof(buf));buffers[numBufs].length = buf.length;buffers[numBufs].offset = (size_t) buf.m.offset;buffers[numBufs].start = mmap(NULL, buf.length, PROT_READ | PROT_WRITE,MAP_SHARED, fd, buf.m.offset);//使用mmap函数将申请的缓存地址转换应用程序的绝对地址
#if 0printf("buffers.length = %d,buffers.offset = %d ,buffers.start[0] = %d\n",buffers[numBufs].length, buffers[numBufs].offset,buffers[numBufs].start[0]);printf("buf2 len is %d\n", sizeof(buffers[numBufs].start));
#endifif (buffers[numBufs].start == MAP_FAILED){perror("buffers error\n");return -1;}if (ioctl(fd, VIDIOC_QBUF, &buf) < 0) //放入缓存队列{printf("VIDIOC_QBUF error\n");return -1;}}enum v4l2_buf_type type; //开始视频显示type = V4L2_BUF_TYPE_VIDEO_CAPTURE; //数据流类型,永远都是V4L2_BUF_TYPE_VIDEO_CAPTUREif (ioctl(fd, VIDIOC_STREAMON, &type) < 0){printf("VIDIOC_STREAMON error\n");return -1;}fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; //数据流类型,永远都是V4L2_BUF_TYPE_VIDEO_CAPTUREif (ioctl(fd, VIDIOC_G_FMT, &fmt) < 0) //读取视频源格式{printf("get format failed\n");return -1;}else{printf("Picture:Width = %d Height = %d\n", fmt.fmt.pix.width, fmt.fmt.pix.height);printf("Image size = %d\n", fmt.fmt.pix.sizeimage);printf("pixelformat = %d\n", fmt.fmt.pix.pixelformat);}FILE * fd_y_file = 0;int a=0;int k = 0;//设置显卡设备framebufferstruct jpeg_decompress_struct cinfo;struct jpeg_error_mgr jerr;FILE *infile; //Jpeg文件的句柄unsigned char *buffer;int fb;char *fb_device;unsigned int x;unsigned int y;char s[15];sprintf(s, "%d.jpg", a);if ((fb = open("/dev/fb0", O_RDWR)) < 0) //打开显卡设备{perror(__func__);return (-1);}//获取framebuffer的状态fb_stat(fb); //获取显卡驱动中的长、宽和显示位宽printf("frame buffer: %dx%d, %dbpp, 0x%xbyte= %d\n", fbdev.fb_width, fbdev.fb_height, fbdev.fb_bpp, fbdev.fb_size, fbdev.fb_size);//映射framebuffer的地址fbdev.fb_mem = mmap (NULL, fbdev.fb_size, PROT_READ|PROT_WRITE,MAP_SHARED,fb,0);//映射显存地址fbdev.fb = fb;memset(fbdev.fb_mem ,0,fbdev.fb_size);//预览采集到的图像while (1){//如果把处理JPEG格式的数据和显示程序分离,把处理JPEG部分的数据作成一个新的线程,预览时会更加流畅。for (numBufs = 0; numBufs < req.count; numBufs++){ if ((fd_y_file = fopen(s, "wb")) < 0){printf("Unable to create y frame recording file\n");return -1;}memset(&buf, 0, sizeof(buf));buf.index = numBufs;buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; //取得原始采集数据buf.memory = V4L2_MEMORY_MMAP; //存储类型:V4L2_MEMORY_MMAP(内存映射)或V4L2_MEMORY_USERPTR(用户指针)if (ioctl(fd, VIDIOC_DQBUF, &buf) < 0) //从缓冲队列中取出数据{perror("VIDIOC_DQBUF failed.\n");return -1;}unsigned char *ptcur = buffers[numBufs].start; //开始霍夫曼解码int i1;//check huffman table,these code are optionalfor (i1=0; i1<buf.bytesused; i1++){if ((buffers[numBufs].start[i1] == 0x000000FF) && (buffers[numBufs].start[i1+1] == 0x000000C4)){break;}}if (i1 == buf.bytesused){printf("huffman table don't exist! \n");goto next_frame;}int i;//SOI = Start Of Image = "FFD8", EOI = End Of Image = "FFD9"for (i=0; i<buf.bytesused; i++){if ((buffers[numBufs].start[i] == 0x000000FF) && (buffers[numBufs].start[i+1] == 0x000000D8))break;ptcur++;}int imagesize = buf.bytesused - i;fwrite(ptcur, imagesize, 1, fd_y_file); //开始向LCD发送数据显示采集到的图像fclose(fd_y_file);if ((infile = fopen(s, "rb")) == NULL){fprintf(stderr, "open %s failed\n", s);exit(-1);}cinfo.err = jpeg_std_error(&jerr);jpeg_create_decompress(&cinfo);//导入要解压的Jpeg文件infilejpeg_stdio_src(&cinfo, infile);//读取jpeg文件的文件头jpeg_read_header(&cinfo, TRUE);//开始解压Jpeg文件,解压后将分配给scanline缓冲区,jpeg_start_decompress(&cinfo);buffer = (unsigned char *) malloc(cinfo.output_width * cinfo.output_components);y = 0;while (cinfo.output_scanline < cinfo.output_height){jpeg_read_scanlines(&cinfo, &buffer, 1);if (fbdev.fb_bpp == 16){unsigned short color;for (x = 0; x < cinfo.output_width; x++){color = RGB888toRGB565(buffer[x * 3],buffer[x * 3 + 1], buffer[x * 3 + 2]);fb_pixel(fbdev.fb_mem, fbdev.fb_width, fbdev.fb_height, x, y, color);///}}else if (fbdev.fb_bpp == 24||fbdev.fb_bpp == 32){memcpy((unsigned char *)fbdev.fb_mem + y * fbdev.fb_width * fbdev.fb_bpp / 8, buffer,cinfo.output_width * cinfo.output_components);}y++;//下一个scanline}//完成Jpeg解码,释放Jpeg文件jpeg_finish_decompress(&cinfo);jpeg_destroy_decompress(&cinfo);//释放帧缓冲区free(buffer);//关闭Jpeg输入文件fclose(infile);next_frame://将取出的图像放回缓冲区memset(&buf, 0 ,sizeof(buf));buf.index = numBufs;buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;buf.memory = V4L2_MEMORY_MMAP;if (ioctl(fd, VIDIOC_QBUF, &buf) < 0){printf("VIDIOC_QBUF error\n");return -1;}}//printf("start the next frame\n");if(stop_flag) break;}printf("go out the while loop\n");//停止视频采集type = V4L2_BUF_TYPE_VIDEO_CAPTURE;if(-1 == ioctl(fd, VIDIOC_STREAMOFF, &type)){perror("VIDIOC_STREAMOFF\n");exit(-1);} for (numBufs = 0; numBufs < req.count; numBufs++){munmap(buffers[numBufs].start, buffers[numBufs].length);}free(buffers);fb_munmap(fbdev.fb_mem, fbdev.fb_size); //释放framebuffer映射close(fb); //关闭Framebuffer设备close(fd);printf("finish the program\n");return 0;
}