Linux V4L2 摄像头采集 + YUYV 转 RGB 显示

Linux 摄像头采集

V4L2 概念

常见的摄像头格式有很多种,比如RGB,YUYV,MJPEG…,获取到摄像头的图片流后必须按照对应的格式做不同的处理。而在 Linux 系统中,摄像头通常采用 V4L2(Video for Linux 2) 作为标准的视频采集框架。V4L2 是 Linux 内核提供的 视频设备驱动框架,可以支持 USB 摄像头、MIPI CSI 摄像头、PCIe 采集卡等。V4L2 设备驱动框架向应用层提供了一套统一、标准的接口规范,应用程序按照该接口规范来进行应用编程,从而使用摄像头。

架构图

摄像头的设备文件

Linux中V4L2摄像头对应的设备文件是 /dev/video0…1…2…

插上摄像头后:

摄像头的设备文件是 /dev/video7

V4L2摄像头的访问接口是通过ioctl接口,ioctl是专用于硬件控制的系统IO接口

#include <sys/ioctl.h>
int ioctl(int fd, unsigned long request, ...);

摄像头采集步骤思路

  1. 打开摄像头open("/dev/video7")
  2. 设置格式VIDIOC_S_FMT
  3. 申请缓冲区VIDIOC_REQBUFS
  4. 获取缓冲区信息VIDIOC_QUERYBUF
  5. 内存映射mmap()
  6. 放入队列VIDIOC_QBUF
  7. 开始采集VIDIOC_STREAMON
  8. 读取数据VIDIOC_DQBUF,处理后再 VIDIOC_QBUF
  9. 停止采集VIDIOC_STREAMOFF
  10. 释放资源munmap() + close(fd)

摄像头采集实现(c 语言)

需要包含的头文件

#include <stdio.h>      
#include <stdlib.h>     
#include <string.h>     
#include <strings.h>    
#include <fcntl.h>      
#include <unistd.h>     
#include <sys/ioctl.h>  
#include <sys/mman.h>   
#include <linux/videodev2.h>  // V4L2 头文件

打开 /dev/videoX 设备

/*打开摄像头设备*/
    camera_fd = open("/dev/video7",O_RDWR);
    if(camera_fd == -1)
    {
        perror("打开摄像头失败了!\n");
        return -1;
    }

📌解释

  • /dev/videoX:Linux 下的摄像头设备节点,一般是 /dev/video0/dev/video7
  • O_RDWR:以 读写 方式打开设备。

设置摄像头采集格式

使用 struct v4l2_format 结构体

struct v4l2_format vfmt;
    bzero(&vfmt,sizeof(vfmt));//清空结构体
    vfmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;//视频捕获
    vfmt.fmt.pix.width = w;//设置分辨率宽度
    vfmt.fmt.pix.height = h;//设置分辨率高度
    vfmt.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV;//设置采集格式为YUYV

    //设置格式
    if(ioctl(camera_fd,VIDIOC_S_FMT,&vfmt) == -1)//VIDIOC_S_FMT是设置格式
    {
        perror("设置摄像头格式失败\n");
        return -1;
    }

📌 解释

  • struct v4l2_format:用于描述摄像头的采集格式。
  • .type = V4L2_BUF_TYPE_VIDEO_CAPTURE:表示是 视频流 采集(而不是音频等)。
  • .fmt.pix.width, height:设置 分辨率
  • .fmt.pix.pixelformat
    • V4L2_PIX_FMT_YUYV:YUV422 格式(每两个像素共用一个 UV 分量)。
    • V4L2_PIX_FMT_MJPEG:MJPEG 格式(压缩格式)。

申请缓冲区

使用 struct v4l2_requestbuffers 结构体

/*给摄像头的驱动发送命令申请缓冲区*/
    struct v4l2_requestbuffers reqbuf;
    bzero(&reqbuf,sizeof(reqbuf));

    reqbuf.count = 4;//申请4个缓冲区
    reqbuf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;//视频捕获
    reqbuf.memory = V4L2_MEMORY_MMAP;//使用内存映射方式
    //申请缓冲区
    if(ioctl(camera_fd,VIDIOC_REQBUFS,&reqbuf) == -1)//VIDIOC_REQBUFS是申请缓冲区
    {
        perror("申请缓冲区失败\n");
        return -1;
    }

