Android变形矩阵——Matrix

对于图像的图形变换,Android系统是通过矩阵来进行处理的,每个像素点都表达了其坐标的X、Y信息。Android的图形变换矩阵是一个3x3的矩阵,如下图所示:

Android绘图机制与处理技巧——Android图像处理之图形特效处理_第1张图片 72F0CAC1-14FB-40F8-A430-8F542B09DC4E.png

当使用变换矩阵去处理每一个像素点的时候,与颜色矩阵的矩阵乘法一样,计算公式如下所示:

X1=aX+bY+c
Y1=dX+eY+f
1=gX+hY+i

通常情况下,会让g=h=0,i=1,这样就使1=gX+hY+i恒成立。因此,只需着重关注上面几个参数即可。

与色彩变换矩阵的初始矩阵一样,图形变换矩阵也有一个初始矩阵。就是对角线元素a、e、i为1,其他元素为0的矩阵,如下图所示:


Android绘图机制与处理技巧——Android图像处理之图形特效处理_第2张图片 图形变换初始矩阵

图像的变形处理通常包含以下四类基本变换:
Translate——平移变换
Rotate——旋转变换
Scale——缩放变换
Skew——错切变换

  • 平移变换

平移变换的坐标值变换过程就是将每个像素点都进行平移变换,当从P(x0,y0)平移到P(x1,y1)时,所需的平移矩阵如下所示:

Android绘图机制与处理技巧——Android图像处理之图形特效处理_第3张图片 F8CD701F-4C5A-40DF-9B67-E50500B702DC.png
  • 旋转变换

旋转变换即指一个点围绕一个中心旋转到一个新的点。当从P(x0,y0)点,以坐标原点O为旋转中心旋转到P(x1,y1)时,可以将点的坐标都表达成OP与X轴正方向夹角的函数表达式(其中r为线段OP的长度,α为OP(x0,y0)与X轴正方向夹角,θ为OP(x0,y0)与OP(x1,y1)之间夹角),如下所示:

x0=rcosα
y0=rsinα
x1=rcos(α+θ)=rcosαcosθ−rsinαsinθ=x0cosθ−y0sinθ
y1=rsin(α+θ)=rsinαcosθ+rcosαsinθ=y0cosθ+x0sinθ

矩阵形式如下图所示:


Android绘图机制与处理技巧——Android图像处理之图形特效处理_第4张图片 旋转变换矩阵

前面是以坐标原点为旋转中心的旋转变换,如果以任意点O为旋转中心来进行旋转变换,通常需要以下三个步骤:
1.将坐标原点平移到O点
2.使用前面讲的以坐标原点为中心的旋转方法进行旋转变换
3.将坐标原点还原

  • 缩放变换

一个像素点是不存在缩放的概念的,但是由于图像是由很多个像素点组成的,如果将每个点的坐标都进行相同比例的缩放,最终就会形成让整个图像缩放的效果,缩放效果的公式如下

x1=K1x0
y1=K2y0

矩阵形式如下图所示:


Android绘图机制与处理技巧——Android图像处理之图形特效处理_第5张图片 缩放变换矩阵
  • 错切变换

错切变换(skew)在数学上又称为Shear mapping(可译为“剪切变换“)或者Transvection(缩并),它是一种比较特殊的线性变换。错切变换的效果就是让所有点的X坐标(或者Y坐标)保持不变,而对应的Y坐标(或者X坐标)则按比例发生平移,且平移的大小和该点到Y轴(或者X轴)的距离成正比。错切变换通常包含两种——水平错切与垂直错切。

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

水平错切

x1=x0+K1y0
y1=y0

垂直错切

x1=x0
y1=K2x0+y0

矩阵形式如下图


Android绘图机制与处理技巧——Android图像处理之图形特效处理_第6张图片 错切变换矩阵

由上面的分析可以发现,这个图形变换3x3的矩阵与色彩变换矩阵一样,每个位置的元素所表示的功能是有规律的,总结如下:

