DSP

图像放大算法

2019-07-13 20:51发布

1 引言


基于ARM体系的微处理器,特别是Intel XScale架构的微处理器,比如PXA255和最新的PXA270,以其高性能、低功耗等优点,已在嵌入式系统领域中得到广泛的应用。而目前PDA的屏幕越来越大,从QVGA的320×240分辨率渐渐提高到VGA的640×480分辨率。但是由于ARM处理器性能有限,一些多媒体和游戏程序只能在QVGA的分辨率下达到比较流畅的速度,但是在VGA的分辨率之下由于要渲染的像素升至4倍,帧速往往就下降到原来的1/4,速度慢得让人无法忍受,所以非常有必要采用一些特殊的方法来提高程序运行的速度。
本文提出一种能有效提高多媒体和游戏程序在VGA分辨率下执行速度的方法,就是先让程序渲染出QVGA分辨率的图像,然后再放大一倍,以只比QVGA分辨率渲染多一点点处理时间,达到VGA分辨率的效果。这样就需要一种快速且失真比较小的图像放大算法来达到非常快的处理时间和比较好的效果。本文详细分析了现有的多种图像放大算法,提出了适用于嵌入式系统的快速优质图像放大算法,并针对XScale架构进行大量的优化来提高速度,最后通过实例说明最终的执行效果。其中实例研究的软件平台为Arm Linux 2.4.19,硬件平台为基于Intel PXA255 的Sitsang开发板和基于PXA270 的Mainstone开发板。

2 图像放大算法


2.1 最邻近法(Nearest Neighbour)


以图像处理领域为例,理想图像是均匀分布在二维平面直角坐标系中的,任意给出一对坐标,就应该能得到一个对应颜 {MOD}值,然而实际上只能用离散的点阵信息来近似表现图像。
现在假设给定一对坐标(3.1,5.3),想要得到这个坐标对应的颜 {MOD}(假设是灰度图),那么最简单的方法是用四舍五入来得到距离该点最近的像素,即像素(3,5)的值来代替,这就是最邻近法。最邻近法显然是速度最快的一种方法,但也是效果最差的一种,如果用这个方法进行图像放大,那么在放大比例较大的情况下就会出现非常明显的“马赛克”现象。

2.2 双线性插值法(Bi-linear Interpolation)


对于上面的例子,更好的办法是把像素(3,5)和像素(4,6)的颜 {MOD}值按照一定的比例混合。原始坐标离哪个像素近,哪个像素的比例就大些,即混合的比例与离像素的距离成反比。
如图一所示,如果要求出点P处的颜 {MOD}值,首先让p00和p01混合求出P0处的颜 {MOD}值:(其中h0+h1=1,v0+v1=1)
P0 = p00×h0 + p01×h1                                                                                 (1)
然后再让p10和p11混合求出P1处的颜 {MOD}值:
P1 = p10×h0 + p11×h1                                                                                 (2)
最后让P0和P1混和求出P处的颜 {MOD}值
P   =  P0 ×v0 + P1 ×v1                                                                             (3)
采用双线性插值法之后颜 {MOD}的过渡比较平滑,效果也比最邻近法要好得多。

2.3        双立方插值法(Bi-cubic Interpolation)


但是双线性插值只采用了2×2=4个点进行计算,就是说颜 {MOD}值在图一所示的正方形内是线形变化的,而一般相邻正方形的这个线性斜率是不同的,这样在正方形的边缘部分颜 {MOD}就不是渐变,而会有个突变,而人眼对于这种突变往往十分敏感,当放大倍数比较大时就会看到明显的马赛克,这时可以采用效果更好的双立方插值。
双立方插值采用的B样条曲线插值,在每个维度上对于连续的4个点进行插值,这样在所有的点之间的斜率都是连续的,不会存在突变。如果是两维的情况在相邻的正方形之间的斜率也都是连续的,人眼就不会看到明显的马赛克了。
先来看一维的情况,这里采用了B样条插值,在4个点之间利用3次曲线进行拟合:
  虽然双立方插值可以得到最佳画质,但是它的计算复杂度实在太大了,即使采用了整数优化和查表法,也将耗去大量的CPU资源。
