图像处理的算法复杂度通常都比较高,计算也相应比较耗时。利用CPU多线程处理能力可以大幅度加快计算速度。但是,为了保证多线程处理的结果和单线程处理的结果完全相同,图像的多线程计算有一些需要特别考虑的地方。
基本思路:为了能让多个线程同时并行处理,那么各自处理的数据不能有交集,这很好理解。那么基本思路是将一副图像分成多个子块,每个子块数据肯定是没有交集的,每个线程对一个子块数据进行处理,完成后将所有子块处理结果合成最终图像。
首先,每个子块的大小当然是必须考虑的问题。通常当应用进行一个较长时间的操作,应该用合适的方式告知用户。既然我们把图像分子块处理,如果单个子块处理时间很短,那么每当有一个子块的数据处理完成,我们就可以立即把它相应的处理结果展示给用户。用户就会看到这个图像各个部分的处理结果不断展示出来,直至整个图像完成。这样某种程度上用这种方式就是在告知用户正在处理进行中,避免为了把整个图像处理完成,用户需要等待太长时间。从这个角度来说,如果子块尺寸取的太大,每个子块计算时间肯定相应地加长,对于快速显示部分处理结果给用户是不利的。但是如果子块太小,子块总数就会增加,肯定会增加线程开销和其他一些开销(分割图像,分配子块数据等等),对于总的计算时间是不利的。这是一个权衡问题,可以根据具体情况确定。
另外,很多图像处理都要考虑像素领域范围的信息,因此对于每个子块的处理不能仅仅使用这个子块的内容。具体地,对于靠近子块边缘的像素,还要把子块外的部分像素信息考虑进来,加入计算,才能保证相应像素的处理结果是正确的。准确来说,如果领域半径为r(对方形或圆形领域来说,其他领域可做相应调整),那么子块处理所需要的所有数据是子块四周向外扩展r像素的范围。
CRect rect1, rect2;
rect1.CopyRect(pRect[i]);
rect1.InflateRect(extend, extend);
rect2.CopyRect(pRect[i]);
rect2.MoveToXY(extend, extend);
if (rect1.top < 0)
{
rect2.OffsetRect(0, rect1.top);
rect1.top = 0;
}
if (rect1.left < 0)
{
rect2.OffsetRect(rect1.left, 0);
rect1.left = 0;
}
if (rect1.bottom > Height) rect1.bottom = Height;
if (rect1.right > Width) rect1.right = Width;
代码中extend就是子块要向四周扩张的大小,其实就是领域半径r。pRect[i]是分割的第i个子块的大小。Height和Width是原图的高宽,扩展子块自然不能超过原图的尺寸。那么最后rect1就是计算所需要的数据在原图中所在的领域范围,应用原图的尺寸对它进行了限制。由于我是把每个子块当成一个新的图像进行处理,rect2就是新图像中子块处理结果所在的位置,用它来合成最终图像。
最后关于线程具体创建销毁,资源分配与回收,线程同步和通信,不做具体讨论。只讨论一下在这里多线程如何协调工作的问题。由于计算子块的线程只负责处理子块,还需要有人来做分割子块,分配数据给子块计算线程等等工作。本来应该画流程图的,实在懒得画了,这里简单描述一下几个线程如何协调工作的,其实也很简单。界面线程A,处理和用户之间的交互,接受用户命令,发送计算消息给线程B。计算协调线程B接受A的消息,分割子块,分配子块数据,创建子块计算线程Ci。子块计算线程Ci负责子块计算,发送处理结果(成功或失败)消息给线程B或者A。界面线程A收到子块完成消息,可以立即显示子块处理结果,当然也可以什么都不做,等到所有子块处理完再显示。协调线程B收到第i个子块完成消息,回收分配给线程Ci的资源,销毁Ci。如果所有的Ci完成了工作,B发送图像处理完成的消息给A,A可以接着做后续的工作。这里单独用了一个线程B来做子块计算协调的工作,感觉这样比较清晰一些。当然也可以让界面线程A来做这个工作,协调的工作量也不是很大,这样就可以不需要B线程。
单线程和多线程处理时间对比
多线程处理速度肯定不能简单地是单线程处理速度的N倍,这只是理想状况。由于很多额外工作(线程开销,准备每个线程的数据,处理结果的合成,线程间同步,图像子块结合部的部分重复计算),多线程是不可能达到理想状况的。下表列出了一副2400x1350大小的24bit图像分成了12个子块,分别在一台I5 4300U(双核四线程)笔记本上和一台I5 6500(四核四线程)台式机上,处理高斯模糊的大概的平均耗时。高斯模糊算法是简单的行列方向两次一维计算,半径取50。在我的测试中,还实时显示了分块的处理结果,速度上可能要更慢一点。
亮度通道 | RGB通道 | |||||
单线程 | 多线程 | 提速 | 单线程 | 多线程 | 提速 | |
I5 4300U(双核四线程) | 1100毫秒 | 550毫秒 | 50% | 3060毫秒 | 1250毫秒 | 59% |
I5 6500(四核四线程) | 670毫秒 | 240毫秒 | 64% | 1850毫秒 | 560毫秒 | 69.7% |
理想状况下四线程耗时最多能减少75%,实际上肯定达不到。在双核四线程平台上,对亮度通道处理,多线程比单线程耗时减少了一半(50%)。对RGB通道,多线程比单线程耗时减少了大概59%。在四核四线程平台上,多线程耗时在亮度通道和RGB通道处理时,分别减少了64%和69%。可以看到,多线程处理的加速效果还是相当明显的。话说牙膏厂超线程的效果还是挺惊人的,不然在双核CPU上耗时减少是不可能超过50%的。当然物理内核的数量就更重要了。
而且还可以看到一个现象,在单线程处理下,RGB三通道的处理耗时是亮度通道处理耗时3倍略少,约为2.8倍(亮度通道还包括一些在RGB和亮度之间转换的额外计算量)。而在多线程下,RGB三通道的处理时间大大小于亮度一个通道处理时间的3倍,约为2.38倍。相对于单线程,节省的时间更多了。这是因为RGB的处理也是在子块的一个线程中处理的,并没有增加新的线程开销。因此线程开销也是必须考虑的一个因素,不可忽略。