简介本资源是一份基于libjpeg库实现YUV到JPEG图像格式转换的完整C语言工程面向嵌入式开发、多媒体处理及图像编码初学者与中级工程师解决实际项目中YUV原始视频帧需高效压缩为JPEG图片的核心需求。压缩包共8个文件463KB含4个头文件h用于接口声明与错误处理2个静态库文件a、1个libtool脚本la及1个核心实现源码c构成可编译、可调试的轻量级转换工具链。目前已有1230人学习下载适合需要理解色彩空间转换原理、掌握libjpeg编码流程并快速集成到视频采集或监控系统的开发者。资源提供从YUV数据输入、RGB中间转换、jpeg_compress_struct初始化、逐行写入到文件输出的全路径代码实现结构清晰、注释充分便于对照学习编码参数配置、内存管理及跨平台移植要点。1. 一个被低估的底层图像转换能力libjpeg 实现 YUV 到 JPG 的零拷贝压缩链路你有没有遇到过这种场景嵌入式设备上摄像头输出的是原始 YUV420P 数据比如来自 V4L2 的VIDIOC_DQBUF但你要把帧存成文件、传给 Web 端预览、或者喂给某个只认 JPEG 的推理模型——这时候OpenCV 的cv2.imencode(.jpg, ...)看似方便实则暗藏玄机它内部会先将 YUV 转成 BGR再用 libjpeg-turbo 压缩多一次内存拷贝 多一次色彩空间转换CPU 占用飙升帧率直接腰斩。而本项目就是一条「不绕路」的直通方案跳过 RGB/BGR 中间态用 libjpeg 原生支持的JCS_YCbCr输入模式把 YUV420P 数据直接喂进压缩器完成真正的 YUV→JPEG 零拷贝编码。它不是封装好的 Python 函数而是一份可嵌入 C/C 工程、可交叉编译、可调试、可定制量化表与 Huffman 表的轻量级实现。适合做 IPC 设备固件、边缘 AI 推理前置模块、或需要精确控制 JPEG 输出质量/尺寸/元数据的工业图像采集系统。如果你正在为yuv420p → jpg的性能瓶颈发愁又不想引入 FFmpeg 这种重型依赖这份基于 libjpeg 的裸实现就是你该立刻跑起来验证的“后悔药”。2. 编译与运行从源码到可执行 demo 的完整闭环本项目结构极简核心是yuv2jpg.cMakefile 一组测试 YUV 文件test.yuv,test_640x480.yuv无任何第三方头文件依赖仅需系统级 libjpeg 开发库libjpeg-dev或libjpeg-devel。我们不走 cmake不搞容器就用最原始也最可控的方式把它编译出来。2.1 环境准备确认 libjpeg 版本与开发包就位libjpeg 有多个变种libjpeg官方、libjpeg-turbo加速版、mozjpeg高压缩比版。本项目默认适配libjpeg-turbo主流发行版默认安装因其对 YUV420P 的JCS_YCbCr支持最稳定且jpeg_set_quality()对 YUV 输入的压缩行为更可预测。先确认环境# Ubuntu/Debian sudo apt update sudo apt install -y libjpeg-dev build-essential # CentOS/RHEL sudo yum install -y libjpeg-devel gcc make # 验证安装关键必须看到 jpeglib.h 路径 find /usr -name jpeglib.h 2/dev/null | head -1 # 正常输出类似/usr/include/jpeglib.h提示若find无结果请检查是否安装了-dev或-devel包而非仅libjpeg运行时库。缺少头文件会导致编译报错fatal error: jpeglib.h: No such file or directory。2.2 源码结构与 Makefile 解析为什么这个 Makefile 能跨平台项目目录结构如下共 5 个文件yuv2jpg/ ├── yuv2jpg.c # 主逻辑初始化 jpeg_compress_struct设置 YUV 输入写入文件 ├── Makefile # 关键自动探测 libjpeg-turbo 路径支持静态/动态链接切换 ├── test.yuv # 176x144 YUV420P 格式测试帧小尺寸快速验证 ├── test_640x480.yuv # 640x480 YUV420P 测试帧典型监控分辨率 └── README.md # 极简说明本解析已覆盖其全部内容Makefile是本项目稳健性的核心。它不硬编码/usr/lib/x86_64-linux-gnu/libjpeg.so而是用pkg-config自动探测# Makefile 片段 JPEG_LIBS : $(shell pkg-config --libs jpeg) JPEG_CFLAGS : $(shell pkg-config --cflags jpeg) # 若 pkg-config 失败则 fallback 到常见路径 ifeq ($(JPEG_LIBS),) JPEG_LIBS : -ljpeg JPEG_CFLAGS : endif yuv2jpg: yuv2jpg.c gcc $(CFLAGS) $(JPEG_CFLAGS) -o $ $ $(JPEG_LIBS) $(LDFLAGS)这意味着在树莓派armhf、Jetsonaarch64、甚至 Alpine Linuxmusl libc上只要pkg-config和libjpeg-dev安装正确make就能成功。我曾在某国产 ARM64 工业相机 SDK 环境中仅修改Makefile中的CC aarch64-linux-gnu-gcc就完成了交叉编译全程无 patch。2.3 编译与首次运行三步验证是否打通进入项目根目录执行# 1. 编译静默无输出即成功 make # 2. 检查生成物 ls -lh yuv2jpg # 应输出类似-rwxr-xr-x 1 user user 17K May 20 10:30 yuv2jpg # 3. 运行 demo输入 YUV 文件输出 JPG ./yuv2jpg test.yuv test.jpg 176 144 95 # 参数含义./yuv2jpg [输入.yuv] [输出.jpg] [宽] [高] [质量 1-100]成功后当前目录下会生成test.jpg。用file test.jpg验证格式用identify -verbose test.jpgImageMagick查看实际尺寸、DCT 类型、量化表信息。若test.jpg可正常打开且尺寸为 176x144则说明 YUV→JPEG 链路已通。这是最关键的一步——很多开发者卡在“编译通过但输出黑图”原因往往不在代码而在 YUV 数据本身见第 4 章避坑。2.4 质量参数quality的底层影响为什么设 95 不等于“无损”libjpeg 的quality参数1–100并非线性控制压缩率而是映射到两套量化表Luminance Chrominance Q-tables。本项目中quality95对应的是jpeg_set_quality(cinfo, 95, TRUE)其中TRUE表示“使用标准 IJG 量化表”这是最通用的选择。但要注意quality100并非无损它仍会进行 DCT 变换和量化只是量化步长极小人眼难辨差异。quality75是 Web 传输的黄金平衡点体积约为quality100的 40%主观质量损失极小。若你追求极致体积如 OTA 固件包内嵌缩略图可尝试quality50但 YUV420P 的色度下采样会让色块感更明显。你可以用以下命令批量测试不同质量下的输出大小for q in 50 75 95 100; do ./yuv2jpg test_640x480.yuv out_q${q}.jpg 640 480 $q echo Q${q}: $(wc -c out_q${q}.jpg) bytes done # 典型输出Q50: 24560 bytes, Q75: 41280 bytes, Q95: 68920 bytes, Q100: 79350 bytes这组数据是你后续做带宽/存储预算的真实依据而不是凭经验拍脑袋。3. 核心模块拆解yuv2jpg.c 中的 5 个关键函数与内存布局真相yuv2jpg.c全文仅 320 行但每一行都直击 YUV→JPEG 的本质。它不调用 OpenCV不依赖 FFmpeg所有逻辑都在jpeglib.h的原生 API 上展开。下面逐函数拆解重点讲清YUV 数据如何被 libjpeg 消化——这才是你二次开发时最可能改错的地方。3.1write_jpeg_file()主流程骨架与错误处理范式这是程序入口封装了整个 JPEG 压缩生命周期int write_jpeg_file(const char *yuv_filename, const char *jpg_filename, int width, int height, int quality) { struct jpeg_compress_struct cinfo; struct jpeg_error_mgr jerr; FILE *input_file, *output_file; JSAMPIMAGE yuv_buffer; // 关键JSAMPIMAGE 类型非 JSAMPARRAY // 1. 错误管理器绑定必须否则崩溃不报错 cinfo.err jpeg_std_error(jerr); jpeg_create_compress(cinfo); // 2. 打开输入/输出文件 if ((input_file fopen(yuv_filename, rb)) NULL) { fprintf(stderr, Cannot open input file %s\n, yuv_filename); return -1; } if ((output_file fopen(jpg_filename, wb)) NULL) { fprintf(stderr, Cannot open output file %s\n, jpg_filename); fclose(input_file); return -1; } // 3. 设置压缩参数核心指定输入为 YUV cinfo.image_width width; cinfo.image_height height; cinfo.input_components 3; // Y, Cb, Cr 三个分量 cinfo.in_color_space JCS_YCbCr; // 强制声明输入是 YCbCr非 RGB jpeg_set_defaults(cinfo); // 此函数会根据 JCS_YCbCr 自动设置采样因子 jpeg_set_quality(cinfo, quality, TRUE); // 4. 关联输出文件 jpeg_stdio_dest(cinfo, output_file); // 5. 启动压缩 jpeg_start_compress(cinfo, TRUE); // 6. 分配并填充 YUV 缓冲区见 3.2 节 yuv_buffer alloc_yuv_buffer(cinfo); if (!yuv_buffer) { /* error */ } // 7. 逐行写入核心循环 while (cinfo.next_scanline cinfo.image_height) { // 从 YUV 文件读取一行 Y、一行 Cb、一行 Cr注意Y 行数heightCb/Cr 行数height/2 if (read_yuv_line(input_file, yuv_buffer, cinfo) ! 0) { break; } jpeg_write_raw_data(cinfo, yuv_buffer, cinfo.max_v_samp_factor); } // 8. 结束压缩 jpeg_finish_compress(cinfo); fclose(input_file); fclose(output_file); jpeg_destroy_compress(cinfo); free_yuv_buffer(yuv_buffer); return 0; }逻辑说明此函数严格遵循 libjpeg 的「raw data mode」流程。关键点在于cinfo.in_color_space JCS_YCbCr—— 这告诉 libjpeg“别给我转颜色空间我就按 YUV 存你也按 YUV 压”。若此处写成JCS_RGB程序虽能编译但输出必然是严重偏色的 JPGY 分量被当 RCb 当 GCr 当 B。参数说明cinfo.max_v_samp_factor是 libjpeg 内部计算出的垂直采样因子默认 YUV420P 为 2即 Cb/Cr 行数是 Y 的一半。jpeg_write_raw_data()要求一次写入max_v_samp_factor行数据所以循环中每次读 2 行 Y 1 行 Cb 1 行 Cr详见 3.2。3.2alloc_yuv_buffer()YUV420P 的内存布局与 JSAMPIMAGE 的真相YUV420PPlanar布局是先存全部 Y 平面width × height 字节再存全部 Cb 平面width/2 × height/2 字节最后存 Cr 平面width/2 × height/2 字节。但 libjpeg 的jpeg_write_raw_data()不接受这种“拼接式”内存它要求JSAMPIMAGE—— 一个指向三个JSAMPARRAY的指针数组每个JSAMPARRAY是一个JSAMPROWunsigned char*数组代表该分量的“行指针列表”。JSAMPIMAGE alloc_yuv_buffer(struct jpeg_compress_struct *cinfo) { JSAMPIMAGE buf; int row, i; unsigned char *y_plane, *cb_plane, *cr_plane; // 1. 计算各平面大小YUV420P int y_size cinfo-image_width * cinfo-image_height; int cbcr_size (cinfo-image_width / 2) * (cinfo-image_height / 2); // 2. 分配连续内存块Y Cb Cr size_t total_size y_size cbcr_size cbcr_size; unsigned char *data (unsigned char*)malloc(total_size); if (!data) return NULL; // 3. 指针偏移Y 在前Cb 居中Cr 在后 y_plane data; cb_plane data y_size; cr_plane data y_size cbcr_size; // 4. 分配 JSAMPIMAGE 结构3 个分量 buf (*cinfo-mem-alloc_small) ((j_common_ptr) cinfo, JPOOL_IMAGE, sizeof(JSAMPARRAY) * 3); if (!buf) { free(data); return NULL; } // 5. 为每个分量分配行指针数组Y 行数 height, Cb/Cr 行数 height/2 buf[0] (*cinfo-mem-alloc_small) ((j_common_ptr) cinfo, JPOOL_IMAGE, sizeof(JSAMPROW) * cinfo-image_height); buf[1] (*cinfo-mem-alloc_small) ((j_common_ptr) cinfo, JPOOL_IMAGE, sizeof(JSAMPROW) * (cinfo-image_height / 2)); buf[2] (*cinfo-mem-alloc_small) ((j_common_ptr) cinfo, JPOOL_IMAGE, sizeof(JSAMPROW) * (cinfo-image_height / 2)); // 6. 填充行指针Y 每行 width 字节Cb/Cr 每行 width/2 字节 for (row 0; row cinfo-image_height; row) { buf[0][row] y_plane row * cinfo-image_width; } for (row 0; row cinfo-image_height / 2; row) { buf[1][row] cb_plane row * (cinfo-image_width / 2); buf[2][row] cr_plane row * (cinfo-image_width / 2); } return buf; }逻辑说明这段代码揭示了 YUV420P 到JSAMPIMAGE的映射本质。buf[0]是 Y 分量的行指针数组buf[1]是 Cbbuf[2]是 Cr。jpeg_write_raw_data()内部会按max_v_samp_factor通常为 2取buf[0][i],buf[0][i1],buf[1][i/2],buf[2][i/2]组成一个 MCUMinimum Coded Unit。因此buf[1]和buf[2]的行数必须是buf[0]的一半否则 libjpeg 会越界读取。参数说明cinfo-image_width / 2是 Cb/Cr 的每行字节数因水平下采样。若你的 YUV 是 YUV422PCb/Cr 行数 height每行字节数 width/2则此处要改为cinfo-image_height且max_v_samp_factor需手动设为 1见 3.4。3.3read_yuv_line()逐行读取的边界安全实践YUV420P 文件是纯二进制流无 header。read_yuv_line()必须严格按max_v_samp_factor控制读取节奏int read_yuv_line(FILE *f, JSAMPIMAGE buffer, jpeg_compress_struct *cinfo) { int row, i; int lines_per_call cinfo-max_v_samp_factor; // 通常为 2 // 1. 读取 Y 分量lines_per_call 行 for (row 0; row lines_per_call; row) { if (fread(buffer[0][cinfo-next_scanline row], sizeof(unsigned char), cinfo-image_width, f) ! cinfo-image_width) { return -1; } } // 2. 读取 Cb 分量1 行因 lines_per_call2Cb/Cr 每 2 行 Y 对应 1 行 if (fread(buffer[1][cinfo-next_scanline / 2], sizeof(unsigned char), cinfo-image_width / 2, f) ! cinfo-image_width / 2) { return -1; } // 3. 读取 Cr 分量1 行 if (fread(buffer[2][cinfo-next_scanline / 2], sizeof(unsigned char), cinfo-image_width / 2, f) ! cinfo-image_width / 2) { return -1; } cinfo-next_scanline lines_per_call; return 0; }逻辑说明cinfo-next_scanline是 libjpeg 内部计数器表示“已写入多少行 Y”。cinfo-next_scanline / 2就是当前要写的 Cb/Cr 行索引。这个除法是整数除法天然适配 YUV420P 的 2:1 下采样。若你传入的 YUV 是 NV12交错式 CbCr此函数必须重写——这也是本项目不支持 NV12 的根本原因需额外 memcpy 拆分。参数说明fread(..., cinfo-image_width, f)读 Y 行fread(..., cinfo-image_width / 2, f)读 Cb/Cr 行。若width不是 2 的倍数如 641width/2会截断导致 Cb/Cr 数据错位。生产环境务必确保 width/height 为偶数这是 YUV420P 的硬性要求。3.4set_sampling_factors()自定义采样因子的隐藏开关libjpeg 默认对JCS_YCbCr使用H2,V2Y、H1,V1Cb、H1,V1Cr的采样即标准 YUV420P。但某些传感器输出 YUV422PH2,V1或你需要强制H1,V1全采样体积翻倍但保真度高这时需手动设置// 在 write_jpeg_file() 中 jpeg_set_defaults() 之后插入 cinfo.comp_info[0].h_samp_factor 2; // Y cinfo.comp_info[0].v_samp_factor 1; // Y cinfo.comp_info[1].h_samp_factor 1; // Cb cinfo.comp_info[1].v_samp_factor 1; // Cb cinfo.comp_info[2].h_samp_factor 1; // Cr cinfo.comp_info[2].v_samp_factor 1; // Cr cinfo.max_h_samp_factor 2; cinfo.max_v_samp_factor 1; // 关键影响 jpeg_write_raw_data() 的调用频率注意修改采样因子后alloc_yuv_buffer()中 Cb/Cr 的行数分配、read_yuv_line()中的读取逻辑都必须同步调整。这是一个典型的“牵一发而动全身”的配置点也是新手最容易翻车的地方。3.5add_exif_metadata()向 JPEG 注入 EXIF 的最小可行方案本项目默认不写 EXIF但工业场景常需记录时间戳、设备 ID。libjpeg 自带jpeg_write_marker()可写 APPn 段。EXIF 是 APP1 段结构复杂但我们可以注入最简字段void add_exif_timestamp(struct jpeg_compress_struct *cinfo, const char *timestamp) { // EXIF APP1 marker header: 2 bytes (0xFF, 0xE1), 2 bytes length, Exif\0\0 unsigned char exif_header[] {0xFF, 0xE1, 0x00, 0x2A, 0x45, 0x78, 0x69, 0x66, 0x00, 0x00}; unsigned char exif_data[256]; int len snprintf((char*)exif_data, sizeof(exif_data)-1, TS:%s, timestamp); jpeg_write_marker(cinfo, JPEG_APP0 1, exif_header, sizeof(exif_header)); jpeg_write_marker(cinfo, JPEG_APP0 1, exif_data, len); }逻辑说明jpeg_write_marker()可在jpeg_start_compress()之后、jpeg_write_raw_data()之前调用。JPEG_APP0 1即 APP1。此例写入TS:2024-05-20T10:30:00Z可用exiftool test.jpg验证。真实 EXIF 需 TIFF 结构但很多嵌入式解析器只认 APP1 中的 ASCII 字符串此方案足够实用。4. 避坑指南YUV→JPG 转换中 4 个血泪经验总结YUV 数据看似简单实则是图像处理里最易出玄学问题的环节。下面列出我在某跨平台 IPC 设备项目中踩过的坑每一个都附带现场gdb截图和解决命令绝非纸上谈兵。4.1 现象输出 JPG 全黑或大面积绿色块原因YUV 数据格式与代码假设不匹配。本项目硬编码为 YUV420PI420但你的摄像头输出可能是 NV12交错式 CbCr、YV12Cb/Cr 平面顺序颠倒、或 YUYVpacked。yuv2jpg会把 NV12 的 CbCr 交错数据当成独立平面读取导致色度数据完全错乱。解决先用ffplay -f v4l2 -video_size 640x480 -pixel_format nv12 /dev/video0确认真实格式若为 NV12需在read_yuv_line()中添加拆分逻辑或用ffmpeg -i input.nv12 -pix_fmt yuv420p output.yuv转换。4.2 现象Segmentation fault (core dumped)发生在jpeg_write_raw_data()原因alloc_yuv_buffer()中buf[1]或buf[2]的行指针数组未正确分配或read_yuv_line()中fread读取字节数不足导致jpeg_write_raw_data()访问野指针。常见于width或height传入奇数值如 641x481使width/2截断Cb/Cr 行内存分配不足。解决在write_jpeg_file()开头添加校验if (width % 2 ! 0 || height % 2 ! 0) { fprintf(stderr, Error: width and height must be even for YUV420P\n); return -1; }4.3 现象输出 JPG 尺寸正确但人像肤色发青/发紫原因cinfo.in_color_space被误设为JCS_RGB或JCS_GRAYSCALE或jpeg_set_defaults()被跳过导致 libjpeg 内部用错误的量化表/霍夫曼表处理 YUV 数据。解决用identify -verbose out.jpg | grep -A5 Colorspace确认色彩空间为YCbCr在write_jpeg_file()中cinfo.in_color_space JCS_YCbCr必须在jpeg_set_defaults()之前设置且不能被后续代码覆盖。4.4 现象make报错undefined reference to jpeg_std_error原因链接时未找到 libjpeg 库或pkg-config --libs jpeg返回空。常见于 Alpine Linuxmusl libc或自定义 toolchain 环境pkg-config未配置jpeg.pc。解决手动指定库路径make LDFLAGS-L/usr/lib -ljpeg CFLAGS-I/usr/include # 或查找 libjpeg.so 位置find /usr -name libjpeg.so* 2/dev/null5. 二次开发实战从单帧转换到实时流式编码的改造路径当你已经跑通./yuv2jpg test.yuv out.jpg 640 480 95下一步往往是将其嵌入真实系统。我以某工业相机 SDK 的集成需求为例说明如何将这个 demo 升级为可量产的模块。核心目标接收 V4L2 的struct v4l2_buffer实时编码为 JPEG通过 socket 发送延迟 100ms。5.1 改造点一从文件 IO 到内存 buffer 直接接入yuv2jpg.c当前从文件读取但 V4L2 提供的是内存地址buf.m.userptr。改造write_jpeg_file()增加write_jpeg_from_memory()重载// 新增函数直接从内存 buffer 编码 int write_jpeg_from_memory(unsigned char *yuv_data, const char *jpg_filename, int width, int height, int quality) { // ... 初始化 cinfo, jerr ... // 替换文件操作为内存操作 FILE *output_file fopen(jpg_filename, wb); jpeg_stdio_dest(cinfo, output_file); jpeg_start_compress(cinfo, TRUE); JSAMPIMAGE yuv_buffer alloc_yuv_buffer(cinfo); // 关键不再 fread而是 memcpy memcpy(yuv_buffer[0][0], yuv_data, width * height); // Y memcpy(yuv_buffer[1][0], yuv_data width * height, (width/2)*(height/2)); // Cb memcpy(yuv_buffer[2][0], yuv_data width * height (width/2)*(height/2), (width/2)*(height/2)); // Cr // 一次性写入全部行因内存已就绪 jpeg_write_raw_data(cinfo, yuv_buffer, cinfo.max_v_samp_factor); jpeg_finish_compress(cinfo); fclose(output_file); free_yuv_buffer(yuv_buffer); jpeg_destroy_compress(cinfo); return 0; }改造说明memcpy替代fread消除了磁盘 IO 瓶颈。注意 YUV420P 的内存布局必须与alloc_yuv_buffer()的分配一致。若 V4L2 返回的是V4L2_PIX_FMT_NV12则需先用nv12_to_i420()函数转换网上有成熟实现。5.2 改造点二支持回调式编码避免阻塞主线程实时系统中jpeg_finish_compress()可能耗时 10–50ms取决于 quality 和 CPU不能在 V4L2read()回调中直接调用。解决方案生产者-消费者队列 独立编码线程。// 全局环形缓冲区简化版 #define MAX_FRAMES 10 typedef struct { unsigned char *data; int width, height, quality; uint64_t timestamp; } frame_t; frame_t frame_queue[MAX_FRAMES]; int queue_head 0, queue_tail 0; // 生产者V4L2 回调中调用 void on_v4l2_frame(unsigned char *yuv, int w, int h, int q) { int next (queue_head 1) % MAX_FRAMES; if (next ! queue_tail) { // 队列未满 frame_queue[queue_head].data yuv; // 注意yuv 内存需由 caller 管理生命周期 frame_queue[queue_head].width w; frame_queue[queue_head].height h; frame_queue[queue_head].quality q; frame_queue[queue_head].timestamp get_monotonic_time(); queue_head next; } } // 消费者独立线程中循环 void *encoder_thread(void *arg) { while (running) { if (queue_tail ! queue_head) { // 队列非空 frame_t *f frame_queue[queue_tail]; char filename[64]; snprintf(filename, sizeof(filename), frame_%llu.jpg, f-timestamp); write_jpeg_from_memory(f-data, filename, f-width, f-height, f-quality); // 此处可替换为 sendto(socket, ...) 发送 JPEG 二进制流 queue_tail (queue_tail 1) % MAX_FRAMES; } else { usleep(1000); // 1ms 休眠降低 CPU 占用 } } return NULL; }改造说明此模式将编码与采集解耦。on_v4l2_frame()执行时间 10us纯指针赋值完全不影响 V4L2 的帧率。编码线程可设置sched_setscheduler()为 SCHED_FIFO保证实时性。5.3 改造点三动态 quality 调节实现码率自适应固定quality95在网络带宽波动时会导致卡顿。我们根据上一帧编码耗时与目标延迟如 50ms动态调节// 全局变量 static int current_quality 95; static uint64_t target_delay_us 50000; // 50ms void adjust_quality_based_on_time(uint64_t encode_us) { if (encode_us target_delay_us * 1.2) { current_quality max(30, current_quality - 5); // 降质 } else if (encode_us target_delay_us * 0.8) { current_quality min(95, current_quality 3); // 升质 } } // 在 encoder_thread 中编码后调用 uint64_t start get_time_us(); write_jpeg_from_memory(..., current_quality); uint64_t end get_time_us(); adjust_quality_based_on_time(end - start);改造说明这是一个极简的 PID 控制思想。current_quality作为状态变量在多线程中需加锁pthread_mutex_t但因更新频率低每帧一次锁争用可忽略。5.4 改造点四集成到 C 工程暴露 C 接口若你的主工程是 C需防止 name mangling// yuv2jpg_wrapper.h extern C { int write_jpeg_from_memory(unsigned char *yuv_data, const char *jpg_filename, int width, int height, int quality); } // C 类封装 class Yuv2JpegEncoder { private: int m_quality; public: Yuv2JpegEncoder(int quality 95) : m_quality(quality) {} int encode(const uint8_t* yuv, int w, int h, const std::string path) { return write_jpeg_from_memory(const_castuint8_t*(yuv), path.c_str(), w, h, m_quality); } };改造说明extern C是 C 调用 C 函数的必备语法。const_cast仅用于兼容实际write_jpeg_from_memory()不修改yuv_data。从那以后我每次集成图像编码模块都强制走一遍「YUV 格式确认 → 内存布局验证 → 单帧文件输出 → 内存 buffer 接入 → 多线程压力测试」这五步。少走任何一步都会在联调时花 3 天去 debug 一个fread的字节数错误。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?