抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

Linux V4L2 视频采集 + JPEG 解码 + LCD 显示实践

本文记录一个完整的嵌入式视频处理项目:使用 V4L2 接口从摄像头采集 MJPEG 图像,使用 libjpeg 解码为 RGB 格式,并通过 framebuffer 显示在 LCD 屏幕上。适用于使用 ARM Cortex-A 系列开发板进行嵌入式 Linux 多媒体开发的学习和实践。


开发环境

  • 操作系统:Linux(支持 V4L2 和 framebuffer)
  • 摄像头:支持 MJPEG 输出格式,分辨率 640×480
  • 显示屏:支持 framebuffer 显示,分辨率 800×480,RGB565 格式
  • 编程语言:C
  • 编译依赖:libjpeg 解码库

实现功能

  • 打开摄像头 /dev/video1,设置 MJPEG 格式采集
  • 申请并映射视频缓冲区
  • 解码采集到的 JPEG 数据为 RGB 图像
  • 将 RGB 图像转换为 RGB565 并显示在 LCD(/dev/fb0)上

关键流程

1. 打开 LCD 设备并映射 framebuffer

1
2
int lcdfd = open("/dev/fb0", O_RDWR);
lcdptr = (unsigned int *)mmap(NULL, 800*480*4, PROT_READ | PROT_WRITE, MAP_SHARED, lcdfd, 0);

2.打开摄像头设备并设置采集格式

1
2
3
4
5
6
int fd = open("/dev/video1", O_RDWR);
struct v4l2_format v4formt;
v4formt.fmt.pix.pixelformat = V4L2_PIX_FMT_MJPEG;
v4formt.fmt.pix.width = 640;
v4formt.fmt.pix.height = 480;
ioctl(fd, VIDIOC_S_FMT, &v4formt);

3.申请缓冲区并映射到用户空间

1
2
3
4
5
6
7
8
9
10
struct v4l2_requestbuffers v4rqbuffer;
v4rqbuffer.count = 4;
v4rqbuffer.memory = V4L2_MEMORY_MMAP;
ioctl(fd, VIDIOC_REQBUFS, &v4rqbuffer);

for (int i = 0; i < 4; i++) {
ioctl(fd, VIDIOC_QUERYBUF, &v4buffer);
mptr[i] = (unsigned char *)mmap(NULL, v4buffer.length, ...);
ioctl(fd, VIDIOC_QBUF, &v4buffer); // 放回队列
}

4.启动采集并循环抓图

1
2
3
4
5
6
7
ioctl(fd, VIDIOC_STREAMON, &type);
while (1) {
ioctl(fd, VIDIOC_DQBUF, &readbuffer); // 取帧
read_JPEG_file(mptr[readbuffer.index], rgbdata, readbuffer.length);
lcd_show_rgb(rgbdata, 640, 480); // 显示图像
ioctl(fd, VIDIOC_QBUF, &readbuffer); // 放回队列
}

图像解码与显示

摄像头输出的是 MJPEG 格式(实质是一帧帧 JPEG 图像),我们使用 libjpeg 将其解码为 RGB888 格式(三通道,每像素 3 字节):

1
2
3
4
jpeg_mem_src(&cinfo, jpegData, size);        // 将 JPEG 数据源指向内存
jpeg_read_header(&cinfo, TRUE); // 读取头部信息
jpeg_start_decompress(&cinfo); // 启动解压
jpeg_read_scanlines(&cinfo, &buffer, 1); // 逐行读取 RGB 数据

RGB → RGB565 显示

LCD framebuffer 使用的是 RGB565 格式(每像素 2 字节),我们将 RGB888 的三通道数据压缩为 RGB565,并写入 /dev/fb0

1
2
3
4
5
unsigned char r = rgbdata[j*3 + 0];
unsigned char g = rgbdata[j*3 + 1];
unsigned char b = rgbdata[j*3 + 2];
unsigned short color = ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3);
ptr[j] = color;

编译方法

需要下载libjpeg源码,然后把一些库文件一直到imx6u里面。交叉编译使用命令

1
source /opt/fsl-imx-x11/4.1.15-2.1.0/environment-setup-cortexa7hf-neon-poky-linux-gnueabi  
1
${CC} -o video_show video_show.c  -I/home/zwl/linux/tool/jpeg/include -L/home/zwl/linux/tool/jpeg/lib -ljpeg -Wl,-rpath,/home/zwl/linux/tool/jpeg/lib

