Android图像处理技巧理论

导语

书上讲的很细,还讲了一些原理,原理需要一些线性代数的知识,线代都忘光了,主要看后面的实例就Ok了,看实例戳我

主要内容

  • 色彩特效处理
  • 图形特效处理
  • 画笔特效处理
  • SurfaceView

具体内容

Android对于图片的处理,最常使用到的数据结构是位图——Bitmap,它包含了一张图片所有的数据。
整个图片都是由点阵和颜色值组成的,所谓点阵就是一个包含像素的矩阵,第一个元素对应着图片的一个像素。而颜色值——ARGB,分别对应透明度、红、绿、蓝这四个通道分量,它们共同决定了每个像素点显示的颜色。

色彩特效处理

Bitmap图片都是由点阵和颜色值组成的,所谓点阵就是一个包含像素的矩阵,每一个元素对应着图片的一个像素。而颜色值——ARGB,分别对应透明度、红、绿、蓝这四个通道分量,它们共同决定了每个像素点显示的颜色。

色彩矩阵分析

在色彩处理中,我们通常用三个角度描述一张图片:

  • 色调:物体传播的颜色。
  • 饱和度:颜色的纯度,从0(灰)-100%(饱和)来进行描述。
  • 亮度:颜色的相对明暗程度。

而在Android中,系统会使用一个颜色矩阵——ColorMatrix,来处理这些色彩的效果,Android中的颜色矩阵是4X5的数字矩阵,他用来对颜色色彩进行处理,而对于每一个像素点,都有一个颜色分量矩阵来保存ARGB值。

根据前面对矩阵A、C的定义,通过矩阵乘法运算法则,可以得到:

矩阵运算的乘法计算过程如下:

我们观察颜色矩阵A:

从这个公式可以发现:

  • 第一行的abcde用来决定新的颜色值R——红色。
  • 第二行的fghij用来决定新的颜色值G——绿色。
  • 第三行的kimno用来决定新的颜色值B——蓝色。
  • 第四行的pqrst用来决定新的颜色值A——透明度。
  • 矩阵A中的第五列——ejot值分别用来决定每个分量中的offset,即偏移量。

通过一个小例子来讲解:
首先重新看一下矩阵变换计算公式,以R分量为例,计算过程如下:

R1 = a * R + b* G + c*B+d *A + e

如果让a = 1,b、c、d、e都等于0,那么计算的结果为R1 = R,因此我们可以构建一个矩阵:

如果把这个矩阵公式带入R1 = AC,那么根据矩阵的乘法运算法则,可以得到R1 = R。因此,这个矩阵通常是用来作为初始的颜色矩阵来使用,他不会对原有颜色进行任何变化。
那么当我们要变换颜色值的时候,通常有两种方法。一个是直接改变颜色的offset,即偏移量的值来修改颜色的分量。另一种方法直接改变对应RGBA值的系数来调整颜色分量的值。
从前面的分析中,可以知道要修改R1的值,只要将第五列的值进行修改即可。即改变颜色的偏移量,其它值保存初始矩阵的值,如图:

在上面这个矩阵中,我们修改了R、G所对应的颜色偏移量,那么最后的处理结果就是图像的红色、绿色分别增加了100。而我们知道,红色混合绿色会得到黄色,所以最终的颜色处理结果就是让整个图片的色调偏黄色。

如果修改颜色分量中的某个系数值,而其他只依然保存初始矩阵的值,如图:

在上面这个矩阵中,改变了G分量所对应的系数g,这样在矩形运算后G分量会变成以前的两倍,最终效果就是图像的色调更加偏绿。

下面通过实例看看如何通过矩阵改变图像的色调、饱和度和亮度:

  • 色调:setRotate(int axis, float degree),第一个参数分别使用0、1、2代表Red、Green、Blue三种颜色,第二参数需要处理的值。
ColorMatrix hueMatrix = new ColorMatrix();
hueMatrix.setRotate(0, hue);
hueMatrix.setRotate(1, hue);
hueMatrix.setRotate(2, hue);
  • 饱和度:setSaturation(float sat),参数代表设置饱和度的值。
ColorMatrix saturationMatrix = new ColorMatrix();
saturationMatrix.setSaturation(saturation);
  • 亮度:setScale(float rscale,float gscale,float bscale,float ascale),参数分别是红、绿、蓝、透明度的亮度大小值。
