Linux嵌入式相机 — 项目总结
main函数执行流程
-
1、初始化触摸屏
Touch_screen_Init();
struct tsdev *ts = NULL; ts = ts_setup(NULL, 0); //以阻塞打开
-
2、初始化 LCD
LCD_Init(void);
通过 ioctl 函数获取 LCD 的固定参数、可变参数,得到分辨率、bpp、一行的长度(以字节为单位),将显存映射到内存上,得到 framebuffer 的首地址,使用 memset 函数将这块区域全部设置为 1,即将LCD设置为白色背景
struct fb_var_screeninfo var; /* Current var */ struct fb_fix_screeninfo fix; /* Current fix */ ioctl(fd_fb, FBIOGET_VSCREENINFO, &var);//获取屏幕可变信息 ioctl(fd_fb, FBIOGET_FSCREENINFO, &fix);//获取屏幕固定信息 ... unsigned char* fbbase; fbbase = mmap(NULL, screen_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd_fb, 0); // 映射 screen_size:整块显存的大小 memset(fbbase, 0xFF, screem_size);
-
3、打开摄像头
int fd = open("/dev/video1", O_RDWR);
-
4、将背景图片1(带有拍照、相册按钮)显示在 LCD 上
LCD_Show_JPEG(background1);
这个图片的分辨率是 1024 * 600(板子的屏幕的分辨率也是 1024 * 600)
-
5、将图片放入相册
将指定目录(/home/)中已有的图片(.jpg格式)加入双向链表中,image_count 为目前图片名称最大索引
image_count = xiangce_Init();
-
6、遍历链表
jpeg_list_printf(void)
遍历双向链表(不包含虚拟头节点),打印所有照片的名字
-
7、设置摄像头的采集格式
struct v4l2_format vfmt; vfmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; //选择视频抓取 // 一定是 800 * 600(600等于LCD的高度,8800于LCD的宽度是为了显示相机的按钮) vfmt.fmt.pix.width = 800;//设置宽 vfmt.fmt.pix.height = 600;//设置高 vfmt.fmt.pix.pixelformat = V4L2_PIX_FMT_MJPEG;//设置视频采集像素格式 int ret = ioctl(fd, VIDIOC_S_FMT, &vfmt);// VIDIOC_S_FMT:设置捕获格式 if(ret < 0) { perror("设置采集格式错误"); } // 判断是否设置成功 memset(&vfmt, 0, sizeof(vfmt)); vfmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; ret = ioctl(fd, VIDIOC_G_FMT, &vfmt); if(ret < 0) { perror("读取采集格式失败"); } printf("设置分辨率width = %d\n", vfmt.fmt.pix.width); printf("设置分辨率height = %d\n", vfmt.fmt.pix.height); unsigned char *p = (unsigned char*)&vfmt.fmt.pix.pixelformat; printf("pixelformat = %c%c%c%c\n", p[0],p[1],p[2],p[3]);
-
8、申请缓冲队列
struct v4l2_requestbuffers reqbuffer; reqbuffer.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; reqbuffer.count = 4; //申请4个缓冲区 reqbuffer.memory = V4L2_MEMORY_MMAP; //采用内存映射的方式 ret = ioctl(fd, VIDIOC_REQBUFS, &reqbuffer); if(ret < 0) { perror("申请缓冲队列失败"); }
-
9、映射
struct v4l2_buffer mapbuffer; unsigned char *mmpaddr[4]; //用于存储映射后的首地址 unsigned int addr_length[4];//存储映射后空间的大小 mapbuffer.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;//初始化type for(int i = 0; i < 4; i++) { mapbuffer.index = i; ret = ioctl(fd, VIDIOC_QUERYBUF, &mapbuffer); //查询缓存信息 if(ret < 0) perror("查询缓存队列失败"); mmpaddr[i] = (unsigned char *)mmap(NULL, mapbuffer.length, PROT_READ|PROT_WRITE, MAP_SHARED, fd, mapbuffer.m.offset);//mapbuffer.m.offset映射文件的偏移量 addr_length[i] = mapbuffer.length; //放入队列 ret = ioctl(fd, VIDIOC_QBUF, &mapbuffer); // 将用户空间的视频缓冲区送入内核中 if(ret < 0) perror("放入队列失败"); }
-
10、打开设备(启动视频流传输)
int type = V4L2_BUF_TYPE_VIDEO_CAPTURE; ret = ioctl(fd, VIDIOC_STREAMON, &type); if(ret < 0) { perror("打开设备失败"); }
-
11、创建线程读取触摸屏输入
pthread_t pthread_read; pthread_create(&pthread_read, NULL, start_read, NULL);
线程函数:
如果点击触摸屏,并且触摸点在 “ 拍照 ” “ 相册 ” 按钮范围内,则会记录触摸点的坐标 read_x read_y(全局变量)
void *start_read(void *arg) { int x = 0, y =0; while(start_read_flag) // 初始化为1,当整个程序要退出时,设为0,退出线程 { printf("线程\n"); read_touchscreen(&x, &y); // 阻塞读取触点坐标 //拍照 if(x > 800 && x < 1000 && y > 0 && y < 600) { printf("chuli\n"); pthread_mutex_lock(&mutex); read_x = x; read_y = y; pthread_mutex_unlock(&mutex); } printf("readx = %d, ready = %d", read_x, read_y); } return NULL; }
read_touchscreen 函数:
ts_read 的默认读取是以阻塞的方式
int read_touchscreen(int *x, int *y) { struct ts_sample samp; // ts_setup 以阻塞的方式打开了触摸屏设备,所以 ts_read 是阻塞读取 if (ts_read(ts, &samp, 1) < 0) // 1:代表对一个触摸点的采集数 { perror("ts_read error"); ts_close(ts); return -1; } *x = samp.x; *y = samp.y; printf("anxia : %d %d", samp.x, samp.y); return 0; }
-
12、进入 while 循环,提取摄像头数据,并在LCD上显示同时实现拍照、相册功能
while(1) { //从队列中提取一帧数据 struct v4l2_buffer readbuffer; readbuffer.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; ret = ioctl(fd, VIDIOC_DQBUF, &readbuffer); //从缓冲队列获取一帧数据(出队列) //出队列后得到缓存的索引index,得到对应缓存映射的地址mmpaddr[readbuffer.index] if(ret < 0) perror("获取数据失败"); if(read_x > 850 && read_x < 1000 && read_y > 0 && read_y < 600) { if(read_x > 850 && read_x < 1000 && read_y > 130 && read_y < 210) { printf("paizhao\n"); char newname[20] = {0}; image_count++; sprintf(newname,"/home/%d.jpg", image_count); FILE *file = fopen(newname, "w+");//建立文件用于保存一帧数据 if(NULL == file) printf("拍照失败"); int res_w = fwrite(mmpaddr[readbuffer.index], readbuffer.length, 1, file); fclose(file); jpeg_list_insert(image_list, newname); // 将图片加入双向链表中 printf("new_image:%s %d\n", newname, image_count); sleep(1); } else if(read_x > 850 && read_x < 1000 && read_y > 390 && read_y < 470) { start_xiangce(); } pthread_mutex_lock(&mutex); read_x = 0; read_y = 0; pthread_mutex_unlock(&mutex); } //打开相册 //显示在LCD上 LCD_JPEG_Show(mmpaddr[readbuffer.index], readbuffer.length); //读取数据后将缓冲区放入队列 ret = ioctl(fd, VIDIOC_QBUF, &readbuffer); if(ret < 0) perror("放入队列失败"); }
-
13、程序终止前的收尾工作
//关闭设备(停止视频流传输) ret = ioctl(fd, VIDIOC_STREAMOFF, &type); if(ret < 0) perror("关闭设备失败"); //取消映射 for(int i = 0; i < 4; i++) munmap(mmpaddr[i], addr_length[i]); start_read_flag = 0; //结束触摸屏监听线程 ts_close(ts); close(fd); close(fd_fb); return 0;
封装的一些大型函数
1、在 LCD 上显示 jpeg 图片
指定 JPEG 文件显示在LCD上,传入 jpeg 文件路径
int LCD_Show_JPEG(const char *jpeg_path)
//指定JPEG文件显示在LCD上,传入jpeg文件路径
int LCD_Show_JPEG(const char *jpeg_path)
{
FILE *jpeg_file = NULL;
int min_hight = LCD_height, min_width = LCD_width, valid_bytes;
struct jpeg_decompress_struct cinfo;
struct jpeg_error_mgr jerr;
//1. 错误处理对象与解码对象绑定
cinfo.err = jpeg_std_error(&jerr);
//2. 打开.jpeg图像文件
jpeg_file = fopen(jpeg_path, "r"); //只读方式打开
//3. 创建解码对象
jpeg_create_decompress(&cinfo);
//4. 指定解码数据源
jpeg_stdio_src(&cinfo, jpeg_file);
//5. 读取图像信息
jpeg_read_header(&cinfo, TRUE);
//6. 设置解码参数
cinfo.out_color_space = JCS_RGB; //可以不设置默认为RGB
//cinfo.scale_num = 1;
//cinfo.scale_denom = 1;设置图像缩放,scale_num/scale_denom缩放比例,默认为1
//7. 开始解码
jpeg_start_decompress(&cinfo);
//8. 为缓冲区分配空间
// cinfo.output_components:每个像素所占的字节数 cinfo.output_width:每一行的像素个数
// 保存解压出来的一行数据
unsigned char* jpeg_line_buf = malloc(cinfo.output_components * cinfo.output_width); // 图片每一行像素所占的字节数
// 将解压出来的数据 BGR 转换成 RGB 进行保存
unsigned int* fb_line_buf = malloc(line_length); // LCD一行的宽度(以字节为单位) 每个成员4个字节刚好和RGB888对应
//判断图像和LCD屏那个分辨率更低
if(cinfo.output_width < min_width)
min_width = cinfo.output_width; // 图片的宽度和LCD宽度之间取最小的
if(cinfo.output_height < min_hight)
min_hight = cinfo.output_height; // 图片的高度和LCD高度之间取最小的
//9. 读取数据,数据按行读取
valid_bytes = min_width * bpp / 8; // 一行的有效字节数,实际写进LCD显存的一行数据大小
unsigned char *ptr = fbbase; // framebuffer的起始地址
while(cinfo.output_scanline < min_hight) // cinfo.output_scanline 当前已经处理的行的数量
{
jpeg_read_scanlines(&cinfo, &jpeg_line_buf, 1); // 每次读取一行(对 jpeg 图片的解码是一行一行进行的)
//将读取到的BGR888数据转化为RGB888
unsigned int red, green, blue;
unsigned int color;
for(int i = 0; i < min_width; i++) // 当图片的宽度大于LCD宽度时,在LCD上只会显示LCD宽度的图片
{
red = jpeg_line_buf[i*3];
green = jpeg_line_buf[i*3+1];
blue = jpeg_line_buf[i*3+2];
color = red<<16 | green << 8 | blue; // 将 BGR 数据转换为 RGB 数据
fb_line_buf[i] = color;
}
memcpy(ptr, fb_line_buf, valid_bytes);
ptr += LCD_width*bpp/8;
}
//完成解码
jpeg_finish_decompress(&cinfo);
//销毁解码对象
jpeg_destroy_decompress(&cinfo);
//释放内存
free(jpeg_line_buf);
free(fb_line_buf);
return 1;
}
关键点:
-
解码流程:
- 1、错误处理对象与解码对象的绑定
- 2、打开 .jpeg 图片文件(获得文件句柄)
- 3、创建解码对象
- 4、指定解码数据源(将解码对象与文件句柄进行绑定)
- 5、读取图像信息
- 6、设置解码参数(缩放比、获取像素信息的BPP格式,默认为 BGR 格式)
- 7、开始解码
- 8、为缓冲区分配空间(一行像素数据的大小)( cinfo.output_components * cinfo.output_width )
- 9、读取数据,数据按行进行读取
-
读取数据的行数:
首先在 LCD y 分辨率和像素的宽度之间取得最小值 min_hight
while( cinfo.output_scanline < min_hight ) cinfo.output_scanline :当前已经处理行的数量
-
BGR 到 RGB 数据的转化:
RBG 像素数据,一个像素占 4 个字节
unsigned int* fb_line_buf = malloc ( line_length ); // 也是分配一行(像素个数为 LCD 的 x 分辨率)
在读取到一行的像素数据后进行转化,但是转化的像素个数为 LCD x 分辨率 与 图像宽度之间的最小值 min_width
for ( int i = 0; i < min_width; i++ )
-
将 RGB 数据写入 framebuffer 中:
valid_bytes = min_width * bpp / 8;
memcpy ( ptr, fb_line_buf, valid_bytes ); (每转化一行,就将这一行的像素数据写入framebuffer中)
2、对图片链表进行操作的一系列函数
为了实现相册中图片的切换功能,将图片统一放置在一个指定目录下,定义一个双向链表,每一项对应一张图片
struct jpeg_node{
char name[30]; //图像名
struct jpeg_node *next; //下一张
struct jpeg_node *pre; //上一张
};
1、初始化链表
定义一个虚拟头节点(方便节点的插入,其它并没有什么作用)
struct jpeg_node *jpeg_list_Init(void)
{
struct jpeg_node* jpeg_head = malloc(sizeof(struct jpeg_node));
strcpy(jpeg_head->name, background2);
// 将背景图片2作为链表的头节点(但是这个头节点相当与是虚拟头节点)
jpeg_head ->pre = jpeg_head;
jpeg_head ->next = jpeg_head;
return jpeg_head;
}
2、插入一个新节点(头插法)
void jpeg_list_insert(struct jpeg_node *jpeg_list, char *name)
{
struct jpeg_node *newnode = malloc(sizeof(struct jpeg_node));
strcpy(newnode->name, name);
struct jpeg_node *p = jpeg_list->next;
//头插法
jpeg_list->next = newnode;
newnode->pre = jpeg_list;
newnode->next = p;
p->pre = newnode;
printf("放入链表成功\n");
p = NULL;
}
3、遍历整个链表(打印所有图片的名字,除了虚拟头节点)
void jpeg_list_printf(void)
{
struct jpeg_node* pnext = image_list->next;
while(pnext != image_list)
{
printf("%s\n", pnext->name);
pnext = pnext->next;
}
pnext = NULL;
}
3、将已有照片放入相册
返回目前照片名称最大索引
int xiangce_Init(void)
int xiangce_Init(void)
{
image_list = jpeg_list_Init(); // 初始化链表
DIR *dp = opendir("/home/"); // 打开home目录
struct dirent *pdir;
char *temp = NULL;
char name[15];
int total = 0;
//遍历目录, 当遍历结束时返回NULL
while(pdir = readdir(dp))
{
if(pdir->d_type == DT_REG) //判断是否为普通文件
{
if(strstr(pdir->d_name, ".jpg")) //判断是否为jpg文件
{
char newname[20] = {0};
sprintf(newname,"/home/%s", pdir->d_name); // 获取文件的绝对路径作为文件名
jpeg_list_insert(image_list, newname); // 将该文件名称插入链表中
bzero(name,15); // 将 name 数组中的前 15 个字符全部清为 0
strcpy(name, pdir->d_name);
temp = strtok(name, ".");
total = atoi(temp) > total ? atoi(temp) : total; // atoi:将字符串转转换为整数 得到名称索引最大的那个
}
}
}
temp = NULL;
return total;
}
关键点:
-
名称索引:
指定目录中的图片名称均为:1.jpg 2.jpg 3.jpg …
获取当前已有照片的最大名称索引,下一次记录照片时,照片名为最大名称索引 + 1
-
目录文件的遍历:
opendir 函数用于打开指定的目录
readdir 函数获取目录中的文件(在 while 循环中依次获取目录中的文件直至为 NULL)
readdir 获取到的文件结构体中有属性表明了文件的类型、文件名等等
-
strstr
char *strstr(const char *haystack, const char *needle)
该函数返回在 haystack 中第一次出现 needle 字符串的位置,如果未找到则返回 null
-
bzero
void bzero(void *s, int n);
将内存块的前 n 个字节清零
-
strtok
char *strtok(char *str, const char *delim)
该函数返回被分解的第一个子字符串,如果没有可检索的字符串,则返回一个空指针
示例:
#include <string.h> #include <stdio.h> int main () { char str[80] = "This is - www.runoob.com - website"; const char s[2] = "-"; char *token; /* 获取第一个子字符串 */ token = strtok(str, s); /* 继续获取其他的子字符串 */ while( token != NULL ) { printf( "%s\n", token ); token = strtok(NULL, s); } return(0); }
结果:
This is www.runoob.com website
-
atoi
int atoi(const char *str)
将字符串转换为整形数据
4、打开相册
start_xiangce();
void start_xiangce(void)
{
struct jpeg_node *curr_image = image_list; //->next;//指向第一张图片
LCD_Show_JPEG(background2);
LCD_Show_JPEG(curr_image->name); // 虚拟头节点的内容设置为背景图片2的名字(绝对路径)
int pre_x = 0, pre_y = 0;
while(1)
{
if(pre_x != read_x && pre_y != read_y)
{
if(read_x>850 && read_x<1000 && read_y>260 &&read_y<340) //下一张
{
printf("下一张\n");
curr_image = curr_image->next;
printf("current image name :%s\n", curr_image->name);
}
if(read_x>850 && read_x<1000 && read_y>0 && read_y<80) //上一张
{
curr_image = curr_image->pre;
printf("上一张\n");
printf("current image name :%s\n", curr_image->name);
}
}
pre_x = read_x;
pre_y = read_y;
if(curr_image == image_list) // 去除背景图片2的循环
curr_image = image_list->next;
LCD_Show_JPEG(curr_image->name);
if(read_x>850 && read_x<1000 && read_y>520 && read_y<600) //返回
{
LCD_Show_JPEG(background1);
printf("返回\n");
break;
}
}
}
V4L2框架
1、打开设备
当摄像头设备插入电脑后:
int fd = open("/dev/video1",O_RDWR);
2、获得所支持的格式
使用 ioctl ( 文件描述符,命令,与命令对应的结构体 ) 来获取摄像头的格式
-
命令
-
关于 v4l2_fmtdesc 结构体
命令使用:VIDIOC_ENUM_FMT(获取当前驱动支持的视频格式)
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]; };
该结构体定义如上,因为v4l2不单单针对摄像头,所以使用前需要对type成员进行初始化,v4l2_buf_type 这个枚举总共有13个成员,这里选择1,即视频抓取
在用代码读取过程中,因为支持多种格式,所以用while循环读取支持的格式
-
关于 v4l2_capability 结构体
命令使用:VIDIOC_QUERYCAP(检查当前视频设备支持的标准)
struct v4l2_capability { u8 driver[16]; // 驱动名字 u8 card[32]; // 设备名字 u8 bus_info[32]; // 设备在系统中的位置 u32 version;// 驱动版本号 u32 capabilities;// 设备支持的操作 u32 reserved[4]; // 保留字段 };
除了使用 v4l2_fmtdesc 结构体获取像素格式,还可以通过 v4l2_capability 结构体来获取设备的功能,主要看capabilities成员,其是否支持视频捕捉(V4L2_CAP_VIDEO_CAPTURE)、以及是否支持流读写(V4L2_CAP_STREAMING)
使用示例:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <linux/videodev2.h>
int main(int argc, char**argv)
{
if(argc != 2)
{
printf("%s </dev/video0,1...>\n", argv[0]);
return -1;
}
//打开摄像头设备
int fd = open(argv[1], O_RDWR);
if(fd < 0)
{
perror("打开设备失败");
return -1;
}
//获取摄像头支持格式,使用ioctl函数int ioctl(int fd, unsigned long request, ...);
struct v4l2_fmtdesc v4fmt;
struct v4l2_capability cap;
v4fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; //选择视频抓取
int i = 0;
while(1)
{
v4fmt.index = i;
i++;
int ret = ioctl(fd, VIDIOC_ENUM_FMT, &v4fmt);
if(ret < 0)
{
perror("获取格式失败");
break;
}
printf("index = %d\n", v4fmt.index);
printf("flags = %d\n", v4fmt.flags);
printf("descrrption = %s\n", v4fmt.description);
unsigned char *p = (unsigned char*)&v4fmt.pixelformat;
printf("pixelformat = %c%c%c%c\n", p[0],p[1],p[2],p[3]);
printf("reserved = %d\n", v4fmt.reserved[0]);
}
int ret = ioctl(fd, VIDIOC_QUERYCAP, &cap);
if(ret < 0)
perror("获取功能失败");
printf("drivers:%s\n", cap.driver);//读取驱动名字
if(cap.capabilities & V4L2_CAP_VIDEO_CAPTURE)
printf("%s 支持视频捕获\n", argv[1]);
if(cap.capabilities & V4L2_CAP_STREAMING)
printf("%s 支持流读写\n", argv[1]);
close(fd);
return 0;
}
结果:
3、配置摄像头
设置视频的采集格式,定义 v4l2_format 结构体变量,然后通过结构体 v4l2_pix_format 来设置采集的高、宽以及像素格式(YUYV),设置之后,可以采用打印的方式来查看是否设置成功
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 */
__u8 raw_data[200]; /* user-defined */
} fmt;
};
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_*) */
__u32 ycbcr_enc; /* enum v4l2_ycbcr_encoding */
__u32 quantization; /* enum v4l2_quantization */
__u32 xfer_func; /* enum v4l2_xfer_func */
};
代码示例:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <linux/videodev2.h>
#include <string.h>
int main(void)
{
int fd = open("/dev/video0",O_RDWR); //根据自己的摄像头设备节点打开
if (fd < 0)
{
perror("打开设备失败");
return -1;
}
//设置摄像头 ioctl(文件描述符,命令,与命令对应的结构体)
struct v4l2_format vfmt;
vfmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; //摄像头采集
vfmt.fmt.pix.width = 640; //设置摄像头采集参数,不可以任意设置
vfmt.fmt.pix.height = 480;
vfmt.fmt.pix.pixelformat = V4L2_PIX_FMT_YYUV; //设置视频采集格式 ,根据上一步测得,注意格式有yyuv和yuyv不要搞混
int ret = ioctl(fd,VIDIOC_S_FMT,&vfmt); // 设置格式命令 VIDIOC_S_FMT:设置当前驱动的频捕捉格式
if (ret < 0)
{
perror("设置格式失败1");
}
memset(&vfmt,0,sizeof(vfmt));
vfmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
ret = ioctl(fd,VIDIOC_G_FMTL:du'qu,&vfmt); // VIDIOC_G_FMTL:读取当前驱动的频捕捉格式
if (ret < 0)
{
perror("设置格式失败2");
}
if(vfmt.fmt.pix.width == 640 && vfmt.fmt.pix.height == 480 && vfmt.fmt.pix.pixelformat == V4L2_PIX_FMT_YUYV)
{
printf("设置成功!");
}else
{
printf("设置失败3");
}
close(fd);
return 0;
}
结果:
4、向内核申请帧缓冲队列并映射
V4L2读取数据时有两种方式,第一种是用read读取(调用read函数),第二种是用流(streaming)读取,在第二步上已经获取到我的设备支持流读写,为了提高效率采用流读写,流读写就是在内核中维护一个缓存队列,然后再映射到用户空间,应用层直接读取队列中的数据
步骤为:申请缓冲区->逐个查询申请到的缓冲区->逐个映射->逐个放入队列中
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <linux/videodev2.h>
#include <string.h>
#include <sys/mman.h>
int main(int argc, char**argv)
{
if(argc != 2)
{
printf("%s </dev/video0,1...>\n", argv[0]);
return -1;
}
//打开摄像头设备
int fd = open(argv[1], O_RDWR);
if(fd < 0)
{
perror("打开设备失败");
return -1;
}
//设置摄像头采集格式
struct v4l2_format vfmt;
vfmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; //选择视频抓取
vfmt.fmt.pix.width = 640; //设置宽,不能随意设置
vfmt.fmt.pix.height = 480; //设置高
vfmt.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV; //设置视频采集格式
int ret = ioctl(fd, VIDIOC_S_FMT, &vfmt); // VIDIOC_S_FMT:设置捕获格式
if(ret < 0)
{
perror("设置采集格式错误");
}
memset(&vfmt, 0, sizeof(vfmt));
vfmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
ret = ioctl(fd, VIDIOC_G_FMT, &vfmt);
if(ret < 0)
{
perror("读取采集格式失败");
}
printf("width = %d\n", vfmt.fmt.pix.width);
printf("width = %d\n", vfmt.fmt.pix.height);
unsigned char *p = (unsigned char*)&vfmt.fmt.pix.pixelformat;
printf("pixelformat = %c%c%c%c\n", p[0],p[1],p[2],p[3]);
//申请缓冲队列
struct v4l2_requestbuffers reqbuffer;
reqbuffer.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
reqbuffer.count = 4; //申请4个缓冲区
reqbuffer.memory = V4L2_MEMORY_MMAP; //采用内存映射的方式
ret = ioctl(fd, VIDIOC_REQBUFS, &reqbuffer); // VIDIOC_REQBUFS: 申请缓冲区队列
if(ret < 0)
{
perror("申请缓冲队列失败");
}
//映射,映射之前需要查询缓存信息->每个缓冲区逐个映射->将缓冲区放入队列
struct v4l2_buffer mapbuffer;
unsigned char *mmpaddr[4]; //用于存储映射后的首地址
mapbuffer.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; //初始化type
for(int i = 0; i < 4; i++)
{
mapbuffer.index = i;
ret = ioctl(fd, VIDIOC_QUERYBUF, &mapbuffer); //查询缓存信息 VIDIOC_QUERYBUF:将数据缓存转换为物理地址
if(ret < 0)
perror("查询缓存队列失败");
mmpaddr[i] = (unsigned char *)mmap(NULL, mapbuffer.length, PROT_READ|PROT_WRITE, MAP_SHARED, fd, mapbuffer.m.offset); //mapbuffer.m.offset映射文件的偏移量
//放入队列
ret = ioctl(fd, VIDIOC_QBUF, &mapbuffer); // 把数据从缓存中读取出来
if(ret < 0)
perror("放入队列失败");
}
close(fd);
return 0;
}
5、采集视频
做完前面的设置就可以进行采集数据,打开设备->读取数据->关闭设备->释放映射。
读取数据的本质就是从上一个步骤中映射的队列中取出数据,取出数据后需要将该缓冲区放入队列中,以便于再去采集下一帧数据。
为了便于查看,在设置采集格式时,选择MJPEG格式,用fopen函数建立一个my.jpg文件,用fwrite函数保存读到的一帧数据。
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <linux/videodev2.h>
#include <string.h>
#include <sys/mman.h>
int main(int argc, char**argv)
{
if(argc != 2)
{
printf("%s </dev/video0,1...>\n", argv[0]);
return -1;
}
//1.打开摄像头设备
int fd = open(argv[1], O_RDWR);
if(fd < 0)
{
perror("打开设备失败");
return -1;
}
//2.设置摄像头采集格式
struct v4l2_format vfmt;
vfmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; //选择视频抓取
vfmt.fmt.pix.width = 640;//设置宽,不能随意设置
vfmt.fmt.pix.height = 480;//设置高
vfmt.fmt.pix.pixelformat = V4L2_PIX_FMT_MJPEG;//设置视频采集像素格式
int ret = ioctl(fd, VIDIOC_S_FMT, &vfmt);// VIDIOC_S_FMT:设置捕获格式
if(ret < 0)
{
perror("设置采集格式错误");
}
memset(&vfmt, 0, sizeof(vfmt));
vfmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
ret = ioctl(fd, VIDIOC_G_FMT, &vfmt);
if(ret < 0)
{
perror("读取采集格式失败");
}
printf("width = %d\n", vfmt.fmt.pix.width);
printf("width = %d\n", vfmt.fmt.pix.height);
unsigned char *p = (unsigned char*)&vfmt.fmt.pix.pixelformat;
printf("pixelformat = %c%c%c%c\n", p[0],p[1],p[2],p[3]);
//4.申请缓冲队列
struct v4l2_requestbuffers reqbuffer;
reqbuffer.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
reqbuffer.count = 4; //申请4个缓冲区
reqbuffer.memory = V4L2_MEMORY_MMAP; //采用内存映射的方式
ret = ioctl(fd, VIDIOC_REQBUFS, &reqbuffer); // VIDIOC_REQBUFS: 申请缓冲区队列
if(ret < 0)
{
perror("申请缓冲队列失败");
}
//映射,映射之前需要查询缓存信息->每个缓冲区逐个映射->将缓冲区放入队列
struct v4l2_buffer mapbuffer;
unsigned char *mmpaddr[4]; //用于存储映射后的首地址
unsigned int addr_length[4]; //存储映射后空间的大小
mapbuffer.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; //初始化type 视频捕捉
for(int i = 0; i < 4; i++)
{
mapbuffer.index = i;
ret = ioctl(fd, VIDIOC_QUERYBUF, &mapbuffer); //查询缓存信息
if(ret < 0)
perror("查询缓存队列失败");
mmpaddr[i] = (unsigned char *)mmap(NULL, mapbuffer.length, PROT_READ|PROT_WRITE, MAP_SHARED, fd, mapbuffer.m.offset);//mapbuffer.m.offset映射文件的偏移量
addr_length[i] = mapbuffer.length;
//放入队列
ret = ioctl(fd, VIDIOC_QBUF, &mapbuffer);
if(ret < 0)
perror("放入队列失败");
}
//打开设备
int type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
ret = ioctl(fd, VIDIOC_STREAMON, &type);
if(ret < 0)
perror("打开设备失败");
//从队列中提取一帧数据
struct v4l2_buffer readbuffer;
readbuffer.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
ret = ioctl(fd, VIDIOC_DQBUF, &readbuffer); //从缓冲队列获取一帧数据(出队列)
//出队列后得到缓存的索引index,得到对应缓存映射的地址mmpaddr[readbuffer.index]
if(ret < 0)
perror("获取数据失败");
FILE *file = fopen("my.jpg", "w+");//建立文件用于保存一帧数据
fwrite(mmpaddr[readbuffer.index], readbuffer.length, 1, file);
fclose(file);
//读取数据后将缓冲区放入队列
ret = ioctl(fd, VIDIOC_QBUF, &readbuffer);
if(ret < 0)
perror("放入队列失败");
//关闭设备
ret = ioctl(fd, VIDIOC_STREAMOFF, &type);
if(ret < 0)
perror("关闭设备失败");
//取消映射
for(int i = 0; i < 4; i++)
munmap(mmpaddr[i], addr_length[i]);
close(fd);
return 0;
}