运行效果

image-20250504215104659

微信图片_20250504215117

279483eb4281cbd13c810b82febd3b9

使用win系统下的obs打开摄像头,对比发现拍摄画质基本相似。

image-20250504215531986

源码附录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <unistd.h>
#include <linux/videodev2.h>
#include <sys/ioctl.h>
#include <string.h>
#include <sys/mman.h>
#include <stdio.h>
#include <jpeglib.h>
#include <linux/fb.h>

int read_JPEG_file (const char *jpegData, char *rgbdata, int size)
{
struct jpeg_error_mgr jerr;
struct jpeg_decompress_struct cinfo;
cinfo.err = jpeg_std_error(&jerr);
//1创建解码对象并且初始化
jpeg_create_decompress(&cinfo);
//2.装备解码的数据
//jpeg_stdio_src(&cinfo, infile);
jpeg_mem_src(&cinfo,jpegData, size);
//3.获取jpeg图片文件的参数
(void) jpeg_read_header(&cinfo, TRUE);
/* Step 4: set parameters for decompression */
//5.开始解码
(void) jpeg_start_decompress(&cinfo);
//6.申请存储一行数据的内存空间
int row_stride = cinfo.output_width * cinfo.output_components;
unsigned char *buffer = malloc(row_stride);
int i=0;
while (cinfo.output_scanline < cinfo.output_height) {
//printf("****%d\n",i);
(void) jpeg_read_scanlines(&cinfo, &buffer, 1);
memcpy(rgbdata+i*640*3, buffer, row_stride );
i++;
}
//7.解码完成
(void) jpeg_finish_decompress(&cinfo);
//8.释放解码对象
jpeg_destroy_decompress(&cinfo);
return 1;
}

int lcdfd = 0;
unsigned int *lcdptr = NULL;

void lcd_show_rgb(unsigned char *rgbdata, int w, int h)
{
unsigned short *ptr = (unsigned short *)lcdptr; // 重要!!16位屏幕要用short指针!!
for (int i = 0; i < h; i++)
{
for (int j = 0; j < w; j++)
{
unsigned char r = rgbdata[j*3 + 0];
unsigned char g = rgbdata[j*3 + 1];
unsigned char b = rgbdata[j*3 + 2];

unsigned short color = ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3);

ptr[j] = color;
}
ptr += 800; // 每行跳800列
rgbdata += w * 3; // 每行跳 w 个像素 * 3字节
}
}