ColorMatrix lumMatrix = new ColorMatrix();
lumMatrix.setScale(lum, lum, lum, 1);
  • 除了单独使用上面三种方式来进行颜色效果处理之外,还提供了postConcat()方法来将矩阵的作用效果混合,从而叠加处理效果。
ColorMatrix imageMatrix = new ColorMatrix();
imageMatrix.postConcat(hueMatrix);
imageMatrix.postConcat(saturationMatrix);
imageMatrix.postConcat(lumMatrix);
常用图像颜色矩阵处理效果
  • 灰色效果:
  • 图像反转:
  • 怀旧效果:
  • 去色效果:
  • 高饱和度:
像素点分析

在Android中,系统系统提供了Bitmap.getPixels()方法来帮我们提取整个Bitmap中的像素点,并保存在一个数组中:

bitmap.getPixels(pixels, offset, stride, x, y, width, height);

这几个参数的具体含义如下:

  • pixels:接收位图颜色值的数组。
  • offset:写入到pixels[]中的第一个像素索引值。
  • stride:pixels[]中的行间距。
  • x:从位图中读取的第一个像素的x坐标值。
  • y:从位图中读取的第一个像素的y坐标值。
  • width:从每一行中读取的像素宽度。
  • height:读取的行数。

通常使用如下代码:

bitmap.getPixels(oldPx, 0, bitmap.getWidth(), 0, 0, width, height);

接下来获取每个像素具体的ARGB值:

color = oldPx[i];
r = Color.red(color);
g = Color.green(color);
b = Color.blue(color);
a = Color.alpha(color);

接下来就是修改像素值,产生新的像素值:

r1 = (int) (0.393 * r + 0.769 * g + 0.189 * b);
g1 = (int) (0.349 * r + 0.686 * g + 0.168 * b);
b1 = (int) (0.272 * r + 0.534 * g + 0.131 * b);

newPx[i] = Color.argb(a, r1, b1, g1);

最后使用我们的新像素值:

bmp.setPixels(newPx, 0, width, 0, 0, width, height);

图形特效处理

Android变形矩阵——Matrix

对于图形变换,系统提供了3x3的举证来处理:

与颜色矩阵一样,计算方法通过矩阵乘法:

X1 = a x X +b x Y +c;
Y1 = d x X +e x Y +f;
1 = g x X +h x Y + i;

与颜色矩阵一样,也有一个初始矩阵:

图像的变形处理包含以下四类基本变换:

  • Translate:平移变换。
  • Rotate:旋转变换。
  • Scale:缩放变换。
  • Skew:错切变换。

平移变换:即对每个像素点都进行平移变换,通过计算可以发现如下平移公式:

X = X0 + △X;
Y = Y0 + △Y;

旋转变换:通过以下三步骤完成以任意点为旋转中心的旋转变换:

  • 将坐标原点平移到O点。
  • 使用前面讲的以坐标原点为中心的旋转方法进行旋转变换。
  • 将坐标原点还原。

缩放变换:缩放变换的效果计算公式如下:

x = K1 X x0;
y = K2 X y0;

错切变换:错切变换的效果计算公式如下:

x = x0 + k1 + y0
y = k2 x x0 + y0

了解四种图形变换矩阵,可以通过setValues()方法将一个一维数组转换为图形变换矩阵:

private float [] mImageMatrix = new float[9];
Matrix matrix = new Matrix();
matrix.setValues(mImageMatrix);
canvas.drawBitmap(mBitmmap,matrix,null);

Android中Matrix类也帮我们封装好了几个操作方法:

  • matrix.setRotate():旋转变换。
  • matrix.setTranslate():平移变换。
  • matrix.setScale():缩放变换。
  • matrix.setSkew():错切变换。
  • pre()和post():提供矩阵的前乘和后乘运算。

举个例子说明前乘和后乘的不同运算方式:

  • 先平移到(300, 100)。
  • 再旋转45度。
  • 最后平移到(200, 200)。
matrix.setRotate(45);
matrix.postTranslate(200, 200);

如果使用前乘运算,代码如下:

matrix.setTranslate(200, 200);
matrix.perRotate(45);
像素块分析

drawBitmapMesh()与操纵像素点来改变色彩的原理类似,只不过是把图像分成了一个个的小块,然后通过改变每一个图像块来修改整个图像:

