上一期实现了 捕获视频帧,但是捕获的是原始帧(我使用的是NV12),我们想直接查看,还需要进一步的转换,因此 本次提供获取到的YUV帧,并进行转化JPG,完整的代码会赋到最后面。
YUV转JPG
科普一下YUV:Y(亮度)U和V(色度),YUV把图像分成"亮度+颜色"两部分存储
YUV 格式有很多种,比如 NV12、YUYV 等。我们的代码处理的是NV12 格式,它的特点是:
- 先存储所有 Y 数据(宽度 × 高度个字节)
- 再存储 UV 数据(宽度 × 高度 ÷2 个字节,U 和 V 交替排列)(UVUVUV...交替排列)
内存布局
Y00 Y01 Y02 ... Ynn | U00 V00 U02 ...
除以 2是因为 YUV420 格式的色度(UV)数据被采样了,导致它的大小只有亮度(Y)数据的四分之一。对于一个宽度×高度的图像,Y 数据需要宽度×高度个字节,而 UV 数据只需要宽度×高度÷4个 U 和宽度×高度÷4个 V,总共宽度×高度÷2个字节。
- Y 值:直接在 Y 区域的第
y×宽度 + x位置 - UV 值:在 UV 区域的第
(y/2)×(宽度/2) + (x/2)对位置,每对包含一个 U 和一个 V(相邻存储)
Y像素布局: 对应的UV采样点:
Y00 Y01 Y02 Y03 (0,0) (0,1)
Y10 Y11 Y12 Y13 (1,0) (1,1)
Y20 Y21 Y22 Y23
Y30 Y31 Y32 Y33
UV采样点与Y像素的对应关系:
UV(0,0) 对应 Y00 Y01 Y10 Y11
UV(0,1) 对应 Y02 Y03 Y12 Y13
UV(1,0) 对应 Y20 Y21 Y30 Y31
UV(1,1) 对应 Y22 Y23 Y32 Y33
为什么需要这样采样?
降低数据量!人类眼睛对亮度变化比对颜色变化更敏感。保留完整的 Y 数据(保证图像细节),
减少 UV 数据(节省存储空间)
- RGB:每个像素 3 字节(R+G+B),总大小 =
宽度×高度×3 - YUV420:Y 占 1 字节,UV 共占 0.5 字节,总大小 =
宽度×高度×1.5
JPEG是一种压缩图像格式,它只支持RGB 颜色空间(红、绿、蓝三原色)。所以,要把 YUV 转为 JPEG,必须先把 YUV 转为 RGB。
YUV 转 RGB 的核心原理
- Y 数据在最前面,直接按位置取
- U 和 V 数据在后面,且每 2×2 个 Y 像素共享一组 UV 值(因为 UV 数据量是 Y 的 1/4)
- 用公式把 YUV 转为 RGB
R = Y + 1.402 × (V-128)
G = Y - 0.34414 × (U-128) - 0.71414 × (V-128)
B = Y + 1.772 × (U-128)
公式中的数字是固定的系数,减去 128 是因为 UV 的中心值是 128(范围 0-255)
计算结果可能超出 0-255,需要 "裁剪"
if (R < 0) R = 0;
if (R > 255) R = 255;
// 同理处理G和B
下面是实际存储后下x,y对应的索引
y_index = i * width + j;
uv_index = width * height + ((i / 2) * (width / 2) + (j / 2)) * 2;
JPEG 压缩过程
转换为 RGB 后,还需要压缩成 JPEG 格式。
这个是使用的对应的库文件。
步骤如下:
- 初始化 JPEG 库:创建压缩对象和错误处理
- 设置参数:图像宽高、颜色空间(RGB)、压缩质量
- 逐行写入数据:JPEG 库按行处理图像
- 完成压缩:关闭文件,释放资源
运行结果
首先先看一下运行的结果


图片

