使用 C++、libpng 和 OpenMP 并行创建 PNG 文件
Posted
技术标签:
【中文标题】使用 C++、libpng 和 OpenMP 并行创建 PNG 文件【英文标题】:Parallelization of PNG file creation with C++, libpng and OpenMP 【发布时间】:2012-06-05 08:02:48 【问题描述】:我目前正在尝试在 C++ 中实现基于 libpng 的 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 进行一些调整吗?非常感谢!
【问题讨论】:
【参考方案1】:这对于评论来说太长了,但也不是真正的答案--
我不确定您是否可以在不修改 libpng(或编写自己的编码器)的情况下做到这一点。无论如何,如果您了解 PNG 压缩的实现方式,将会有所帮助:
在高层次上,图像是一组像素行(通常是表示 RGBA 元组的 32 位值)。
每一行都可以独立地应用一个filter——过滤器的唯一目的是使该行更“可压缩”。例如,“子”过滤器使每个像素的值与其左侧的值之间的差异。乍一看,这种 delta 编码可能看起来很愚蠢,但如果相邻像素之间的颜色相似(往往是这种情况),那么无论它们代表的实际颜色如何,结果值都非常小。压缩此类数据更容易,因为它的重复性更高。
再往下看,图像数据可以看作是字节流(行不再相互区分)。这些字节被压缩,产生另一个字节流。压缩后的数据被任意分割成段(你想要的任何地方!),每个段写入一个 IDAT 块(每个块还有一点记账开销,包括 CRC 校验和)。
最低级别将我们带到有趣的部分,即压缩步骤本身。 PNG 格式使用zlib 压缩数据格式。 zlib 本身只是真正压缩数据格式deflate(zip 文件也使用它)的包装器(有更多的簿记,包括 Adler-32 校验和)。 deflate 支持两种压缩技术:霍夫曼编码(考虑到字符串中每个不同字节出现的频率,它将表示某个字节串所需的位数减少到最佳数量)和 LZ77 编码(它允许重复的字符串已经发生被引用而不是写入输出两次)。
并行化 deflate 压缩的棘手之处在于,一般来说,压缩输入流的一部分需要前一部分也可用,以防需要引用。 但是,就像 PNG 可以有多个 IDAT 块一样,deflate 被分解为多个“块”。一个块中的数据可以引用另一个块中先前编码的数据,但它没有有(当然,如果没有,它可能会影响压缩率)。
因此,并行化 deflate 的一般策略是将输入分成多个 large 部分(以便压缩比保持较高),将每个部分压缩成一系列块,然后将块在一起(这实际上很棘手,因为块并不总是在字节边界上结束 - 但您可以放置一个空的非压缩块(类型 00),它将与字节边界对齐,在部分之间)。然而,这并非易事,需要控制最低级别的压缩(手动创建 deflate 块),创建跨越所有块的适当 zlib 包装器,并将所有这些填充到 IDAT 块中。
如果您想使用自己的实现,我建议您阅读我专门为压缩 PNG 创建的 my own zlib/deflate implementation(和 how I use it)(它是用 Haxe for Flash 编写的,但应该相对容易移植到 C++ )。由于 Flash 是单线程的,因此我不进行任何并行化,但我确实将编码拆分为多个帧上几乎独立的部分(“实际上”是因为在部分之间保留了小数字节状态),这在很大程度上相当于同样的事情。
祝你好运!
【讨论】:
好吧,我想并行化放气压缩有点太复杂/太费时了。但是并行化不能发生在更高的层次上吗?如果我将图像分成多个部分,让 libpng 为每个部分生成 IDAT 块,然后将它们粘合在一起,那么 PNG 查看器会不会有任何问题? @Pascal:试试看! :-) 但我认为它不会起作用,因为 IDAT 块中的数据不会是一个压缩的 zlib 流拆分(如预期的那样),而是连接了几个 zlib 流。话虽如此,您可能可以从每个部分中剥离 zlib 页眉和页脚,并为整个数据创建自己的 zlib 页眉和页脚。您需要结合 Adler-32 校验和,但我认为这确实可行!虽然您需要在压缩数据进入 IDAT 块之前访问它,但我不确定如何使用 libpng...【参考方案2】:我终于得到了它来并行化压缩过程。 正如卡梅隆在对他的回答的评论中提到的那样,我必须从 zstreams 中剥离 zlib 标头以将它们组合起来。不需要剥离页脚,因为 zlib 提供了一个名为 Z_SYNC_FLUSH 的选项,该选项可用于所有块(除了必须用 Z_FINISH 写入的最后一个块)来写入字节边界。因此,您可以在之后简单地连接流输出。最终,必须在所有线程上计算 adler32 校验和并将其复制到组合 zstream 的末尾。
如果您对结果感兴趣,可以在 https://github.com/anvio/png-parallel 找到完整的概念证明
【讨论】:
以上是关于使用 C++、libpng 和 OpenMP 并行创建 PNG 文件的主要内容,如果未能解决你的问题,请参考以下文章
C++ Armadillo 和 OpenMp:外积求和的并行化 - 定义 Armadillo 矩阵的约简