如何基于V4L2从MJPEG流中提取单帧图像?
问题背景
现有一套基于V4L2的视频捕获实现,之前在测试过的相机上运行正常,核心处理逻辑如下:
bidx=read_frame(data); if (bidx>-1) { if (data->pixelformat==V4L2_PIX_FMT_YUYV) yuv422_to_rgb24(data,(unsigned char*)data->buffers[bidx].start); else if (data->pixelformat==V4L2_PIX_FMT_YUV422P) yuv422p_to_rgb24(data,(unsigned char*)data->buffers[bidx].start); else { printf("Error: unsupported pixel format 0x%X\n",data->pixelformat); assert(0); } }
其中V4L2_PIX_FMT_YUYV和V4L2_PIX_FMT_YUV422P格式下,缓冲区数据为单帧图像,符合需求。但现在某设备返回MJPG格式(疑似Linux内核更新导致),触发错误分支。
需求:纯编程实现从MJPG数据中提取单帧图像,禁止调用外部命令行工具。
补充:V4L2的帧读取逻辑在read_frame()函数:
int read_frame(struct instData *data) { struct v4l2_buffer buf; memset(&buf,0,sizeof(buf)); buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; buf.memory = V4L2_MEMORY_MMAP; if (-1 == xioctl (data->fd, VIDIOC_DQBUF, &buf)) { switch (errno) { case EAGAIN: return -1; case EIO: /* Could ignore EIO, see spec. */ /* fall through */ default: return -1; } } xioctl (data->fd, VIDIOC_QBUF, &buf); return buf.index; }
解决方案
1. 理解V4L2返回的MJPG数据结构
V4L2设备返回的V4L2_PIX_FMT_MJPEG格式数据,每个出队的缓冲区(VIDIOC_DQBUF)中存储的是完整的JPEG帧——包含JPEG标准的起始标记(0xFFD8,SOI)和结束标记(0xFFD9,EOI),直接可用于JPEG解码。
注意:必须使用buf.bytesused字段获取实际有效数据长度,而非缓冲区的总大小,因为MJPG是压缩格式,单帧数据长度不固定。
2. 使用libjpeg库解码MJPG
libjpeg是C语言环境下处理JPEG的标准库,可直接嵌入代码中实现解码,无需外部工具。以下是核心解码逻辑:
步骤1:添加libjpeg依赖
编译时链接libjpeg库(比如使用-ljpeg编译参数)。
步骤2:实现MJPG到RGB24的解码函数
#include <jpeglib.h> #include <setjmp.h> // 自定义错误处理结构体,避免libjpeg默认的退出行为 struct my_error_mgr { struct jpeg_error_mgr pub; jmp_buf setjmp_buffer; }; METHODDEF(void) my_error_exit(j_common_ptr cinfo) { struct my_error_mgr* myerr = (struct my_error_mgr*)cinfo->err; (*cinfo->err->output_message)(cinfo); longjmp(myerr->setjmp_buffer, 1); } int mjpg_to_rgb24(struct instData *data, unsigned char *mjpg_data, unsigned long mjpg_size, unsigned char *rgb_out) { struct jpeg_decompress_struct cinfo; struct my_error_mgr jerr; JSAMPARRAY buffer; int row_stride; // 初始化错误处理 cinfo.err = jpeg_std_error(&jerr.pub); jerr.pub.error_exit = my_error_exit; if (setjmp(jerr.setjmp_buffer)) { jpeg_destroy_decompress(&cinfo); return -1; } // 初始化解码器 jpeg_create_decompress(&cinfo); jpeg_mem_src(&cinfo, mjpg_data, mjpg_size); // 读取JPEG头信息 (void)jpeg_read_header(&cinfo, TRUE); // 设置输出格式为RGB24(和之前的YUV转RGB逻辑对齐) cinfo.out_color_space = JCS_RGB; // 启动解码 (void)jpeg_start_decompress(&cinfo); // 计算每行的字节数(RGB24格式) row_stride = cinfo.output_width * 3; // 分配一行的缓冲区 buffer = (*cinfo.mem->alloc_sarray)((j_common_ptr)&cinfo, JPOOL_IMAGE, row_stride, 1); // 逐行解码并写入输出缓冲区 unsigned char *rgb_ptr = rgb_out; while (cinfo.output_scanline < cinfo.output_height) { (void)jpeg_read_scanlines(&cinfo, buffer, 1); memcpy(rgb_ptr, buffer[0], row_stride); rgb_ptr += row_stride; } // 清理解码器 (void)jpeg_finish_decompress(&cinfo); jpeg_destroy_decompress(&cinfo); return 0; }
步骤3:修改现有代码的帧处理逻辑
首先需要调整read_frame()函数,将buf.bytesused存储到instData结构体中(因为当前函数只返回缓冲区索引,无法获取实际数据长度):
// 修改instData结构体,添加bytesused字段(根据实际结构体定义调整) struct instData { // 原有字段... int fd; struct v4l2_buffer buffers[...]; __u32 pixelformat; unsigned long current_bytesused; // 新增字段,存储当前帧的有效数据长度 }; // 修改read_frame函数 int read_frame(struct instData *data) { struct v4l2_buffer buf; memset(&buf,0,sizeof(buf)); buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; buf.memory = V4L2_MEMORY_MMAP; if (-1 == xioctl (data->fd, VIDIOC_DQBUF, &buf)) { switch (errno) { case EAGAIN: return -1; case EIO: /* Could ignore EIO, see spec. */ /* fall through */ default: return -1; } } // 存储当前帧的有效数据长度 data->current_bytesused = buf.bytesused; xioctl (data->fd, VIDIOC_QBUF, &buf); return buf.index; }
然后修改主处理逻辑,添加MJPG分支:
bidx=read_frame(data); if (bidx>-1) { unsigned char *frame_data = (unsigned char*)data->buffers[bidx].start; if (data->pixelformat==V4L2_PIX_FMT_YUYV) { yuv422_to_rgb24(data, frame_data); } else if (data->pixelformat==V4L2_PIX_FMT_YUV422P) { yuv422p_to_rgb24(data, frame_data); } else if (data->pixelformat==V4L2_PIX_FMT_MJPEG) { // 假设rgb_out是预先分配好的缓冲区,大小为width*height*3 if (mjpg_to_rgb24(data, frame_data, data->current_bytesused, data->rgb_out) != 0) { printf("Error: failed to decode MJPG frame\n"); // 这里根据需求处理错误,而非直接assert } } else { printf("Error: unsupported pixel format 0x%X\n",data->pixelformat); // 替换assert为更温和的错误处理,避免程序崩溃 // assert(0); continue; // 或者其他错误处理逻辑 } }
3. 注意事项
- 确保
rgb_out缓冲区的大小足够:需根据JPEG帧的宽高(可从libjpeg的cinfo.output_width和cinfo.output_height获取)预先分配,或者在解码时动态分配。 - 错误处理:libjpeg默认遇到错误会直接调用
exit(),因此必须自定义错误处理函数(如上述my_error_exit),避免程序意外退出。 - 性能优化:如果需要低延迟,可考虑复用libjpeg的解码结构体,避免每次解码都重新初始化。
内容的提问来源于stack exchange,提问作者Elmi
相关产品推荐
相关产品推荐