📌 解释

  • struct v4l2_requestbuffers:用于向摄像头驱动申请缓冲区。
  • .count = 4:请求 4 个缓冲区(一般申请 4~8 即可)。
  • .memory = V4L2_MEMORY_MMAP
    • V4L2_MEMORY_MMAP:使用 mmap 方式映射到用户空间😍。
    • V4L2_MEMORY_USERPTR:用户直接提供缓冲区指针。

获取每个缓冲区信息并进行 mmap同时申请入队

使用 struct v4l2_buffer

在这里插入图片描述

定义结构体数据

//自定义结构体把每个缓冲块的首地址和大小存放起来
struct bufmsg
{
	void *start; //存放每个缓冲块的首地址
	int len;  //存放每个缓冲块的实际大小
};
struct bufmsg buffers[4];  // 用结构体数组存储缓冲区信息
    //分配缓冲区给你
    struct v4l2_buffer buf;
    for (int i = 0; i < 4; i++) 
    {
        bzero(&buf, sizeof(buf));
        buf.type   = V4L2_BUF_TYPE_VIDEO_CAPTURE;
        buf.memory = V4L2_MEMORY_MMAP;
        buf.index  = i;//缓冲区的索引号

        // 分配缓冲区信息
        if (ioctl(camera_fd, VIDIOC_QUERYBUF, &buf) == -1) {
            perror("分配缓冲区信息失败");
            return -1;
        }

        // 存储缓冲区大小
        buffers[i].len = buf.length;

        // 映射缓冲区到用户空间,并存储首地址
        buffers[i].start = mmap(NULL, buf.length, PROT_READ | PROT_WRITE, MAP_SHARED, camera_fd, buf.m.offset);
        if (buffers[i].start == MAP_FAILED) {
            perror("映射缓冲区失败");
            return -1;
        }

        printf("缓冲区 %d: 地址 = %p, 大小 = %d 字节\n", i, buffers[i].start, buffers[i].len);

        //顺便申请画面入队--》跟摄像头说我打算等一会把画面数据往四个内存块里面存放了(先打招呼)
		ret=ioctl(camera_fd,VIDIOC_QBUF,&buf);
		if(ret==-1)
		{
			perror("申请入队失败了!\n");
			return -1;
		}
    }

📌 解释

  • VIDIOC_QUERYBUF:获取 缓冲区信息(长度、偏移量等)。
  • mmap()
    • MAP_SHARED:映射到用户空间,允许进程间共享。
    • PROT_READ | PROT_WRITE:可读可写权限。
  • VIDIOC_QBUF:将缓冲区放入队列 等待采集

开启视频流

int type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
if (ioctl(camera_fd, VIDIOC_STREAMON, &type) == -1) {
    perror("启动视频流失败");
    return -1;
}

📌 解释

  • VIDIOC_STREAMON:开启 视频采集

读取摄像头数据

FILE *fp = fopen("output.yuv", "wb");
if (!fp) {
    perror("打开文件失败");
    return -1;
}

for (int i = 0; i < 100; i++) {  // 采集 100 帧
    if (ioctl(camera_fd, VIDIOC_DQBUF, &buf) == -1) {
        perror("获取帧数据失败");
        return -1;
    }

    fwrite(buffer_start[buf.index], buf.bytesused, 1, fp); // 写入 YUYV 数据

    if (ioctl(camera_fd, VIDIOC_QBUF, &buf) == -1) {
        perror("重新放入缓冲区失败");
        return -1;
    }
}

fclose(fp);

📌 解释

  • 采集 100 帧 YUYV 数据,并保存到 output.yuv 文件。
  • buffer_start[buf.index]当前帧的 YUYV 数据,大小为 buf.bytesused
  • fwrite() 将 YUYV 数据直接写入文件

停止采集 & 释放资源

if (ioctl(camera_fd, VIDIOC_STREAMOFF, &type) == -1) {
    perror("停止视频流失败");
}

for (int i = 0; i < 4; i++) {
    munmap(buffer_start[i], buf.length);
}

close(camera_fd);

如何播放视频

