使用 C++、libpng 和 OpenMP 并行创建 PNG 文件

Parallelization of PNG file creation with C++, libpng and OpenMP

本文关键字:并行 创建 PNG 文件 OpenMP C++ libpng 使用      更新时间:2023-10-16

我目前正在尝试在基于 libpng 的 C++ 中实现一个 PNG 编码器,该编码器使用 OpenMP 来加速压缩过程。该工具已经能够从各种图像格式生成PNG文件。我将完整的源代码上传到 pastebin.com 以便您可以看到我到目前为止所做的工作:http://pastebin.com/8wiFzcgV

目前为止,一切都好!现在,我的问题是找到一种方法来并行生成包含压缩图像数据的IDAT块。通常,libpng 函数png_write_row在 for 循环中被调用,该循环带有指向包含有关 PNG 文件的所有信息的结构的指针和一个包含单个图像行的像素数据的行指针。

(Pastebin 文件中的第 114-117 行)

//Loop through image
for (i = 0, rp = info_ptr->row_pointers; i < png_ptr->height; i++, rp++) {
    png_write_row(png_ptr, *rp);
}

然后,Libpng 压缩一行又一行,并用压缩的数据填充内部缓冲区。缓冲区已满后,压缩数据将在 IDAT 块中刷新到图像文件。

我的方法是将图像分成多个部分,让一个线程压缩第 1 行到 10,另一个线程压缩第 11 行到 20,依此类推。但是由于libpng使用内部缓冲区,因此并不像我最初想象的那么容易:)我必须以某种方式让libpng将压缩数据写入每个线程的单独缓冲区。之后,我需要一种方法以正确的顺序连接缓冲区,以便我可以将它们一起写入输出图像文件。

那么,有人知道我如何使用 OpenMP 和对 libpng 进行一些调整吗?谢谢!

这太

长了,无法发表评论,但也不是真正的答案——

我不确定您可以在不修改libpng(或编写自己的编码器)的情况下执行此操作。无论如何,如果您了解 PNG 压缩是如何实现的,这将有所帮助:

在高级别,图像是一组像素行(通常为 32 位值,表示 RGBA 元组)。

每一行都可以独立地应用一个过滤器——过滤器的唯一目的是使该行更"可压缩"。例如,"sub"过滤器使每个像素的值与其左侧的值之间的差异。乍一看,这种增量编码可能看起来很愚蠢,但如果相邻像素之间的颜色相似(往往是这种情况),那么无论它们表示的实际颜色如何,结果值都非常小。压缩此类数据更容易,因为它的重复性要大得多。

往下看,图像数据可以看作是字节流(行不再彼此区分)。这些字节被压缩,产生另一个字节流。压缩数据被任意分解为多个段(任何你想要的!),每个段写入一个IDAT块(每个块都有一点簿记开销,包括CRC校验和)。

最低级别将我们带到有趣的部分,即压缩步骤本身。PNG 格式使用 zlib 压缩数据格式。zlib 本身只是一个包装器(有更多的簿记,包括 Adler-32 校验和)围绕真正的压缩数据格式,放气(zip 文件也使用它)。deflate 支持两种压缩技术:霍夫曼编码(根据字符串中每个不同字节出现的频率,将表示某个字节字符串所需的位数减少到最佳数字)和 LZ77 编码(允许引用已经发生的重复字符串,而不是写入输出两次)。

并行压缩压缩

的棘手之处在于,通常,压缩输入流的一部分要求前一部分也可用,以防需要引用。但是,就像PNG可以有多个IDAT块一样,deflate被分解为多个"块"。一个块中的数据可以引用另一个块中以前编码的数据,但它不必这样做(当然,如果不这样做,它可能会影响压缩率)。

因此,并行放气的一般策略是将输入分解为多个部分(以便压缩比保持高),将每个部分压缩成一系列块,然后将块粘合在一起(这实际上很棘手,因为块并不总是以字节边界结束 - 但你可以放一个空的非压缩块(类型 00), 这将与字节边界对齐,在部分之间)。然而,这并非微不足道,并且需要控制最低级别的压缩(手动创建放气块),创建跨越所有块的正确 zlib 包装器,并将所有这些填充到 IDAT 块中。

如果你想使用你自己的实现,我建议阅读我自己的zlib/deflate实现(以及我如何使用它),这是我专门为压缩PNG而创建的(它是用Haxe for Flash编写的,但应该相对容易移植到C++)。由于Flash是单线程的,我不做任何并行化,但我确实将编码分成几乎独立的部分("虚拟",因为部分之间保留了小数字节状态)在多个帧上,这相当于大致相同的事情。

祝你好运!

我终于让它并行化压缩过程。正如卡梅隆在他的回答的评论中提到的,我不得不从 zstreams 中剥离 zlib 标头以组合它们。不需要剥离页脚,因为 zlib 提供了一个名为 Z_SYNC_FLUSH 的选项,可用于所有块(最后一个必须用 Z_FINISH 写入)以写入字节边界。因此,您可以稍后简单地连接流输出。最终,adler32 校验和必须在所有线程上计算,并复制到组合 zstream 的末尾。

如果您对结果感兴趣,可以在 https://github.com/anvio/png-parallel 找到完整的概念验证