多线程加速JPEG编码

2021-01-04 20:18:12

JPEG标准于1992年发布后,JPEG图像成为数字摄影的代名词,几乎在所有处理照片质量图像的应用程序中都使用JPEG图像。采用该标准之所以快速且几乎通用是因为它同时利用多种技术来减小压缩文件的大小。其中之一是对人类视觉系统的局限性的理解,哪些是重要的信息,哪些是不重要的信息,可以删除。

使用JPEG方法压缩图像需要几个步骤(请参见下面的图像)。首先,通常将其从RGB转换为YCbCR色彩空间。这是必要的原因是允许像素的二次采样。人眼对亮度的变化比对色度的变化更敏感。这样可以在保持亮度通道处于全分辨率的同时,对色度通道进行下采样。通过此步骤,图像可能会丢失其数据的50%,并且感觉到的降级非常小。然后将图像分为8x8 MCU或最小编码单位(视频编解码器的等效术语是“宏块”)。这些MCU是像素的正方形块,它们基于彼此的相似性进行压缩。每个MCU中的像素都通过离散余弦变换(DCT)从空间域转换到频域。该操作允许轻松去除高频信息(精细细节)以进一步压缩图像。去除的高频系数越多,文件越小,图像变得越模糊。这样可以有效地控制编码器使用的“ Q值”或压缩量。

该标准中包含的许多巧妙思想之一是使用相邻MCU之间的DC值(基本上是亮度)的相似性,通过仅压缩从一个到下一个的变化而不是编码整个值来进一步减小数据大小。上面在“ DPCM编码”块中对此进行了描述。这是一个好主意,但会带来一个小问题。通过使每个连续的MCU的DC值取决于前一个值的增量,这意味着如果数据中存在错误,则从那以后开始的MCU都是错误的。

更糟糕的是,对图像数据进行编码的压缩符号(上面显示为“霍夫曼编码”)是可变长度代码(VLC)。从那一点开始,单个错误的位可能会破坏数据。早在发明JPEG的“过去”时代,这是一个非常现实的问题,因为图像通常是通过通道(例如,声音调制解调器)传输的,而这些通道可能没有经过严格的纠错,或者存储在容易产生错误的介质(例如软盘)上错误。知道数据可能会出现错误,因此在标准中添加了一项功能,以减轻可能因不良数据而损坏的图像量。这样做的想法是定期将先前的DC值重置为0,迫使下一个MCU将其整个值编码为从0开始的增量。这意味着任何损坏的DC值只会影响直到下一个重启点的像素。

这是通过重新启动标记实现的。它们是2字节的标记,它们以固定的间隔(例如,每100个MCU)放置在MCU之间。如果发生数据损坏,可以很容易地在文件中向前扫描到下一个重新启动标记(JPEG标记始终位于字节边界,并以0xFF开头)。一旦找到下一个重启标记,由于已知每个重启标记之间的MCU数量,因此可以从该点开始对图像进行正确解码。

在发布JPEG标准后不久,计算机网络和存储设备就变得更加可靠,并集成了错误检测和纠正功能(例如TCP / IP)。数码相机中使用的固态存储卡相当可靠,并且重新启动标记“被遗忘了”了一阵子,因为它们使文件稍大了一点,却没有带来太大的好处。在此期间,计算机软件主要设计为在使用单个线程的单个处理器上运行。 JPEG图像由于使用可变长度代码和一串连续的增量值用于MCU而需要单次解码,这一事实并没有给软件带来任何问题,因为它被设计为可以单线程运行无论如何。

但是,在最近几年中,计算机和我们对JPEG图像的使用发生了巨大变化。几乎每个计算设备都具有多个CPU,并运行具有多个线程的操作系统(甚至是电话)。另一个变化是人们正在使用手机拍摄,编辑和查看数十亿张JPEG照片。每一代手机都会生成更大和更高分辨率的图像。对于处理大量照片的任何人来说,对它们进行编码和解码的时间变得越来越重要,因为所生成的新图像数量巨大且尺寸巨大。

自1970年代以来,计算机在几乎连续的轨迹上变得越来越强大。为了描述戈登·摩尔关于计算机每18个月晶体管数量将增加一倍的预测,人们在多年前创造了一个通用术语,用以描述硅工艺的持续改进,称为摩尔定律。对于晶体管数量而言,这基本上是正确的,但是由于硅的物理局限性以及功率和热量问题,计算机的最高速度已基本陷入僵局。由于过去几年中单个处理器的速度没有太大提高,因此重点已转移到使用许多处理器来通过并行工作更快地完成任务。在当今的计算环境中,将任务划分为多个部分并将它们分配给多个CPU很有好处。并非所有任务都可以划分,因为每个连续的部分都可能取决于前一个的结果。 JPEG编码和解码通常很难分为几部分,并且很难并行运行,因为每个连续的MCU取决于前一个MCU的方式以及可变长度代码的使用。

但是…重新启动标记的一个方便好处是VLC数据被重置为字节边界(在标记之后)并且MCU DC增量值也被重置。这意味着可以使用重新启动标记将JPEG编码和解码都分为多个线程。为了进行编码,可以将图像分为对称的条带,并且每个条带可以由不同的处理器编码。当每个处理器完成该任务后,可以使用重新启动标记将每个输出“粘合”在一起。对于解码,任务可以分布在与重新启动标记一样多的处理器上。唯一需要做的工作就是先向前扫描压缩数据以查找重启标记,因为每个压缩数据之间的大小各不相同,并且JPEG文件中没有“目录”来显示重启标记的位置。

对于多线程应用程序,性能很少会随所用CPU的数量而按1:1比例缩放(例如,将任务分成12个CPU内核上的12个线程并不意味着运行速度会快12倍)。管理线程会产生额外的开销,并且内存通常是处理器之间共享的单个实体。让我们对下图进行测试:

除其他技术外,在Optidash,我们结合了使用重新开始标记的思想,以极大地加速图像的解码和编码。上图是通过我们的测试工具之一在配备6核Intel i7处理器的2018年MacBook Pro 15英寸笔记本电脑上运行的。结果如下:

如上表所示,将JPEG编码分为多个部分并将它们分配给不同的CPU有一个可衡量的优势。根据任务的不同,在使用更多CPU时,速度很少会线性扩展。在这种情况下,大量内存的大量使用限制了多个CPU可以提高整体速度的收益。

void ThreadedWriteJPEG(各种JPEG参数,int numThreads){int slice; pthread_t tinfo; //设置完成计数器sliceRemaining = numThreads; //为每个切片启动一个线程,用于(切片= 0; slice< numThreads; slice ++){pthread_t tinfo; //使用“切片”结构来保存有关每个线程的工作的信息// //包括指向该像素带的起点的指针//以及要压缩<每个线程的工作的设置切片结构的行数pthread_create(& tinfo,NULL,JPEGBuffer,& slices [slice]); } //对于每个切片// //等待所有工作线程完成WaitForThreads(& sliceRemaining); //将切片合并为一个文件,然后将其写入WriteJPEGBuffer(slices,numThreads);}