而在放大至原来2倍的情况下,双立方插值和双线性插值画质相近,这时双线性插值就有明显的速度优势,所以更加适合嵌入式环境。

2.4        其他图像放大算法


对于游戏程序来说,又有一些专门的放大算法,可以达到更加好的放大效果。比如:2xSaI,Supersai,Egale,Scale2x,hq2x,lq2x等。这些算法都是专门放大图像到原来大小两倍的算法,由于固定了放大倍率,所以都经过特殊的优化,效果往往比双线性插值和双立方插值要好。如图二所示,hq2x对于简单线条和轮廓比较清晰的图像就有非常好的效果:
hq2x采用了模式匹配的思想,原始图像的每个点放大后都扩展成4个点,在hq2x里面就是参考了这个点周围的八个点,就是说一共用9个点的颜 {MOD}值来确定放大后4个点的颜 {MOD}值。首先比较当前点和周围八个点的颜 {MOD}值的差别,根据差别的大小分成颜 {MOD}相近和颜 {MOD}差别较大两种类别,因为周围共有八个点,这样共有256种可能的排列组合。
对于每种组合,每种情况,都要确定如何根据原先的9个点的颜 {MOD}值混合成放大后4个点的颜 {MOD}值,可以事先做好这256种可能组合的混 {MOD}表,每项都存放了每种情况应该如何混 {MOD},放大时只要根据当前的情况查询一下混 {MOD}表,就知道应该如何混 {MOD}生成放大后4个点的颜 {MOD}值了。
这些算法对于线条轮廓比较清晰的卡通游戏会有非常好的放大效果,所以通常都使用在游戏模拟器中,比如任天堂模拟器,Gameboy Advanced模拟器等等。但是如果是一般的多媒体程序,比如视频的播放,由于模式比较复杂,难于统计出合适的混 {MOD}表,放大效果并不是很好,仅仅和双线性插值差不多,又由于算法复杂度太高,并且只能用于放大一倍的情况,所以使用的不是很多。

3 XScale架构下程序的优化


对比这些放大算法,由于双线性插值算法的通用性比较强,复杂度又低,所以最后采用双线性插值作为放大算法,下面对双线性插值算法进行优化:

3.1 定点数优化


把双线性插值算法的(1)、(2)、(3)式合起来就是:
P = (p00*h0+p01*h1)*v0 + (p10*h0+p11*h1)*v1                                           (7)
其中h0+h1=1,v0+v1=1,这个公式中共有6次浮点乘法,RGB需要分开算,这样一个像素点需要18次浮点乘法,这还不包括生成浮点坐标的时间。
由于XScale架构没有浮点运算单元(FPU),所有的浮点运算都由软件模拟实现,所以这段程序在XScale架构中运行得非常慢,需要进行大量的优化来提高程序的性能。
首先把浮点运算改成定点运算,采用Q16.16的定点数。即:
定点数 = (int)(浮点数*65536.0+0.5),
把v0,v1,h0,h1转成定点数为:V0,V1,H0,H1。式(7)就变为:
P = (((p00*H0+p01*H1)>>16)*V0 + ((p10*H0+ p11*H1)>>16)*V1) >>16      (8)
此外,V0,V1,H0,H1也不需要每次都计算坐标然后转换成16位定点数,可以一开始计算好后使用增量法,在相应的循环中递增等量值,这样也可以减少每次计算的时间。

3.2        查表法优化


在式(8)中还是有6次定点乘法,在XScale架构下乘法的操作需要花费1-5个时钟周期,而且乘法流水线是伪流水线,只有上一条乘法语句执行完了,下一条乘法语句才能进入流水线。
同时对这行代码编译后的汇编代码进行分析,发现数据先后关联性非常大,如果存在关联性,乘法流水线和主流水线就不能并行工作,必须等待乘法运算结束了主流水线才能继续工作。所以即使采取了重排序等手工汇编优化手段,这段代码的实际性能也并不是很高,所以最后采用查表法作更进一步的优化。
对于两个颜 {MOD}a,b和相应的归一化距离v构建表:
LinearTable[a][b][v]=(a*v + b*(255-v))>>8                                                  (9)
其中a,b为6位(由于使用的是RGB565,采用最高的6位),v使用8位的精度(经实践,8位的精度已经足够了),这样一个表所占内存为:6×6×8=1MB
对于多媒体程序来说多耗1M的内存是完全可以接受的。这样,一次双线性插值的开销减少到3次查表:
P0 = LinearTable [p00][p01][H0];
P1 = LinearTable [p10][p11][H0];                                                             (10)
P   = LinearTable [P0][P1][V0];

