正点原子V4L2 捕获摄像头视频帧同时转为JPG (2)

上一期实现了 捕获视频帧,但是捕获的是原始帧(我使用的是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 格式。

这个是使用的对应的库文件。

步骤如下:

  1. 初始化 JPEG 库:创建压缩对象和错误处理
  2. 设置参数:图像宽高、颜色空间(RGB)、压缩质量
  3. 逐行写入数据:JPEG 库按行处理图像
  4. 完成压缩:关闭文件,释放资源

运行结果

首先先看一下运行的结果

图片

用到库了,编译时候要加上可以先查看 编译链里面有没有例如:

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;
}

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值