目录
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, ...);
摄像头采集步骤思路
- 打开摄像头:
open("/dev/video7")
- 设置格式:
VIDIOC_S_FMT
- 申请缓冲区:
VIDIOC_REQBUFS
- 获取缓冲区信息:
VIDIOC_QUERYBUF
- 内存映射:
mmap()
- 放入队列:
VIDIOC_QBUF
- 开始采集:
VIDIOC_STREAMON
- 读取数据:
VIDIOC_DQBUF
,处理后再VIDIOC_QBUF
- 停止采集:
VIDIOC_STREAMOFF
- 释放资源:
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),决定图像的亮暗。U
、V
负责 色彩信息: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;
}