前段时间接触了一些数字图像处理的问题,在1位师兄的指导下,在j2me平台,完成了一些基本的2D图像处理算法。就当是对这段知识做一下总结,决定把这些算法写出来,和各位朋友共同探讨。这篇文章先介绍图像放大缩小的实现,程序是以Nokia S40的机器为平台实现的。

1、实现图形缩放的基本思想:

图像的变形变换,简单的说就是把源图像每个点坐标通过变形运算转为目标图像相应点的新坐标,但是这样会导致一 个问题就是目标点的坐标通常不会是整数。所以我们在做放大变换时,需要计算生成没有被映射到的点;而在缩小变换时,需要删除一些点。这里我们采用最简单的 一种插值算法:“最近邻域法”。顾名思义,就是把非整数坐标作一个四舍五入,取最近的整数点。
看下面的一个图片放大的例子,左图为原始图像,右图为放大1倍的图像。里面的数字,表示所在像素的信息
      

2、对于图片像素的操作:

获取Image图片像素信息:
标准的midp1.0没有提供获取图片像素信息的函数,对于NOKIA的机器,我们可以采用Nokia SDK提供的API获取像素信息。具体程序如下:

g = image.getGraphics();
DirectGraphics dg = DirectUtils.getDirectGraphics(g);
dg.getPixels(short[] pixels, int offset, int scanlength, int x, int y,
             int width, int height, int format);

参数介绍:
short[] pixels: 用于接收像素信息的数组
int offset:这篇文章中的用到的地方,添0就可以了
int scanlength:添图片的宽度就行了
int x:添0
int y:添0
int width:图片宽度
int height:图片高度
int format:444,表示图形格式,好像Nokia S40的机器都是采用444格式表示RGB颜色的。就是红,绿,蓝各用4位表示,至于可以表示透明色ARGB的4444格式,应该是机器硬件实现的。
想具体了解Nokia SDK的信息,可以查看Nokia SDK的帮助文档。

使用像素信息数组生成Image图片:

image = Image.createImage(w, h);
g = image.getGraphics();
DirectGraphics dg = DirectUtils.getDirectGraphics(g);
dg.drawPixels(short[] pixels, boolean transparency, int offset, int scanlength,
              int x, int y, int width, int height, int manipulation, int format)

short[] pixels:像素信息数组
boolean transparency:是否包含alpha位信息
int offset:添 0
int scanlength:添图片的宽度就行了
int x:添 0
int y:添 0
int width:图片宽度
int height:图片高度
int manipulation:添 0
int format:444

下面开始介绍具体的算法,首先给出图像缩放的完整函数,然后对代码,分段进行解释

/*********************************
 * @todo 图片放大缩小
 * @param srcImg 原始图片
 * @param desW 变化后图片的宽
 * @param desH 变化后图片的高
 * @return 处理后的图片
 *********************************/
