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

11

我目前正在尝试在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进行一些调整的想法?非常感谢!

2个回答

13

这段内容太长了,适合作为评论,但并不是一个真正的答案。

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

在高层次上,图像是一组像素行(通常是表示RGBA元组的32位值)。

每一行都可以独立地应用过滤器-- 过滤器的唯一目的就是使行更易于“压缩”。例如,“sub”过滤器将每个像素的值设置为其与左侧像素之间的差异。这种增量编码乍一看可能很愚蠢,但如果相邻像素之间的颜色相似(这往往是这种情况),则得到的值非常小,而与它们实际表示的颜色无关。这样的数据更容易压缩,因为它更具有重复性。

进入下一级,图像数据可以看作是字节流(行不再彼此区分)。这些字节被压缩,生成另一个字节流。压缩的数据被任意地分成段(你想在任何地方!),每个段写入一个IDAT块(以及每个块的一些记账开销,包括CRC校验和)。

最低层带给我们有趣的部分,即压缩步骤本身。 PNG格式使用zlib压缩数据格式。 zlib本身只是一个包装器(具有更多的记账,包括Adler-32校验和),它包装了真正的压缩数据格式deflate(zip文件也使用这种格式)。 deflate支持两种压缩技术:霍夫曼编码(将一些字节串所需的位数减少到给定字节在串中出现频率的最佳数量)和LZ77编码(让已经出现过的重复字符串被引用而不是写入输出两次)。

并行化deflate压缩的棘手之处在于,一般来说,压缩输入流的一部分需要前面的部分也可用以备引用。 然而,就像PNG可以拥有多个IDAT块一样,deflate被分成多个“块”。 一个块中的数据可以引用另一个块中先前编码的数据,但不一定要这样做(当然,如果不这样做,可能会影响压缩比)。
因此,对于并行化deflate的一般策略是将输入分成多个部分(以保持高压缩比),将每个部分压缩为一系列块,然后将这些块粘合在一起(这实际上很棘手,因为块不总是以字节边界结束-但你可以插入一个空的非压缩块(类型00),它将对齐到字节边界,在部分之间)。 然而,这并不是微不足道的,并且需要控制最低级别的压缩(手动创建deflate块),创建跨越所有块的适当zlib包装器,并将所有这些东西塞入IDAT块中。
如果你想使用自己的实现,我建议阅读我自己的zlib/deflate实现(以及我如何使用它),我专门为压缩PNG而创建它(用Haxe写成,适用于Flash,但应该相对容易移植到C++)。 由于Flash是单线程的,我没有进行任何并行化,但我将编码分成多个帧中的虚拟独立部分(“虚拟”是因为在部分之间保留了小数字节状态),这基本上就是同样的事情。
祝你好运!

好吧,我猜对于我来说并行压缩可能有点太复杂/耗时了。但是并行化可以在更高的层面上发生吗?如果我将图像分成多个部分,并让libpng为每个部分生成IDAT块,然后将它们粘合在一起,PNG查看器会有任何问题吗? - Pascal
@Pascal:试一下吧!:-) 但我认为这不会起作用,因为IDAT块中的数据不会是一个压缩的zlib流分割开来(如预期的那样),而是几个zlib流串联在一起。话虽如此,您可能可以从每个部分中剥离zlib头和尾,并为整个数据创建自己的zlib头和尾。您需要合并Adler-32校验和,但我认为那实际上会起作用!虽然您需要访问进入IDAT块之前的压缩数据,但我不确定如何使用libpng做到这一点... - Cameron

5
我最终成功将压缩过程并行化。如卡梅伦在他的答案评论中提到的那样,我需要从zstreams中去除zlib头部以将它们组合起来。不需要去除页脚,因为zlib提供了一个名为Z_SYNC_FLUSH的选项,可用于所有块(除了必须使用Z_FINISH写入的最后一个块)以写入字节边界。因此,您可以简单地将流输出连接在一起。最终,必须在所有线程上计算adler32校验和,并将其复制到组合的zstreams的末尾。
如果您对结果感兴趣,可以在https://github.com/anvio/png-parallel找到完整的概念证明。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接