在 Linux 终端使用 ffplay 播放:

ffplay -video_size 640x480 -pixel_format yuyv422 -f rawvideo -i output.yuv

📌 解释

  • -video_size 640x480:指定 分辨率,要和你设置的一致。
  • -pixel_format yuyv422:告诉 ffplay 这是 YUYV422 格式。
  • -f rawvideo:原始未压缩的 原始视频流
  • -i output.yuv:输入你的 YUYV 视频文件

在 LCD 开发板显示 YUYV 格式采集的视频

为什么 LCD 不能显示 YUYV 格式视频

LCD 屏幕通常支持 RGB(如 ARGB8888) 格式,而 YUYV 是 YUV 颜色空间,它的存储方式不同:

  • YUYV 颜色空间(YUV422)
    • 存储的是 亮度(Y)+ 色度(U/V)信息,不包含完整的
      R/G/B 信息。
    • 需要 通过公式计算
      才能转换为 RGB 颜色。
  • RGB 颜色空间
    • 直接存储 红、绿、蓝(R、G、B)三原色信息,每个像素都有完整的颜色数据。
    • LCD 需要的是 RGB 数据,而 YUYV 需要转换。

YUYV 编码格式简单介绍

UYV 是 YUV422 格式,它的存储方式如下:

在这里插入图片描述

YUYV 到 ARGB 的转换原理

YUV 转换成 RGB 的公式如下:

R = Y + 1.4075 * (V - 128)
G = Y - 0.3455 * (U - 128) - 0.7169 * (V - 128)
B = Y + 1.779 * (U - 128)
  • Y 表示 亮度(Luminance),决定图像的亮暗。
  • UV 负责 色彩信息
    • U 控制 蓝色分量(Blue chrominance)
    • V 控制 红色分量(Red chrominance)
  • 注意: RGB 计算结果要限制在 0-255 之间

因此,我们今天封装一个转换函数到时出队时直接调用转换并显示到 LCD 屏幕上即可

int yuvtoargb(int y, int u, int v) {
    int r, g, b;
    int pix;

    // 使用转换公式
    r = y + 1.4075 * (v - 128);
    g = y - 0.3455 * (u - 128) - 0.7169 * (v - 128);
    b = y + 1.779 * (u - 128);

    // 修正溢出
    if (r > 255) r = 255; if (r < 0) r = 0;
    if (g > 255) g = 255; if (g < 0) g = 0;
    if (b > 255) b = 255; if (b < 0) b = 0;

    // 组装成 ARGB 格式(Alpha 通道设为 0x00)
    pix = (0x00 << 24) | (r << 16) | (g << 8) | b;
    return pix;
}
  • r, g, b 计算完成后,必须保证在 0-255 之间,否则颜色会溢出导致异常显示。
  • ARGB 格式:32 位像素值
    • 0x00 << 24:Alpha 通道(透明度)
    • r << 16:红色分量
    • g << 8:绿色分量
    • b:蓝色分量

一帧 YUYV 转换为 ARGB

上面只是将一组 YUYV 转换成了两个 ARGB 的像素点,但是一帧画面有好多组,因此我们还需要封装一个函数

int aint allyuyvtoargb(char *yuyvdata,int *argbdata)
{
	int i,j;
	/*
		yuyvdata[0] -->Y1
		yuyvdata[1] -->U
		yuyvdata[2] -->Y2
		yuyvdata[3] -->V
		我们设置最终的画面宽是W
		我们设置最终的画面高是H
		得出结论:最终一帧画面像素点的个数W*H个像素点
		下面的循环,每一轮可以得到两个像素点,因此循环总共需要循环W*H/2轮
	*/
	for(i=0,j=0; i<W*H; i+=2,j+=4)  //循环总次数 W*H/2 
	{
		//一组YUYV可以计算得到两组ARGB
		//Y1跟UV配合
		argbdata[i]=yuvtoargb(yuyvdata[j],yuyvdata[j+1],yuyvdata[j+3]);
		//Y2跟UV配合
		argbdata[i+1]=yuvtoargb(yuyvdata[j+2],yuyvdata[j+1],yuyvdata[j+3]);
	}
	return 0;
}

在这里插入图片描述

