如何使用 libjpeg 将 YUYV 原始数据压缩为 JPEG?

Posted

技术标签:

【中文标题】如何使用 libjpeg 将 YUYV 原始数据压缩为 JPEG?【英文标题】:How to compress YUYV raw data to JPEG using libjpeg? 【发布时间】:2013-04-29 17:43:21 【问题描述】:

我正在寻找如何使用libjpeg 库将 YUYV 格式帧保存到 JPEG 文件的示例。

【问题讨论】:

请描述失真。缩放、垃圾、一致的不正确颜色重新分配。?也许尝试在这里或 imgur 发布您的输入和输出图像。 这是两个图像示例(第一个示例通过 Cheese 网络摄像头展位拍摄,第二个示例通过 libjpeg 从流中创建):imgur.com/Vg0tai6,iXQ6eMv YUYV 必须先转换为 RGB,然后才能将其写入 JPEG。如果 YUYV 与 YUV422 相同,您可能会发现这个答案很有用:***.com/a/7959522/5987 马克,谢谢你的评论。我认为libjpeg允许直接从yuyv422保存jpeg图像。 【参考方案1】:

在典型的计算机 API 中,“YUV”实际上表示 YCbCr,而“YUYV”表示“YCbCr 4:2:2”,存储为 Y0、Cb01、Y1、Cr01、Y2 ...

因此,如果您有一个“YUV”图像,您可以使用 JCS_YCbCr 颜色空间将其保存到 libjpeg。

当您有 422 图像 (YUYV) 时,您必须在将扫描线写入 libjpeg 之前将 Cb/Cr 值复制到需要它们的两个像素。因此,这个写入循环将为您完成:

// "base" is an unsigned char const * with the YUYV data
// jrow is a libjpeg row of samples array of 1 row pointer
cinfo.image_width = width & -1; 
cinfo.image_height = height & -1; 
cinfo.input_components = 3; 
cinfo.in_color_space = JCS_YCbCr; 
jpeg_set_defaults(&cinfo); 
jpeg_set_quality(&cinfo, 92, TRUE); 
jpeg_start_compress(&cinfo, TRUE); 
unsigned char *buf = new unsigned char[width * 3]; 
while (cinfo.next_scanline < height)  
    for (int i = 0; i < cinfo.image_width; i += 2)  
        buf[i*3] = base[i*2]; 
        buf[i*3+1] = base[i*2+1]; 
        buf[i*3+2] = base[i*2+3]; 
        buf[i*3+3] = base[i*2+2]; 
        buf[i*3+4] = base[i*2+1]; 
        buf[i*3+5] = base[i*2+3]; 
     
    jrow[0] = buf; 
    base += width * 2; 
    jpeg_write_scanlines(&cinfo, jrow, 1); 

jpeg_finish_compress(&cinfo);
delete[] buf;

如果您的错误或写入函数可以抛出 /longjmp,请使用您最喜欢的 auto-ptr 以避免泄漏“buf”。

直接将YCbCr提供给libjpeg比转成RGB更可取,因为它会直接以那种格式存储,从而节省了大量的转换工作。当图像来自网络摄像头或其他视频源时,通常将其以某种 YCbCr(例如 YUYV)格式获取也是最有效的。

最后,“U”和“V”在模拟分量视频中的含义略有不同,因此在真正表示 YCbCr 的计算机 API 中 YUV 的命名非常令人困惑。

【讨论】:

【参考方案2】:

libjpeg 还有一个原始数据模式,您可以直接提供原始的下采样数据(这几乎是您在 YUYV 格式中所拥有的)。这比复制 UV 值只是让 libjpeg 在内部再次缩小它们更有效。

为此,您可以使用jpeg_write_raw_data 而不是jpeg_write_scanlines,默认情况下它一次将处理16 条扫描线。默认情况下,JPEG 期望 U 和 V 平面被 2 倍下采样。 YUYV 格式已经对水平维度进行了下采样,但没有对垂直维度进行下采样,所以我每隔一个扫描线跳过 U 和 V。

初始化:

cinfo.image_width = /* width in pixels */;
cinfo.image_height = /* height in pixels */;
cinfo.input_components = 3;
cinfo.in_color_space = JCS_YCbCr;
jpeg_set_defaults(&cinfo);

cinfo.raw_data_in = true;

JSAMPLE y_plane[16][cinfo.image_width];
JSAMPLE u_plane[8][cinfo.image_width / 2];
JSAMPLE v_plane[8][cinfo.image_width / 2];

JSAMPROW y_rows[16];
JSAMPROW u_rows[8];
JSAMPROW v_rows[8];

for (int i = 0; i < 16; ++i)

    y_rows[i] = &y_plane[i][0];


for (int i = 0; i < 8; ++i)

    u_rows[i] = &u_plane[i][0];


for (int i = 0; i < 8; ++i)

    v_rows[i] = &v_plane[i][0];


JSAMPARRAY rows[]  y_rows, u_rows, v_rows ;

压缩:

jpeg_start_compress(&cinfo, true);

while (cinfo.next_scanline < cinfo.image_height)

    for (JDIMENSION i = 0; i < 16; ++i)
    
        auto offset = (cinfo.next_scanline + i) * cinfo.image_width * 2;
        for (JDIMENSION j = 0; j < cinfo.image_width; j += 2)
        
            y_plane[i][j] = image.data[offset + j * 2 + 0];
            y_plane[i][j + 1] = image.data[offset + j * 2 + 2];

            if (i % 2 == 0)
            
                u_plane[i / 2][j / 2] = image_data[offset + j * 2 + 1];
                v_plane[i / 2][j / 2] = image_data[offset + j * 2 + 3];
            
        
    

    jpeg_write_raw_data(&cinfo, rows, 16);


jpeg_finish_compress(&cinfo);

与@JonWatte 的回答中的方法相比,我使用这种方法可以减少大约 33% 的压缩时间。但是,此解决方案并不适合所有人。一些警告:

您只能压缩尺寸为 8 倍数的图像。如果您有不同尺寸的图像,则必须编写代码以填充边缘。但是,如果您是从相机获取图像,则很可能是这种方式。 由于我只是跳过交替扫描线的颜色值而不是像平均它们这样更花哨的东西,因此质量有些受损。不过,对于我的应用来说,速度比质量更重要。 现在的写入方式会在堆栈上分配大量内存。这对我来说是可以接受的,因为我的图像很小 (640x480) 并且有足够的可用内存。

libjpeg-turbo 的文档:https://raw.githubusercontent.com/libjpeg-turbo/libjpeg-turbo/master/libjpeg.txt

【讨论】:

以上是关于如何使用 libjpeg 将 YUYV 原始数据压缩为 JPEG?的主要内容,如果未能解决你的问题,请参考以下文章

如何将 libjpeg-turbo Android NDK 添加为静态库

win10--vs2015--libjpeg--64位库的编译过程记录

[AssistantTool]_4_生成原始Color数据文件

[AssistantTool]_4_生成原始Color数据文件

如何将包(例如 libjpeg-dev)自动加载到我的 Elastic Beanstalk 应用程序?

隔行扫描 YUYV 到灰度的霓虹灯优化