用到库了,编译时候要加上可以先查看 编译链里面有没有例如:
code@machine:~find /opt/atk-dlrk356x-toolchain -name "jpeglib.h"
/opt/atk-dlrk356x-toolchain/aarch64-buildroot-linux-gnu/sysroot/usr/include/jpeglib.h
/opt/atk-dlrk356x-toolchain/include/jpeglib.h
没啥问题可以使用
code@machine:~/yingyong/v4l2_danbu/2$ find /opt/atk-dlrk356x-toolchain -name "libjpeg*"
/opt/atk-dlrk356x-toolchain/lib/libjpeg.so.9
/opt/atk-dlrk356x-toolchain/lib/libjpeg.la
/opt/atk-dlrk356x-toolchain/lib/libjpeg.so
/opt/atk-dlrk356x-toolchain/lib/libjpeg.so.9.2.0
/opt/atk-dlrk356x-toolchain/lib/pkgconfig/libjpeg.pc
/opt/atk-dlrk356x-toolchain/aarch64-buildroot-linux-gnu/sysroot/usr/lib/libjpeg.so.62
/opt/atk-dlrk356x-toolchain/aarch64-buildroot-linux-gnu/sysroot/usr/lib/libjpeg.so.62.3.0
/opt/atk-dlrk356x-toolchain/aarch64-buildroot-linux-gnu/sysroot/usr/lib/libjpeg.so
/opt/atk-dlrk356x-toolchain/aarch64-buildroot-linux-gnu/sysroot/usr/lib/pkgconfig/libjpeg.pc
/opt/atk-dlrk356x-toolchain/aarch64-buildroot-linux-gnu/sysroot/usr/share/doc/libjpeg-turbo
/opt/atk-dlrk356x-toolchain/aarch64-buildroot-linux-gnu/sysroot/usr/share/doc/libjpeg-turbo/libjpeg.txt
编译
aarch64-buildroot-linux-gnu-gcc video_capture1.c -o video_capture1 -ljpeg
代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <sys/mman.h>
#include <sys/ioctl.h>
#include <sys/select.h>
#include <linux/videodev2.h>
#include <jpeglib.h> // 添加libjpeg头文件
#define DEVICE_PATH "/dev/video1"
#define NUM_BUFFERS 4
#define MAX_FORMATS 16
// YUV420(NV12)转JPEG函数
int yuv420_to_jpeg(const unsigned char *yuv_data, int width, int height, const char *filename, int quality) {
struct jpeg_compress_struct cinfo;
struct jpeg_error_mgr jerr;
FILE *outfile;
JSAMPROW row_pointer[1];
int row_stride;
unsigned char *rgb_data;
int i, j, y, u, v, r, g, b;
int y_index, uv_index;
// 分配RGB缓冲区
rgb_data = (unsigned char *)malloc(width * height * 3);
if (!rgb_data) {
fprintf(stderr, "内存分配失败\n");
return -1;
}
// NV12(YUV420)转RGB
for (i = 0; i < height; i++) {
for (j = 0; j < width; j++) {
y_index = i * width + j;
// NV12的UV数据在Y数据之后,每个UV对对应2x2的Y区域
uv_index = width * height + ((i / 2) * (width / 2) + (j / 2)) * 2;
y = yuv_data[y_index];
u = yuv_data[uv_index];
v = yuv_data[uv_index + 1];
// YUV转RGB公式 (BT.601标准)
r = y + 1.402 * (v - 128);
g = y - 0.34414 * (u - 128) - 0.71414 * (v - 128);
b = y + 1.772 * (u - 128);
// 限制在0-255范围内
r = r < 0 ? 0 : (r > 255 ? 255 : r);
g = g < 0 ? 0 : (g > 255 ? 255 : g);
b = b < 0 ? 0 : (b > 255 ? 255 : b);
rgb_data[(i * width + j) * 3] = r;
rgb_data[(i * width + j) * 3 + 1] = g;
rgb_data[(i * width + j) * 3 + 2] = b;
}
}
// 初始化JPEG压缩对象
cinfo.err = jpeg_std_error(&jerr);
jpeg_create_compress(&cinfo);
// 打开输出文件
if ((outfile = fopen(filename, "wb")) == NULL) {
fprintf(stderr, "无法打开输出文件 %s\n", filename);
free(rgb_data);
return -1;
}
jpeg_stdio_dest(&cinfo, outfile);
// 设置JPEG参数
cinfo.image_width = width;
cinfo.image_height = height;
cinfo.input_components = 3;
cinfo.in_color_space = JCS_RGB;
jpeg_set_defaults(&cinfo);
jpeg_set_quality(&cinfo, quality, TRUE);
// 开始压缩
jpeg_start_compress(&cinfo, TRUE);
// 逐行写入
row_stride = width * 3;
while (cinfo.next_scanline < cinfo.image_height) {
row_pointer[0] = &rgb_data[cinfo.next_scanline * row_stride];
(void)jpeg_write_scanlines(&cinfo, row_pointer, 1);
}
// 完成压缩
jpeg_finish_compress(&cinfo);
fclose(outfile);
jpeg_destroy_compress(&cinfo);
free(rgb_data);
return 0;
}
// 列出设备支持的格式
void list_supported_formats(int fd) {
struct v4l2_fmtdesc fmtdesc = {0};
fmtdesc.type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE;
printf("设备支持的格式:\n");
while (ioctl(fd, VIDIOC_ENUM_FMT, &fmtdesc) == 0) {
printf(" %c%c%c%c: %s\n",
fmtdesc.pixelformat & 0xFF,
(fmtdesc.pixelformat >> 8) & 0xFF,
(fmtdesc.pixelformat >> 16) & 0xFF,
(fmtdesc.pixelformat >> 24) & 0xFF,
fmtdesc.description);
// 列出此格式支持的分辨率
struct v4l2_frmsizeenum frmsize = {0};
frmsize.pixel_format = fmtdesc.pixelformat;
frmsize.index = 0;
printf(" 支持的分辨率:\n");
while (ioctl(fd, VIDIOC_ENUM_FRAMESIZES, &frmsize) == 0) {
if (frmsize.type == V4L2_FRMSIZE_TYPE_DISCRETE) {
printf(" %dx%d\n",
frmsize.discrete.width,
frmsize.discrete.height);
} else if (frmsize.type == V4L2_FRMSIZE_TYPE_CONTINUOUS) {
printf(" 连续范围: %dx%d - %dx%d\n",
frmsize.stepwise.min_width,
frmsize.stepwise.min_height,
frmsize.stepwise.max_width,
frmsize.stepwise.max_height);
} else if (frmsize.type == V4L2_FRMSIZE_TYPE_STEPWISE) {
printf(" 步进范围: %dx%d - %dx%d (步长: %dx%d)\n",
frmsize.stepwise.min_width,
frmsize.stepwise.min_height,
frmsize.stepwise.max_width,
frmsize.stepwise.max_height,
frmsize.stepwise.step_width,
frmsize.stepwise.step_height);
}
frmsize.index++;
}
fmtdesc.index++;
}
}
int main(int argc, char *argv[]) {
int fd;
struct v4l2_format fmt;
struct v4l2_requestbuffers req;
struct v4l2_buffer buf;
struct v4l2_plane planes[NUM_BUFFERS];
unsigned char *buffers[NUM_BUFFERS] = {0};
enum v4l2_buf_type type;
int buffer_count;
fd_set fds;
struct timeval timeout;
// 优先尝试的像素格式列表
unsigned int pixel_formats[MAX_FORMATS] = {
V4L2_PIX_FMT_MJPEG, // 优先使用MJPEG (JPEG压缩)
V4L2_PIX_FMT_NV12, // 其次NV12 (YUV 4:2:0)
V4L2_PIX_FMT_UYVY, // 然后UYVY (YUV 4:2:2)
V4L2_PIX_FMT_YUYV // 最后YUYV (YUV 4:2:2)
};
// 尝试的分辨率列表
struct {
int width;
int height;
} resolutions[] = {
{640, 480},
{800, 600},
{1280, 720},
{1920, 1080}
};
int resolution_count = sizeof(resolutions) / sizeof(resolutions[0]);
// 打开设备
fd = open(DEVICE_PATH, O_RDWR);
if (fd < 0) {
perror("打开设备失败");
return -1;
}
// 查询设备能力
struct v4l2_capability cap;
if (ioctl(fd, VIDIOC_QUERYCAP, &cap) < 0) {
perror("查询设备能力失败");
close(fd);
return -1;
}
printf("设备: %s, 驱动: %s\n", cap.card, cap.driver);
printf("视频捕获支持: %s\n",
(cap.capabilities & V4L2_CAP_VIDEO_CAPTURE) ? "是" : "否");
printf("多平面支持: %s\n",
(cap.capabilities & V4L2_CAP_VIDEO_CAPTURE_MPLANE) ? "是" : "否");
// 列出支持的格式
list_supported_formats(fd);
// 设置格式 (尝试多种格式和分辨率)
memset(&fmt, 0, sizeof(fmt));
fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE;
int format_found = 0;
int resolution_found = 0;
// 尝试所有格式和分辨率组合
for (int i = 0; i < MAX_FORMATS; i++) {
if (pixel_formats[i] == 0) break;
for (int r = 0; r < resolution_count; r++) {
fmt.fmt.pix_mp.width = resolutions[r].width;
fmt.fmt.pix_mp.height = resolutions[r].height;
fmt.fmt.pix_mp.pixelformat = pixel_formats[i];
fmt.fmt.pix_mp.field = V4L2_FIELD_NONE;
fmt.fmt.pix_mp.num_planes = 1;
if (ioctl(fd, VIDIOC_S_FMT, &fmt) == 0) {
printf("成功设置格式: %c%c%c%c, %dx%d\n",
fmt.fmt.pix_mp.pixelformat & 0xFF,
(fmt.fmt.pix_mp.pixelformat >> 8) & 0xFF,
(fmt.fmt.pix_mp.pixelformat >> 16) & 0xFF,
(fmt.fmt.pix_mp.pixelformat >> 24) & 0xFF,
fmt.fmt.pix_mp.width, fmt.fmt.pix_mp.height);
format_found = 1;
resolution_found = 1;
break;
} else {
perror("设置格式失败,尝试下一种");
}
}
if (format_found) break;
}
if (!format_found) {
fprintf(stderr, "错误: 无法设置任何支持的格式\n");
close(fd);
return -1;
}
// 验证实际设置的格式
if (ioctl(fd, VIDIOC_G_FMT, &fmt) < 0) {
perror("获取格式失败");
close(fd);
return -1;
}
printf("实际格式: %c%c%c%c, %dx%d, %u 平面\n",
fmt.fmt.pix_mp.pixelformat & 0xFF,
(fmt.fmt.pix_mp.pixelformat >> 8) & 0xFF,
(fmt.fmt.pix_mp.pixelformat >> 16) & 0xFF,
(fmt.fmt.pix_mp.pixelformat >> 24) & 0xFF,
fmt.fmt.pix_mp.width, fmt.fmt.pix_mp.height,
fmt.fmt.pix_mp.num_planes);
// 申请缓冲区
memset(&req, 0, sizeof(req));
req.count = NUM_BUFFERS;
req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE;
req.memory = V4L2_MEMORY_MMAP;
if (ioctl(fd, VIDIOC_REQBUFS, &req) < 0) {
perror("申请缓冲区失败");
close(fd);
return -1;
}
buffer_count = req.count;
printf("分配了 %d 个缓冲区\n", buffer_count);
// 映射缓冲区
for (int i = 0; i < buffer_count; i++) {
memset(&buf, 0, sizeof(buf));
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE;
buf.memory = V4L2_MEMORY_MMAP;
buf.index = i;
buf.m.planes = planes;
buf.length = 1;
if (ioctl(fd, VIDIOC_QUERYBUF, &buf) < 0) {
perror("查询缓冲区失败");
goto cleanup;
}
buffers[i] = mmap(NULL, planes[0].length, PROT_READ | PROT_WRITE,
MAP_SHARED, fd, planes[0].m.mem_offset);
if (buffers[i] == MAP_FAILED) {
perror("映射缓冲区失败");
goto cleanup;
}
printf("缓冲区 %d: 大小=%zu 字节\n", i, planes[0].length);
// 将缓冲区放入队列
if (ioctl(fd, VIDIOC_QBUF, &buf) < 0) {
perror("缓冲区入队失败");
munmap(buffers[i], planes[0].length);
goto cleanup;
}
}
// 开始捕获
type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE;
if (ioctl(fd, VIDIOC_STREAMON, &type) < 0) {
perror("启动捕获失败");
goto cleanup;
}
printf("等待视频数据...\n");
// 使用select()等待数据可用
FD_ZERO(&fds);
FD_SET(fd, &fds);
timeout.tv_sec = 5;
timeout.tv_usec = 0;
int r = select(fd + 1, &fds, NULL, NULL, &timeout);
if (r < 0) {
perror("select失败");
goto cleanup;
} else if (r == 0) {
printf("select超时\n");
goto cleanup;
}
// 捕获一帧
memset(&buf, 0, sizeof(buf));
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE;
buf.memory = V4L2_MEMORY_MMAP;
buf.m.planes = planes;
buf.length = 1;
if (ioctl(fd, VIDIOC_DQBUF, &buf) < 0) {
perror("获取缓冲区失败");
goto cleanup;
}
printf("捕获到一帧: 缓冲区索引=%d, 大小=%zu 字节\n",
buf.index, planes[0].bytesused);
// 根据格式保存数据
const char *extension = "yuv";
if (fmt.fmt.pix_mp.pixelformat == V4L2_PIX_FMT_MJPEG) {
extension = "jpg";
}
char yuv_filename[32];
sprintf(yuv_filename, "frame.%s", extension);
FILE *yuv_fp = fopen(yuv_filename, "wb");
if (!yuv_fp) {
perror("打开YUV文件失败");
if (ioctl(fd, VIDIOC_QBUF, &buf) < 0)
perror("缓冲区回队失败");
goto cleanup;
}
size_t written = fwrite(buffers[buf.index], 1, planes[0].bytesused, yuv_fp);
fclose(yuv_fp);
printf("已保存 %zu 字节到 %s\n", written, yuv_filename);
// 转换为JPG(仅当格式为NV12时)
if (fmt.fmt.pix_mp.pixelformat == V4L2_PIX_FMT_NV12) {
char jpg_filename[32] = "frame.jpg";
printf("正在将YUV转换为JPG...\n");
if (yuv420_to_jpeg(buffers[buf.index], fmt.fmt.pix_mp.width, fmt.fmt.pix_mp.height, jpg_filename, 80) == 0) {
printf("已转换并保存为JPG文件: %s\n", jpg_filename);
} else {
printf("转换为JPG失败\n");
}
} else if (fmt.fmt.pix_mp.pixelformat == V4L2_PIX_FMT_MJPEG) {
printf("已为JPEG格式,无需转换\n");
} else {
printf("不支持的格式,无法转换为JPG\n");
}
// 验证数据有效性(MJPEG格式)
if (fmt.fmt.pix_mp.pixelformat == V4L2_PIX_FMT_MJPEG) {
FILE *fp = fopen(yuv_filename, "rb");
if (fp) {
unsigned char signature[2];
if (fread(signature, 1, 2, fp) == 2) {
if (signature[0] == 0xFF && signature[1] == 0xD8) {
printf("验证: 确认为JPEG格式文件\n");
} else {
printf("警告: 文件不是有效的JPEG格式\n");
}
}
fclose(fp);
}
}
// 将缓冲区重新放入队列
if (ioctl(fd, VIDIOC_QBUF, &buf) < 0) {
perror("缓冲区回队失败");
}
// 停止捕获
if (ioctl(fd, VIDIOC_STREAMOFF, &type) < 0) {
perror("停止捕获失败");
}
// 释放缓冲区
for (int i = 0; i < buffer_count; i++) {
if (buffers[i]) {
munmap(buffers[i], planes[0].length);
}
}
close(fd);
return 0;
cleanup:
// 清理资源
type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE;
ioctl(fd, VIDIOC_STREAMOFF, &type);
for (int i = 0; i < buffer_count; i++) {
if (buffers[i]) {
munmap(buffers[i], planes[0].length);
}
}
close(fd);
return -1;
}
2124

被折叠的 条评论
为什么被折叠?