canvas.drawBitmapMesh(Bitmap bitmap,int meshWidth,int meshHeight,float [] verts,
    int vertOffset,int [] colors,int colorOffset,Paint paint);

参数分析:

  • bitmap:将要扭曲的图像。
  • meshWidth:需要的横向网格数目。
  • meshHeight:需要的纵向网格数目。
  • verts:网格交叉点的坐标数组。
  • vertOffset:verts数组中开始跳过的(X,Y)坐标对的数目。

画笔特效处理

PorterDuffXfermode

PorterDuffXfermod设置的是两个图层交集区域的显示方式,dst是先画的图形,而src是后画的图形。

以一个圆角图片为例子:

mBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.test1);
mOut = Bitmap.createBitmap(mBitmap.getWidth(), mBitmap.getHeight(), Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(mOut);
mPaint = new Paint();
mPaint.setAntiAlias(true);
canvas.drawRoundRect(0, 0, mBitmap.getWidth(), mBitmap.getHeight(), 80, 80, mPaint);
mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
canvas.drawBitmap(mBitmap,0,0,mPaint);

效果图,由于图片过大,只能看出一边角:

Shader

Shader又被称为着色器。渲染器,它可以实现渲染,渐变等效果,Android中的Shader包括以下几种:

  • BitmapShader:位图Shader。
  • LinearGradient:线性Shader。
  • RadialGradient:光束Shader。
  • SweepGradient:梯形Shader。
  • ComposeShader:混合Shader。

其中BitmapShader有三种模式可以选择:

  • CLAMP拉伸:拉伸的是图片最后的那一个像素,不断重复。
  • REPEAT重复:横向,纵向不断重复。
  • MIRROR镜像:横向不断翻转重复,纵向不断翻转重复。

下面看下例子说明,圆形图片:

mBitmap = BitmapFactory.decodeResource(getResources(),R.drawable.nice);
mBitmapShader = new BitmapShader(mBitmap, Shader.TileMode.CLAMP,Shader.TileMode.CLAMP);
mPaint = new Paint();
mPaint.setShader(mBitmapShader);
canvas.drawCircle(500,250,200,mPaint);

效果图:

下面把TileMode改为REPEAT:

mBitmap = BitmapFactory.decodeResource(getResources(),R.mipmap.ic_launcher);
mBitmapShader = new BitmapShader(mBitmap, Shader.TileMode.REPEAT,Shader.TileMode.REPEAT);
mPaint = new Paint();
mPaint.setShader(mBitmapShader);
canvas.drawCircle(500,250,200,mPaint);

使用LinearGradient:

mPaint = new Paint();
mPaint.setShader(new LinearGradient(0,0,400,400, Color.BLUE,Color.YELLOW, Shader.TileMode.REPEAT));
canvas.drawRect(0,0,400,400,mPaint);

效果图:

PathEffect

先上一张直观的图:

Android提供的几种绘制PathEffect方式:

  • 没效果。
  • CornerPathEffect:拐弯角变得圆滑。
  • DiscretePathEffect:线段上会产生许多杂点。
  • DashPathEffect:绘制虚线,用一个数据来设置各个点之间的间隔。
  • PathDashPathEffect:绘制虚线,可以使用方形点虚线和圆形点虚线。
  • ComposePathEffect:可任意组合两种路径(PathEffect)的特性。

我们通过一个实例来认识这些效果:

public class PathEffectView extends View{

    private Path mPath;
    private PathEffect [] mEffect = new PathEffect[6];
    private Paint mPaint;

    /**
     * 构造方法
     * @param context
     * @param attrs
     */
    public PathEffectView(Context context, AttributeSet attrs) {
        super(context, attrs);

        init();
    }

    /**
     * 初始化
     */
    private void init() {
        mPaint = new Paint();
        mPath = new Path();
        mPath.moveTo(0,0);
        for (int i = 0; i<= 30;i++){
            mPath.lineTo(i*35,(float)(Math.random()*100));
        }
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        mEffect[0] = null;
        mEffect[1] = new CornerPathEffect(30);
        mEffect[2] = new DiscretePathEffect(3.0F,5.0F);
        mEffect[3] = new DashPathEffect(new float[]{20,10,5,10},0);
        Path path = new Path();
        path.addRect(0,0,8,8,Path.Direction.CCW);
        mEffect[4]= new PathDashPathEffect(path,12,0,PathDashPathEffect.Style.ROTATE);
        mEffect[5] = new ComposePathEffect(mEffect[3],mEffect[1]);
        for (int i = 0; i<mEffect.length;i++){
            mPaint.setPathEffect(mEffect[i]);
            canvas.drawPath(mPath,mPaint);
            canvas.translate(0,200);
        }
    }
}

每绘制一个Path,就将画布平移,从而让各种PathEffect依次绘制出来。效果图:

View之孪生兄弟——SurfaceView

SurfaceView与View的区别

View的绘制刷新间隔时间为16ms,如果在16ms内完成你所需要执行的所有操作,那么在用户视觉上,就不会产生卡顿的感觉,否则,就会出现卡顿,所以可以考虑使用SurfaceView来替代View的绘制。

通常在Log会看到这样的提示:

Skipped 47 frames! The application may be doing too much work on its main thread

SurfaceView与View的主要区别:

  • View主要适用于主动更新的情况下,而surfaceVicw主要适用于被动更新,例如频繁刷新。
  • View在主线程中对画面进行刷新,而surfaceView通常会通过一 个子线程来进行页面的刷新。
  • View在绘制时没有使用双缓冲机制,而surfaceVicw在底层实现机制中就已经实现了双缓冲机制。

总结一句话就是,如果你的自定义View需要频繁刷新,或者刷新数据处理量比较大,就可以考虑使用SurfaceView替代View。

SurfaceView的使用

SurfaceView使用步骤:

  • 创建SurfaceView继承自SurfaceView,并实现两个接口——SurfaceHolder.Callback和Runnable。
  • 初始化SurfacHolder对象,并注册SurfaceHolder的回调方法。
  • 通过SurfacHolder对象lockCanvas()方法获得Canvas对象进行绘制,并通过unlockCanvasAndPost(mCanvas)方法对画布内容进行提交。

整个使用SurfaceView的模板代码:

public class SurfaView extends SurfaceView implements SurfaceHolder.Callback, Runnable {

    //SurfaceHolder
    private SurfaceHolder mHolder;
    //用于绘制的Canvas
    private Canvas mCanvas;
    //子线程标志位
    private boolean mIsDrawing;

    /**
     * 构造方法
     *
     * @param context
     * @param attrs
     */
    public SurfaView(Context context, AttributeSet attrs) {
        super(context, attrs);

        mHolder = getHolder();
        mHolder.addCallback(this);
        setFocusable(true);
        setFocusableInTouchMode(true);
        setKeepScreenOn(true);
    }

    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        mIsDrawing = true;
        new Thread(this).start();
    }

    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {

    }

    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
        mIsDrawing = false;
    }

    @Override
    public void run() {
        while (mIsDrawing) {
            draw();
        }
    }

    private void draw() {
        try {
            mCanvas = mHolder.lockCanvas();
        } catch (Exception e) {

        } finally {
            if (mCanvas != null) {
                //提交
                mHolder.unlockCanvasAndPost(mCanvas);
            }
        }
    }
}

唯一注意的是,在绘制中将mHolder.unlockCanvasAndPost(mCanvas)方法放到finally代码块中,保证每次都能将内容提交。

更多内容戳这里(整理好的各种文集)

推荐阅读更多精彩内容

  • 图像色彩处理 Android对图片的处理,通常用到的数据结构就是位图—Bitmap,它包含了一张图片的所有数据。整...
    SheldonChen4215阅读 3,084评论 0 6
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 166,786评论 24 703
  • 1 前言 一直想沿着图像处理这条线建立一套完整的理论知识体系,同时积累实际应用经验。因此有了从使用AVFounda...
    RichardJieChen阅读 4,881评论 5 12
  • 只要有一个理由让自己充满斗志 即使碰到其他再糟糕的境况 都不会觉得那么沮丧 1 从冬至开始,节日便接二连三的来。圣...
    蜜思琳的世界阅读 139评论 0 0
  • 写给自己一句正能量的话:工作上做靠谱的人,生活中成为有趣的人!
    小六你好阅读 172评论 0 1
  • 英国作家罗琳的《哈利波特》系列构建了一个世界,从人物、家族、服饰、动物、魔法到学校、法律、战争,罗琳几乎仅凭一人之...
    郭零阅读 150评论 0 2