3.3 对于放大一倍情况的优化


特别的对于放大一倍的情况,算法简化了很多,式(7)变为:
P =  (p00 +p01 + p10 + p11)>>2                                                            (11)
这样算法中只有加法和位移,计算复杂度小了很多。
但是对于一个象素的计算要分别计算RGB三个分量,这时可以采用重新排列数据结构的方法来提高速度。
原来是16bit的RGB565:   0x0000 0000 0000 0000 RRRR RGGG GGGB BBBB
现在分开存放:           0x0000 0GGG GGG0 0000 RRRR R000 000B BBBB
这样一次就能同时处理RGB三种颜 {MOD},相当于提高速度3倍。

3.4 基于XScale架构的优化


在XScale架构中使用了很多PC上所使用的先进技术来提高程序的速度,下面对双线性插值算法进行进一步的优化。
现在计算的瓶颈就是内存访问了,由于图像数据比较大,远大于XScale的32K数据缓存,所以几乎每次都要从SDRAM中取数据,在主频为624MHz的PXA270中首先需要3个总线周期CAS延迟,然后可以连续取到8个32bit数据,正好构成cache的一行,这样一共11个总线周期(66个内核周期)仅可取到8个32bit数据,RGB565的象素点都是16bit的,所以平均66个周期可以取到16个数据。
在式(11)中的4个数据平均需要约16个时钟周期才能全部取到,但是整个式子的计算只要5个时钟周期就可以完成,这样CPU的大部分时间都处于等待数据的状态,显然如何提高内存读写效率成了提高程序运行速度的关键。
首先可以采用PLD命令预取数据,在计算当前像素点前就用PLD命令预取下一次要计算的像素点,然后在计算当前点的同时CPU就会把下一次要计算的像素点的数据预取到数据缓存中,这样,到了下一次计算时可以直接从数据缓存中取数据,这样可以保证CPU的流水线效率最大。
然后再针对XScale的流水线进行指令重排序。把arm-linux-gcc编译出来的代码反汇编,对汇编代码进行分析,删除、合并部分冗余代码,并将ldr指令前后不相关的指令进行略微的重排序来防止流水线的停顿。这样可以更进一步提高程序的性能。
最后还可以利用PXA270 所特有的Wireless MMX指令集来优化,可以同时处理64bit的数据,这样还能再提高一倍的速度。

4 性能测试


下面对各种放大算法放大一帧图像(从QVGA放大成VGA)所花的时间进行比较:
假设程序渲染QVGA分辨率的图像需要30ms的时间,这样程序跑在QVGA分辨率下可以达到33.3帧的速度,画面非常流畅。而渲染VGA分辨率的时间是QVGA的4倍,需要120ms,这样速度仅有8.3帧了,这时画面很不流畅。
从图三可以看出优化前的双线性插值法速度是相当慢的,即使程序只执行放大算法,一秒也只能渲染12帧图像,加上渲染QVGA的图像所需的30ms,程序的速度和直接渲染VGA分辨率差不多,而画质肯定不如直接渲染的,这样采用放大算法就完全没有意义了。而经过上述一系列优化之后,双线性插值只需17.3ms就能完成一帧图像的放大,这样渲染QVGA分辨率的图像加上放大只需47.3ms,可以达到比较流畅的21.1帧的速度。即使是效果最差、速度最快的最邻近法也需要14.4ms,而采用双线性插值可以用稍多一点的放大时间来达到好得多的图像效果。
hq2x算法非常耗时,需要131.5ms来放大,虽然可以达到非常好的放大效果,但由于速度问题而不能应用到实际程序中。

5 总结


经过对各种图像放大算法的研究和分析,并且结合实际应用程序的编写和测试,最终找到一种适合嵌入式系统,特别是适合XScale架构的快速优质的图像放大算法。经过大量代码优化之后,使其放大速度接近直接数据拷贝,并在实际应用程序中达到很好的效果。