实现显示代码到 LCD 屏幕上

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <linux/videodev2.h>  //V4L2的头文件,写代码需要用到这个头文件定义的一些数据类型
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <sys/mman.h>

#define w 640
#define h 480

//封装函数把一组YUV转换得到ARGB数据
int yuvtoargb(int y, int u, int v) {
    int r, g, b;
    int pix;

    // 使用转换公式
    r = y + 1.4075 * (v - 128);
    g = y - 0.3455 * (u - 128) - 0.7169 * (v - 128);
    b = y + 1.779 * (u - 128);

    // 修正溢出
    if (r > 255) r = 255; if (r < 0) r = 0;
    if (g > 255) g = 255; if (g < 0) g = 0;
    if (b > 255) b = 255; if (b < 0) b = 0;

    // 组装成 ARGB 格式(Alpha 通道设为 0x00)
    pix = (0x00 << 24) | (r << 16) | (g << 8) | b;
    return pix;
}

//封装函数把一帧YUYV数据全部转换成ARGB保存
/*
	参数:yuyvdata一帧YUYV数据的首地址
	      argbdata用来保存一帧ARGB数据
*/
int allyuyvtoargb(char *yuyvdata,int *argbdata)
{
	int i,j;
	/*
		yuyvdata[0] -->Y1
		yuyvdata[1] -->U
		yuyvdata[2] -->Y2
		yuyvdata[3] -->V
		我们设置最终的画面宽是W
		我们设置最终的画面高是H
		得出结论:最终一帧画面像素点的个数W*H个像素点
		下面的循环,每一轮可以得到两个像素点,因此循环总共需要循环W*H/2轮
	*/
	for(i=0,j=0; i<w*h; i+=2,j+=4)  //循环总次数 W*H/2 
	{
		//一组YUYV可以计算得到两组ARGB
		//Y1跟UV配合
		argbdata[i]=yuvtoargb(yuyvdata[j],yuyvdata[j+1],yuyvdata[j+3]);
		//Y2跟UV配合
		argbdata[i+1]=yuvtoargb(yuyvdata[j+2],yuyvdata[j+1],yuyvdata[j+3]);
	}
	return 0;
}

