几个图像缩放算法的比较


前段时间由于项目的需求,需要实现图像的缩放功能,期间查找了不少关于图像缩放算法的资料,现把自己的心得整理一下。

由于研究生期间没有选修过图像处理方面的课程,所以对图像缩放的原理可谓一窍不通,当时开始编写代码的时候简直就是一头雾水。而且网上虽然介绍图像处理的代码很多,但涉及图像缩放的代码却很少,因为很多软件都直接使用了windows的GDI函数库的API函数:StretchBlt,或者VCL中TCanvas类的StretchDraw。无奈这两个函数都是直接对BMP图像进行缩放,而且StretchBlt是在CDC里面调用的,结果只是在显示的时候对图像进行缩放,不能够进行缩放的存储。那些天在GDI和GDIPLUS摸索了半天,都找不到合适的函数,某天却迸出个想法来:图像放大不就是把每个象素点再多弄几个出来,而缩小不就是去掉里面一些象素点。所以就按照自己的想法写了一个比较粗糙的放大函数:

BYTE *src,*dst,*ptr,*buffer,*next;
   for(int i=0,n=0; i < this->Height(); i++,n=n+rate)
   {   
    src = this->GetLinePtr(i);
    dst = tempdib->GetLinePtr(n);
    ptr = dst;
    for(int j=0; j < this->Width(); j++,ptr=ptr+3*rate)
    {
     memcpy(ptr,src+j*3,3);
     for(int m=1;m<rate;m++)
       memcpy(ptr+m*3,ptr,3);
    }
    for(int m=n+1;m<n+rate;m++)
    {
     
     buffer = dst;
     next = tempdib->GetLinePtr(m);
     ptr = next;
     memcpy(ptr,buffer,dstwidth*3);
     
    }
   }

这段代码的效果比较粗糙,但处理的办法比较有意思。首先是读取一行的图像数据,然后在每一行循环读取一个象素的RGB值并复制到新图像的内存空间,然后根据放大的比例再作一次循环,把这个RGB值按照比例复制进内存空间。当进行完一行的处理后,在新图像的内存空间进行一次循环处理,把这行数据按照比例复制给下面几行。这样就通过象素点的复制实现了图片的放大。不过放大的效果不是特别好,图像列方向上会出现很多的毛刺,放大4倍的话图像就很模糊了。

所以还是重新去查找资料,结果在网上搜到一篇不错的文章——用线性插值算法实现图像缩放。看了文章,才发现我原先的办法还真不是一般的原始,不过思路还跟GDI里面的StretchBlt差不多。StretchBlt采用的方法在图像处理领域称为最近邻域法,其基本原理就是先取出原图的相邻四个点,然后把新位置的点跟这四个点的位置做比较,把最近一个点的RGB值赋给新位置的点。所以在放大的时候,几乎就是像我那样把前一个点的象素赋给新位置的点。这样处理的结果就是导致图像不够平滑,因为点与点之间是一个过渡的过程,不是简单的复制,稍微好点的办法就是把新点附近几个点的颜色值取平均再赋给这个点。这种方法在数值计算方法叫做线性插值。但那篇文章提供了一个更好的方法,叫做二维线性插值,其原理也是对附近的点取平均,但它对各个点的颜色值加上不同的权数,这个权数就是各个点距离这个点的位置。其计算方法如下:

P = n*b*PA + n * ( 1 – b )*PB + ( 1 – n ) * b * PC + ( 1 – n ) * ( 1 – b ) * PD

    其中:n为v(映射后相应点在源图像中的Y轴坐标,一般不是整数)下面最接近的行的Y轴坐标与v的差;同样b也类似,不过它是X轴坐标。PA—PD分别是(u,v)点周围最接近的四个(左上,右上,左下,右下)源图像点的颜色(用TCanvas的 Pixels属性)。P为(u,v)点的插值颜色,即(x,y)点的近似颜色。

不过这个公式用的是浮点运算,真的用程序来实现效率有点低,所以作者在这个基础上对它进行优化,并且用C++Builder把它实现。而我所需要做的,只是把它移植到.net里面,哈,其实就是ctrl+c、ctrl+v。

int sw = this->width, sh = this->height;
   int dw = tempdib->width, dh = tempdib->height;
   int B, N, x, y;
   BYTE *pLinePrev, *pLineNext;
   BYTE *pDest;
   BYTE *pA, *pB, *pC, *pD;
   for(int i=0;i<dh;i++)
   {
    pDest = (BYTE* )tempdib->GetLinePtr(i);
    y = i * sh / dh;
    N = dh - i * sh % dh;
    pLinePrev = (BYTE* )this->GetLinePtr(y++);
    pLineNext = (N == dh) ? pLinePrev : (BYTE* )this->GetLinePtr(y);
    for(int j=0;j<dw;j++)
    {
     x = j * sw /dw * 3;
     B = dw - j * sw % dw;
     pA = pLinePrev + x;
     pB = pA + 3;
     pC = pLineNext + x;
     pD = pC +3;
     if(B == dw)
     {
      pB = pA;
      pD = pC;
     }
     for(int k=0;k<3;k++)
     {
      *pDest++ = (BYTE)(int)((B * N * (*pA++ - *pB - *pC + *pD)
         + dw * N * *pB++ +dh * B * *pC++ + (dw * dh -
         dh * B - dw * N) * *pD++
         + dw *dh / 2)/(dw * dh));
     }
    }
   }

不过这段代码还不算是最好的,后来在天鼎买了一本《VC.NET图像编程》,认真学了图像处理的原理。前面使用的方法,在图像处理里面是叫做空间域的图像处理,还有一种更有效但算法比较复杂的频域处理,它首先把图像通过傅立叶变换等等转换为频域的数据,然后用各种滤波器对图像进行处理。后来通过google在codeproject上找到这种基于频域的缩放算法。作者把它命名为2_pass_scaling,或许这是目前能找到的最好的缩放算法,实在太牛B了。作者把接口写成一个template,然后根据不同的滤波器去调用不同的算法,只要修改template的实现,就能够得到不同的缩放效果。最后的代码就这么简单:

C2PassScale<CBilinearFilter> ScaleEngine;
    BYTE *psrc,*pdest;
    pNewRGB = new BYTE[(this->width)*this->height*3];
    pdest = pNewRGB;
    for(int i=0;i<this->height;i++)
    {
     psrc = this->GetLinePtr(i);
     for(int j=0;j<this->width;j++)
     {
      memcpy(pdest,psrc,3);
      pdest += 3;
      psrc += 3;
     }
    }
    pNewBitmap = (BYTE* )ScaleEngine.AllocAndScale((myRGB* )pNewRGB,this->width,this->height,dstwidth,dstheight);
    delete[] pNewRGB; 
   }
   BYTE *src,*dst,*ptr,*buffer,*next;
   ptr = (BYTE* )pNewBitmap;
   for(int i=0;i<tempdib->height;i++)
   {
    src = tempdib->GetLinePtr(i);
    for(int j=0;j<tempdib->width;j++,src+=3,ptr+=3)
     memcpy(src,ptr,3);
   }
   delete pNewBitmap;

首先是根据不同的滤波器从template生成一个类,我这里用的是二次线性插值,所以就是<CBilinearFilter>,其他的滤波器还有CBoxFilter、CGaussianFilter、CHammingFilter、CBlackmanFilter。然后就只要调用ScaleEngine.AllocAndScale,把图像象素数据传进去,它就能返回一个新的内存空间,里面就是缩放后的图像数据。代码里面关于滤波器的代码比较复杂,看了半天还是看不懂,不过,好用就行 :)