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;
	
}
相关推荐
lldhsds3 分钟前
书生大模型实战营第四期-入门岛-1. Linux前置基础
linux
wowocpp26 分钟前
ubuntu 22.04 硬件配置 查看 显卡
linux·运维·ubuntu
山河君38 分钟前
ubuntu使用DeepSpeech进行语音识别(包含交叉编译)
linux·ubuntu·语音识别
鹏大师运维42 分钟前
【功能介绍】信创终端系统上各WPS版本的授权差异
linux·wps·授权·麒麟·国产操作系统·1024程序员节·统信uos
筱源源44 分钟前
Elasticsearch-linux环境部署
linux·elasticsearch
pk_xz1234563 小时前
Shell 脚本中变量和字符串的入门介绍
linux·运维·服务器
小珑也要变强3 小时前
Linux之sed命令详解
linux·运维·服务器
Lary_Rock5 小时前
RK3576 LINUX RKNN SDK 测试
linux·运维·服务器
云飞云共享云桌面7 小时前
8位机械工程师如何共享一台图形工作站算力?
linux·服务器·网络
Peter_chq7 小时前
【操作系统】基于环形队列的生产消费模型
linux·c语言·开发语言·c++·后端