//自定义结构体把每个缓冲块的首地址和大小存放起来
struct bufmsg
{
	void *start; //存放每个缓冲块的首地址
	int len;  //存放每个缓冲块的实际大小
};
int main()
{
    int i,j;//循环变量
    int ret;//返回值
    int camera_fd;//摄像头文件描述符
    int lcd_fd;//液晶屏文件描述符
    int *lcd_mem;//液晶屏内存指针

    /****打开液晶屏驱动****/
    lcd_fd = open("/dev/fb0",O_RDWR);
    if (lcd_fd == -1)
    {
        perror("打开液晶屏失败了!\n");
        return -1;
    }
    //映射得到液晶屏首地址
    lcd_mem = mmap(NULL,800*480*4,PROT_READ|PROT_WRITE,MAP_SHARED,lcd_fd,0);
    if(lcd_mem == NULL)
    {
        perror("映射液晶屏失败了!\n");
        return -1;
    }


    /*打开摄像头设备*/
    camera_fd = open("/dev/video7",O_RDWR);
    if(camera_fd == -1)
    {
        perror("打开摄像头失败了!\n");
        return -1;
    }

    /*设置摄像头格式*/
    struct v4l2_format vfmt;
    bzero(&vfmt,sizeof(vfmt));//清空结构体
    vfmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;//视频捕获
    vfmt.fmt.pix.width = w;//设置分辨率宽度
    vfmt.fmt.pix.height = h;//设置分辨率高度
    vfmt.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV;//设置采集格式为YUYV,因为我摄像头是这个格式的
                                                 /*
                                                    V4L2_PIX_FMT_YUYV 表示YUV格式的画面
                                                    V4L2_PIX_FMT_JPEG 表示jpg格式的画面
                                                 */
    //设置格式
    if(ioctl(camera_fd,VIDIOC_S_FMT,&vfmt) == -1)//VIDIOC_S_FMT是设置格式
    {
        perror("设置摄像头格式失败\n");
        return -1;
    }

    /*给摄像头的驱动发送命令申请缓冲区*/
    struct v4l2_requestbuffers reqbuf;
    bzero(&reqbuf,sizeof(reqbuf));

    reqbuf.count = 4;//申请4个缓冲区
    reqbuf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;//视频捕获
    reqbuf.memory = V4L2_MEMORY_MMAP;//使用内存映射方式
    //申请缓冲区
    if(ioctl(camera_fd,VIDIOC_REQBUFS,&reqbuf) == -1)//VIDIOC_REQBUFS是申请缓冲区
    {
        perror("申请缓冲区失败\n");
        return -1;
    }

    
    struct bufmsg buffers[4];  // 用结构体数组存储缓冲区信息
    //分配缓冲区给你
    struct v4l2_buffer buf;
    for (int i = 0; i < 4; i++) 
    {
        bzero(&buf, sizeof(buf));
        buf.type   = V4L2_BUF_TYPE_VIDEO_CAPTURE;
        buf.memory = V4L2_MEMORY_MMAP;
        buf.index  = i;//缓冲区的索引号

        // 分配缓冲区信息
        if (ioctl(camera_fd, VIDIOC_QUERYBUF, &buf) == -1) {
            perror("分配缓冲区信息失败");
            return -1;
        }

        // 存储缓冲区大小
        buffers[i].len = buf.length;

        // 映射缓冲区到用户空间,并存储首地址
        buffers[i].start = mmap(NULL, buf.length, PROT_READ | PROT_WRITE, MAP_SHARED, camera_fd, buf.m.offset);
        if (buffers[i].start == MAP_FAILED) {
            perror("映射缓冲区失败");
            return -1;
        }

        printf("缓冲区 %d: 地址 = %p, 大小 = %d 字节\n", i, buffers[i].start, buffers[i].len);

        //顺便申请画面入队--》跟摄像头说我打算等一会把画面数据往四个内存块里面存放了(先打招呼)
		ret=ioctl(camera_fd,VIDIOC_QBUF,&buf);
		if(ret==-1)
		{
			perror("申请入队失败了!\n");
			return -1;
		}
    }

    //开始启动摄像头采集画面--》画面马上就有了
	enum v4l2_buf_type mytype;
	mytype=V4L2_BUF_TYPE_VIDEO_CAPTURE;
	ret=ioctl(camera_fd,VIDIOC_STREAMON,&mytype);
	if(ret==-1)
	{
		perror("启动摄像头开始采集画面失败了!\n");
		return -1;
	}
	
	//定义数组存放转换得到一帧ARGB数据
	int argbbuf[w*h];
	while(1)
	{
		for(i=0; i<4; i++)
		{
			//出队
			ret=ioctl(camera_fd,VIDIOC_DQBUF,&buf);
			if(ret==-1)
			{
				perror("申请出队失败了!\n");
				return -1;
			}
			
			//把出队的画面显示出来
			//YUYV转换成ARGB,液晶屏不可以直接显示YUYV数据
			allyuyvtoargb(buffers[i].start,argbbuf);
			//使用memcpy把argbbuf里面的数据拷贝到液晶屏
			/*
				第一行像素点 argbbuf[0]---argbbuf[W-1]
				             lcdmem+0*800
				第二行像素点 argbbuf[W]---argbbuf[2*W-1]
				             lcdmem+1*800
				第三行像素点 argbbuf[2*W]---argbbuf[3*W-1]
				             lcdmem+2*800
			*/
			for(j=0; j<h; j++)
				memcpy(lcd_mem+j*800,&argbbuf[j*w],w*4);  //每一次拷贝一行像素点
			
			//入队
			ret=ioctl(camera_fd,VIDIOC_QBUF,&buf);
			if(ret==-1)
			{
				perror("申请入队失败了!\n");
				return -1;
			}		
		}
	}
	
	//收尾
	ret=ioctl(camera_fd,VIDIOC_STREAMOFF,&mytype);
	if(ret==-1)
	{
		perror("关闭摄像头失败了!\n");
		return -1;
	}
	close(camera_fd);
	close(lcd_fd);
	munmap(lcd_mem,800*480*4);
	for(i=0; i<4; i++)
		munmap(buffers[i].start,buffers[i].len);
	return 0;


}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值