You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何基于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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.02 05:05:19