Android绘图机制与处理技巧——Android图像处理之图形特效处理_第7张图片 矩阵变换规律

可以发现,a、b、c、d、e、f这六个矩阵元素分别对应以下变换:
a和e控制Scale——缩放变换
b和d控制Skew——错切变换
a和e控制Trans——平移变换
a、b、d、e共同控制Rotate——旋转变换
通过类似色彩矩阵中模拟矩阵的例子来模拟变形矩阵。在图形变换矩阵中,同样是通过一个一维数组来模拟矩阵,并通过setValues()方法将一个一维数组转换为图形变换矩阵,代码如下所示:

private float[] mImageMatrix = new float[9];Matrix matrix = new Matrix();matrix.setValues(mImageMatrix);````当获得了变换矩阵后,就可以通过以下代码将一个图像以这个变换矩阵的形式绘制出来。     canvas.drawBitmap(mBitmap, mMatrix, null);

public class HandleImage1Activity extends BaseActivity {
private ImageView mImageView;
private GridLayout mGroup;
private float mHue, mSaturation, mLum;
private Bitmap mBitmap;

private int mEtWidth, mEtHeight;private EditText[] mEts = new EditText[9];private float[] mImageMatrix = new float[9];@Overrideprotected void onCreate(@Nullable Bundle savedInstanceState) {    super.onCreate(savedInstanceState);    setContentView(R.layout.activity_handleimg1);    mBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.iu1);    mImageView = (ImageView) findViewById(R.id.img);    mGroup = (GridLayout) findViewById(R.id.group);    mGroup.post(new Runnable() {        @Override        public void run() {            // 获取宽高信息            mEtWidth = mGroup.getWidth() / 3;            mEtHeight = mGroup.getHeight() / 3;            addEts();            initMatrix();        }    });    mImageView.setImageBitmap(mBitmap);}// 初始化颜色矩阵为初始状态private void initMatrix() {    for (int i = 0; i < 9; i++) {        if (i % 4 == 0)            mEts[i].setText(String.valueOf(1));        else            mEts[i].setText(String.valueOf(0));    }}// 添加EditTextprivate void addEts() {    for (int i = 0; i < 9; i++) {        EditText editText = new EditText(this);        editText.setInputType(InputType.TYPE_NUMBER_FLAG_DECIMAL);        mEts[i] = editText;        mGroup.addView(mEts[i], mEtWidth, mEtHeight);    }}// 获取矩阵值private void getMatrix() {    for (int i = 0; i < 9; i++) {        mImageMatrix[i] = Float.valueOf(mEts[i].getText().toString());    }}// 将矩阵值设置到图像private void setImageMatrix() {    Bitmap bmp = Bitmap.createBitmap(mBitmap.getWidth(), mBitmap.getHeight(), Bitmap.Config.ARGB_8888);    Canvas canvas = new Canvas(bmp);    Matrix matrix = new Matrix();    matrix.setValues(mImageMatrix);    canvas.drawBitmap(mBitmap,matrix,null);    mImageView.setImageBitmap(bmp);}// 作用矩阵效果public void btnChange(View view) {    getMatrix();    setImageMatrix();}// 重置矩阵效果public void btnReset(View view) {    initMatrix();    getMatrix();    setImageMatrix();}

}````
Android系统同样提供了一些API来简化矩阵的运算,我们不必每次都去设置矩阵的每一个元素值。Android中使用Matrix类来封装矩阵,并提供了以下几个操作方法来实现上面的四中变换方式:

matrix.setRotate()——旋转变换
matrix.setTranslate()——平移变换
matrix.setScale()——缩放变换
matrix.setSkew()——错切变换
matrix.preX和matrix.postY——提供矩阵的前乘和后乘运算

Matrix类的set方法会重置矩阵中的值,而post和pre方法不会,这两个方法常用来实现矩阵的混合作用。不过要注意的是,矩阵运算不满足乘法的交换律,所以矩阵乘法的前乘和后乘是两种不同的运算方式。举例说明,比如需要实现以下效果:

先旋转45度
再平移到(200, 200)
如果使用后乘运算,表示当前矩阵乘上参数代表的矩阵,代码如下所示:

    matrix.setRotate(45);    matrix.postTranslate(200, 200);

如果使用前乘运算,表示参数代表的矩阵乘上当前矩阵,代码如下所示:

    matrix.setTranslate(200, 200);    matrix.preRotate(45);

像素块分析

图像的特效处理有两种方式,即使用矩阵来进行图像变换和使用drawBitmapMesh()方法来进行处理。drawBitmapMesh()与操纵像素点来改变色彩的原理类似,只不过是把图像分成了一个个的小块,然后通过改变每一个图像块来修改整个图像。

drawBitmapMesh()方法代码如下:

public void 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)坐标对的数目
要使用drawBitmapMesh()方法就需先将图片分割为若干个图像块。所以,在图像上横纵各画N条线,而这横纵各N条线就交织成了NxN个点,而每个点的坐标则以x1,y1,x2,y2,...,xn,yn的形式保存在verts数组中。也就是说verts数组的每两位用来保存一个交织点,第一个是横坐标,第二个是纵坐标。而整个drawBitmapMesh()方法改变图像的方式,就是靠这些坐标值的改变来重新定义每一个图像块,从而达到图像效果处理的功能。

drawBitmapMesh()方法的功能非常强大,基本上可以实现所有的图像特效,但使用起来也非常复杂,其关键就是在于计算、确定新的交叉点的坐标。下面举例说明如何使用drawBitmapMesh()方法来实现一个旗帜飞扬的效果。

要想达到旗帜飞扬的效果,只需要让图片中每个交叉点的横坐标较之前不发生变化,而纵坐标较之前坐标呈现一个三角函数的周期性变化即可。

首先获取交叉点的坐标,并将坐标保存到orig数组中,其获取交叉点坐标的原理就是通过循环遍历所有的交叉线,并按比例获取其坐标,代码如下所示:

    mBitmap = BitmapFactory.decodeResource(context.getResources(), R.mipmap.test);    float bitmapWidth = mBitmap.getWidth();    float bitmapHeight = mBitmap.getHeight();    int index = 0;    for (int y = 0; y <= HEIGHT ; y++) {        float fy = bitmapHeight * y / HEIGHT;        for (int x = 0; x <= WIDTH; x++) {            float fx = bitmapWidth * x / WIDTH;            orig[index * 2] = verts[ index * 2] = fx;            //这里人为将坐标+100是为了让图像下移,避免扭曲后被屏幕遮挡            orig[index * 2 + 1] = verts[ index * 2 + 1] = fy + 100;            index++;        }    }

接下来,在onDraw()方法中改变交叉点的纵坐标的值,为了实现旗帜飘扬的效果,使用一个正弦函数sinx来改变交叉点纵坐标的值,而横坐标不变,并将变化后的值保存到verts数组中,代码如下所示:

@Overrideprotected void onDraw(Canvas canvas) {    super.onDraw(canvas);    flagWave();    K += 0.1f;//将K的值增加    canvas.drawBitmapMesh(mBitmap, WIDTH, HEIGHT, verts, 0, null, 0, null);    invalidate();}/** * 按当前点所在的横坐标的位置来确定纵坐标的偏移量,其中A代表正弦函数中的振幅大小 */private void flagWave() {    for (int j = 0; j <= HEIGHT; j++) {        for (int i = 0; i <= WIDTH; i++) {            //在获取纵坐标的偏移量时,利用正弦函数的周期性给函数增加一个周期K * Math.PI,就是为了让图像能够动起来            float offsetY = (float) Math.sin(2 * Math.PI * i / WIDTH + K * Math.PI);            verts[(j * (WIDTH + 1) + i) * 2 + 1] = orig[(j * (WIDTH + 1) + i) * 2 + 1] + offsetY * A;        }    }}

这样,每次在重绘时,通过改变相位来改变偏移量,从而造成一个动态的效果,就好象旗帜在风中飘扬一样,效果图如下。

使用drawBitmapMesh()方法可以创建很多复杂的图像效果,但是对它的使用也相对复杂,需要我们对图像处理有很深厚的功底。同时,对算法的要求也比较高,需要计算各种特效下不同的坐标点变化规律,从而设计出不同的特效。

代码如下:

public class WaveView extends AppCompatImageView {    private static final int HEIGHT=200;//想要划分的高    private static final int WIDTH=200;//想要划分的宽    private int COUNT = (WIDTH + 1) * (HEIGHT + 1);    private float[] verts = new float[COUNT * 2];    private float[] orig = new float[COUNT * 2];    private float A = 50;//表示正弦函数中的振幅大小    private float K = 1;    private Bitmap mBitmap;    private int mWaveSrc;    public WaveView(Context context) {        this(context,null);    }    public WaveView(Context context, @Nullable AttributeSet attrs) {        this(context,attrs,0);    }    public WaveView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {        super(context, attrs, defStyleAttr);        TypedArray typedArray = context.obtainStyledAttributes(R.styleable.WaveView);        mWaveSrc=typedArray.getResourceId(R.styleable.WaveView_waveSrc,R.drawable.iu1);        mBitmap= BitmapFactory.decodeResource(getResources(),mWaveSrc);        float bitmapWidth = mBitmap.getWidth();        float bitmapHeight = mBitmap.getHeight();        int index = 0;        for (int y = 0; y <= HEIGHT ; y++) {            float fy = bitmapHeight * y / HEIGHT;            for (int x = 0; x <= WIDTH; x++) {                float fx = bitmapWidth * x / WIDTH;                orig[index * 2] = verts[ index * 2] = fx;                //这里人为将坐标+100是为了让图像下移,避免扭曲后被屏幕遮挡                orig[index * 2 + 1] = verts[ index * 2 + 1] = fy ;                index++;            }        }        typedArray.recycle();    }    @Override    protected void onDraw(Canvas canvas) {        super.onDraw(canvas);        flagWave();        K += 0.1f;//将K的值增加        canvas.drawBitmapMesh(mBitmap, WIDTH, HEIGHT, verts, 0, null, 0, null);        setImageBitmap(mBitmap);        invalidate();    }    /**     * 按当前点所在的横坐标的位置来确定纵坐标的偏移量,其中A代表正弦函数中的振幅大小     */    private void flagWave() {        for (int j = 0; j <= HEIGHT; j++) {            for (int i = 0; i <= WIDTH; i++) {                //在获取纵坐标的偏移量时,利用正弦函数的周期性给函数增加一个周期K * Math.PI,就是为了让图像能够动起来                float offsetY = (float) Math.sin(2 * Math.PI * i / WIDTH + K * Math.PI);                verts[(j * (WIDTH + 1) + i) * 2 + 1] = orig[(j * (WIDTH + 1) + i) * 2 + 1] + offsetY * A;            }        }    }}

更多相关文章

  1. Android异步加载图像小结(含线程池,缓存方法)[转]
  2. [Android] 使用Matrix矩阵类对图像进行缩放、旋转、对比度、亮度
  3. Android图像识别扫名片识别技术SDK
  4. android 获取定位坐标,在百度地图出现大偏移的处理办法
  5. Android平台上的Gphone 图像演示
  6. (android图像处理)android之bitmap各种常用函数
  7. Android中SensorManager.getRotationMatrix函数:计算出旋转矩阵,
  8. Android 官方文档:(一)动画和图像 —— 1.5 画布和画图

随机推荐

  1. android中调用requestFocus()的详细过程
  2. Android(安卓)系统名字、版本、API level
  3. android DatePickerDialog的应用举例
  4. 如何使用android手机拍照
  5. 使用Android(安卓)Studio可能会遇到的问
  6. Android官方文档阅读之旅——Introductio
  7. 举例说明Android中AnalogClock的使用
  8. image button on android
  9. android 判断是否第一次进入欢迎页
  10. MySQL数据库开发的36条原则(小结)