int main(void)
{
lcdfd = open("/dev/fb0", O_RDWR);
if (lcdfd < 0)
{
perror("/dev/fb0打开失败\n");
return -1;
}

lcdptr = (unsigned int *)mmap(NULL, 800*480*4, PROT_READ | PROT_WRITE, MAP_SHARED, lcdfd, 0);
if(lcdptr < 0)
{
perror("lcd内存映射失败\n");
return -1;
}


//1.打开设备
int fd = open("/dev/video1",O_RDWR);
if (fd < 0){
perror("video0 打开失败");
return -1;
}

//2.获取摄像头支持的格式
struct v4l2_fmtdesc v4fmtdesc;
v4fmtdesc.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
for (int i = 0; i < 3; i++)
{
v4fmtdesc.index = i;
int ret = ioctl(fd, VIDIOC_ENUM_FMT, &v4fmtdesc);
if (ret < 0)
{
perror("VIDIOC_ENUM_FMT获取结束!");
break;
}
printf("index=%d\n",v4fmtdesc.index);
printf("flags=%d\n",v4fmtdesc.flags);
printf("description=%s\n",v4fmtdesc.description);
unsigned char *p = (unsigned char *)&v4fmtdesc.pixelformat;
printf("pixelformat=%C%C%C%C\n",p[0],p[1],p[2],p[3]);
printf("reserved[0]=%d\n",v4fmtdesc.reserved[0]);
}
printf("---------------设置采集格式--------------\n");

//3.设置采集格式
struct v4l2_format v4formt;
v4formt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; //摄像头采集
v4formt.fmt.pix.width = 640; //设置宽 不能任意大小
v4formt.fmt.pix.height = 480; //设置高
v4formt.fmt.pix.pixelformat = V4L2_PIX_FMT_MJPEG; //设置视频采集格式
int ret = ioctl(fd, VIDIOC_S_FMT, &v4formt);
if(ret < 0)
{
perror("VIDIOC_S_FMT:设置格式失败");
}
//验证
memset(&v4formt, 0, sizeof(v4formt)); //清空
v4formt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
ret = ioctl(fd, VIDIOC_G_FMT, &v4formt);
if(ret < 0)
{
perror("获取格式失败");
}
else
{
printf("v4formt.fmt.pix.width = %d\n",v4formt.fmt.pix.width);
printf("v4formt.fmt.pix.height = %d\n",v4formt.fmt.pix.height);
unsigned char *p = (unsigned char *)&v4formt.fmt.pix.pixelformat;
printf("v4formt.fmt.pix.pixelformat = %C%C%C%C\n",p[0],p[1],p[2],p[3]);
printf("设置成功\n");
}

printf("---------------4.申请内核缓冲队列--------------\n");

//4.申请内核缓冲区队列
struct v4l2_requestbuffers v4rqbuffer;
v4rqbuffer.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
v4rqbuffer.count = 4; //申请4个缓冲区
v4rqbuffer.memory = V4L2_MEMORY_MMAP; //映射方式
ret = ioctl(fd, VIDIOC_REQBUFS, &v4rqbuffer);
if (ret < 0)
{
perror("申请队列空间失败");
}
printf("---------------5.映射队列空间到用户空间--------------\n");
//5.映射队列空间到用户空间
unsigned char *mptr[4]; //保存映射后空间的首地址 重要!!!
unsigned int size[4];
struct v4l2_buffer v4buffer;
v4buffer.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;

for (int i = 0; i < 4; i++)
{
v4buffer.index = i;
ret = ioctl(fd, VIDIOC_QUERYBUF, &v4buffer); //从内核空间中查询一个空间做映射
if (ret < 0)
{
perror("查询内核空间队列失败");
}
mptr[i] = (unsigned char *)mmap(NULL,v4buffer.length, PROT_READ | PROT_WRITE, MAP_SHARED, fd, v4buffer.m.offset);
size[i] = v4buffer.length;

//通知使用完毕--‘放回去’
ret = ioctl(fd, VIDIOC_QBUF, &v4buffer);
if(ret < 0)
{
perror("返回失败");
}
}

/* VIDIOC_STREAMON(开始采集写数据到队列中)
VIDIOC_DQBUF(告诉内核我要某一个数据,内核不可以修改)
VIDIOC_QBUF(告诉内核我已经使用完毕)
VIDIOC_STREAMOFF(停止采集-不在向队列中写数据)*/
printf("---------------6.开始采集--------------\n");
//6.开始采集
int type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
ret = ioctl(fd, VIDIOC_STREAMON, &type);
if (ret < 0)
{
perror("开启失败");
}

printf("---------------7.采集数据 从队列中提取一帧数据--------------\n");
//7.采集数据 从队列中提取一帧数据
unsigned char rgbdata[640*480*3]; //定义一个空间存储解码后的RGB数据
struct v4l2_buffer readbuffer;
readbuffer.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
while (1)
{
ret = ioctl(fd, VIDIOC_DQBUF, &readbuffer);
if (ret < 0)
{
perror("读取数据失败");
}

//显示在lcd上

read_JPEG_file(mptr[readbuffer.index], rgbdata, readbuffer.length);//把jpeg数据解码为RGB数据
lcd_show_rgb(rgbdata, 640, 480);

//通知内核已经使用完毕
ret = ioctl(fd, VIDIOC_QBUF, &readbuffer);
if (ret < 0)
{
perror("放回队列失败");
}
}
printf("---------------8.停止采集--------------\n");
//8.停止采集
ret = ioctl(fd, VIDIOC_STREAMOFF, &type);
printf("---------------9.释放映射--------------\n");
//9.释放映射
for (int i = 0; i < 4; i++)
{
printf("size[%d]: %d\n",i,size[i]);
munmap(mptr[i],size[i]);
}

printf("---------------10.关闭设备--------------\n");
//10.关闭设备
close(fd);

printf("all end\n");
return 0;
}

评论