private Image ZoomImage(Image srcImg, int desW, int desH) {
    int srcW = srcImg.getWidth(); //原始图像宽
    int srcH = srcImg.getHeight(); //原始图像高

    short[] srcBuf = new short[srcW * srcH]; //原始图片像素信息缓存

    //srcBuf获取图片像素信息
    Image desImg = Image.createImage(srcW, srcH);
    if (srcImg.isMutable()) { /*如果是可变图像*/
        DirectUtils.getDirectGraphics(srcImg.getGraphics()).
                getPixels(srcBuf, 0, srcW, 0, 0, srcW, srcH, 444);
    } else { /*如果是非可变图像*/
        desImg.getGraphics().drawImage(srcImg, 0, 0, 0);
        DirectUtils.getDirectGraphics(desImg.getGraphics()).
                getPixels(srcBuf, 0, srcW, 0, 0, srcW, srcH, 444);
    }

    //计算插值表
    short[] tabY = new short[desH];
    short[] tabX = new short[desW];

    int sb = 0;
    int db = 0;
    int tems = 0;
    int temd = 0;
    int distance = srcH > desH ? srcH : desH;
    for (int i = 0; i <= distance; i) { /*垂直方向*/
        tabY[db] = (short) sb;
        tems = srcH;
        temd = desH;
        if (tems > distance) {
            tems -= distance;
            sb;
        }
        if (temd > distance) {
            temd -= distance;
            db;
        }
    }

    sb = 0;
    db = 0;
    tems = 0;
    temd = 0;
    distance = srcW > desW ? srcW : desW;
    for (int i = 0; i <= distance; i) { /*水平方向*/
        tabX[db] = (short) sb;
        tems = srcW;
        temd = desW;
        if (tems > distance) {
            tems -= distance;
            sb;
        }
        if (temd > distance) {
            temd -= distance;
            db;
        }
    }

    //生成放大缩小后图形像素buf
    short[] desBuf = new short[desW * desH];
    int dx = 0;
    int dy = 0;
    int sx = 0;
    int sy = 0;
    int oldy = -1;
    for (int i = 0; i < desH; i) {
        if (oldy == tabY[i]) {
            System.arraycopy(desBuf, dy - desW, desBuf, dy, desW);
        } else {
            dx = 0;
            for (int j = 0; j < desW; j) {
                desBuf[dy dx] = srcBuf[sy tabX[j]];
                dx;
            }
            sy = (tabY[i] - oldy) * srcW;
        }
        oldy = tabY[i];
        dy = desW;
    }

    //生成图片
    desImg = Image.createImage(desW, desH);
    DirectUtils.getDirectGraphics(desImg.getGraphics()).
            drawPixels(desBuf, true, 0, desW, 0, 0, desW, desH, 0, 444);
    return desImg;
}

首先看函数的头两句,很容易,就是获取原始图片的宽度和高度
    int srcW = srcImg.getWidth(); //原始图像宽
    int srcH = srcImg.getHeight(); //原始图像高
接下来一句我们要定义一个short型数组,作为获取原始图片像素信息的缓存
    short[] srcBuf = new short[srcW * srcH];

再下来一段,有的朋友可能会有些不明白,这里要解释一下。由于getPixels()这个函数,只能获取可变图像的像素信息,非可变图像,无法获取 像素信息。所以我们要用srcImg.isMutable() 来判断,原始图像是不是可变图像,然后分两种情况来处理。如果srcImg是可变图像,我们就直接用getPixels()来获取它的像素信息,并保存在 srcBuf里。如果srcImg不是可变图像,我们就需要把srcImage画到事先生成的可变图像desImg上,然后再获取desImg的像素信 息。

Image desImg = Image.createImage(srcW, srcH);
if (srcImg.isMutable()) { /*如果是可变图像*/
    DirectUtils.getDirectGraphics(srcImg.getGraphics()).
            getPixels(srcBuf, 0, srcW, 0, 0, srcW, srcH, 444);
} else { /*如果是非可变图像*/
    desImg.getGraphics().drawImage(srcImg, 0, 0, 0);
    DirectUtils.getDirectGraphics(desImg.getGraphics()).
            getPixels(srcBuf, 0, srcW, 0, 0, srcW, srcH, 444);
}

再往下就是缩放算法的重点:插值表的生成。插值表分水平差值表和垂直插值表,我们要分别生成原始图像矩阵的2种插值表,然后利用插值表生成放大缩小后的图像矩阵。由于这个内容比较抽象,很难用文字表述清楚,所以我们用实例进行介绍。
大家看下面这个水平的1*4的表格
-----------------
| 0 | 1 | 2 | 3 |
-----------------
如果要将这个表格放大成1*6的表格,放大的表格比原始表格多出了2个格子,我们只能对这多出来的2个格子进行插值,才能完成放大的操作。现在结合生成水平插值表的代码来完成这个过程。

distance = srcW>desW ? srcW : desW;
for (int i = 0; i <= distance; i) { /*水平方向*/
    tabX[db] = (short) sb;
    tems = srcW;
    temd = desW;
    if (tems > distance) {
        tems -= distance;
        sb;
    }
    if (temd > distance) {
        temd -= distance;
        db;
    }
}

很明显原始表格宽度srcW = 4;放大后的表格宽度desW = 6;所以distance = desW = 6
接下来进入for循环,我们一步步的演算其循环的过程
-----------------------------------------------
| i| tabX赋值操作| tems | temd| sb | db |
-----------------------------------------------
|0| tabX[0] = 0 |   4   |   6    | 0 | 0 |
-----------------------------------------------
|1| tabX[0] = 0 |   2   |   6    | 1 | 1 |
-----------------------------------------------
|2| tabX[0] = 1 |   6   |   6    | 1 | 2 |
-----------------------------------------------
|3| tabX[0] = 1 |   4   |   6    | 2 | 3 |
-----------------------------------------------
|4| tabX[0] = 2 |   2   |   6    | 3 | 4 |
-----------------------------------------------
|5| tabX[0] = 3 |   6   |   6    | 3 | 5 |
-----------------------------------------------
|6| tabX[0] = 3 |   4   |   6    | 4 | 6 |
-----------------------------------------------
有此得到放大后1*6的表格为下图所示。其中每一个单元格中的数字n表示这个单元格的内容,和原始表格中第n个单元格的内容一样。
--------------------------
| 0 | 1 | 1 | 2 | 3 | 3 |
--------------------------
例如,左图为原始表格,右图为放大的表格
---------------------      --------------------------------
| 红 | 绿 | 兰 | 紫 |      | 红 | 绿 | 绿 | 兰 | 紫 | 紫 |
---------------------      --------------------------------
同样,垂直方向的插值表我们也可以用相同的方法获得。

有了2个插值表,下面就可以生成放大和缩小后的图像了。

short[] desBuf = new short[desW*desH];
int dx = 0;
int dy = 0;
int sx = 0;
int sy = 0;
int oldy = -1;
for (int i = 0; i < desH; i) {
    if (oldy == tabY[i]) { /**********情况一**********/
        System.arraycopy(desBuf, dy - desW, desBuf, dy, desW);

    } else { /**********情况二**********/
        dx = 0;
        for (int j = 0; j < desW; j) {
            desBuf[dy dx] = srcBuf[sy tabX[j]];
            dx;
        }
        sy = (tabY[i] - oldy) * srcW;

    }
    oldy = tabY[i];
    dy = desW;
}

desBuf是用来保存放大缩小后的图像数据。例如我们把一个4*4像素的图像A放大成6*6的图像B,据前面的介绍我们可以生成2个插值表。tabX = {0,1,1,2,3,3},tabY = {0,1,1,2,3,3}。
在 循环中会判断是否oldy 等于 tabY[i],这个操作等同于tabY[i-1]是否等于tabY[i]。如果等于,表示图像B前一行已经生成的数据和即将要生成的第i行数据相同,则 只要执行System.arraycopy(desBuf, dy - desW, desBuf, dy, desW)把上一行的数据复制过来即可;如果不等,则需要对照水平插值表tabX生成这一行的数据。
算法演示过程如下:
----------------------------
| i|oldy|tabY[i]|运算情况|
----------------------------
|0|-1   |   0    | 情况2 |
----------------------------
|1| 0   |   1    | 情况2 |
----------------------------
|2| 1   |   1    | 情况1 |
----------------------------
|3| 1   |   2    | 情况2 |
----------------------------
|4| 2   |   3    | 情况2 |
----------------------------
|5| 3   |   3    | 情况1 |
----------------------------
然后我们用desBuf生成最终放大或缩小后的图片

desImg = Image.createImage(desW, desH);
DirectUtils.getDirectGraphics(desImg.getGraphics()).
        drawPixels(desBuf, true, 0, desW, 0, 0, desW, desH, 0, 444);
return desImg;

下面是采用此算法实现放大缩小的效果图

 
最后要说明一点的是,由于该算法中使用了Image.createImage(w, h)来创建图像,这个函数会创建一个w*h像素的全白可变图像,所以透明图片放大缩小后,背景不再透明,而是白色了,这是j2me本身的缺憾和算法没有关 系。对于这个问题,有一个解决办法,就是程序的图片不采用Image来保存,而是采用short[]数组保存,画图的时候用drawPixels()来画 图。