Android繪圖機制與處理技巧——Android圖像處理之圖形特效處理

Android變形矩陣——Matrix

對于圖像的圖形變換,Android系統(tǒng)是通過矩陣來進行處理的,每個像素點都表達了其坐標的X、Y信息。Android的圖形變換矩陣是一個3x3的矩陣,如下圖所示:

72F0CAC1-14FB-40F8-A430-8F542B09DC4E.png

當(dāng)使用變換矩陣去處理每一個像素點的時候,與顏色矩陣的矩陣乘法一樣,計算公式如下所示:

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

通常情況下,會讓g=h=0,i=1,這樣就使1=gX+hY+i恒成立。因此,只需著重關(guān)注上面幾個參數(shù)即可。

與色彩變換矩陣的初始矩陣一樣,圖形變換矩陣也有一個初始矩陣。就是對角線元素a、e、i為1,其他元素為0的矩陣,如下圖所示:


圖形變換初始矩陣

圖像的變形處理通常包含以下四類基本變換:
Translate——平移變換
Rotate——旋轉(zhuǎn)變換
Scale——縮放變換
Skew——錯切變換

  • 平移變換

平移變換的坐標值變換過程就是將每個像素點都進行平移變換,當(dāng)從P(x0,y0)平移到P(x1,y1)時,所需的平移矩陣如下所示:

F8CD701F-4C5A-40DF-9B67-E50500B702DC.png
  • 旋轉(zhuǎn)變換

旋轉(zhuǎn)變換即指一個點圍繞一個中心旋轉(zhuǎn)到一個新的點。當(dāng)從P(x0,y0)點,以坐標原點O為旋轉(zhuǎn)中心旋轉(zhuǎn)到P(x1,y1)時,可以將點的坐標都表達成OP與X軸正方向夾角的函數(shù)表達式(其中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θ

矩陣形式如下圖所示:


旋轉(zhuǎn)變換矩陣

前面是以坐標原點為旋轉(zhuǎn)中心的旋轉(zhuǎn)變換,如果以任意點O為旋轉(zhuǎn)中心來進行旋轉(zhuǎn)變換,通常需要以下三個步驟:
1.將坐標原點平移到O點
2.使用前面講的以坐標原點為中心的旋轉(zhuǎn)方法進行旋轉(zhuǎn)變換
3.將坐標原點還原

  • 縮放變換

一個像素點是不存在縮放的概念的,但是由于圖像是由很多個像素點組成的,如果將每個點的坐標都進行相同比例的縮放,最終就會形成讓整個圖像縮放的效果,縮放效果的公式如下

x1=K1x0
y1=K2y0

矩陣形式如下圖所示:


縮放變換矩陣
  • 錯切變換

錯切變換(skew)在數(shù)學(xué)上又稱為Shear mapping(可譯為“剪切變換“)或者Transvection(縮并),它是一種比較特殊的線性變換。錯切變換的效果就是讓所有點的X坐標(或者Y坐標)保持不變,而對應(yīng)的Y坐標(或者X坐標)則按比例發(fā)生平移,且平移的大小和該點到Y(jié)軸(或者X軸)的距離成正比。錯切變換通常包含兩種——水平錯切與垂直錯切。

錯切變換的計算公式如下:

水平錯切

x1=x0+K1y0
y1=y0

垂直錯切

x1=x0
y1=K2x0+y0

矩陣形式如下圖


錯切變換矩陣

由上面的分析可以發(fā)現(xiàn),這個圖形變換3x3的矩陣與色彩變換矩陣一樣,每個位置的元素所表示的功能是有規(guī)律的,總結(jié)如下:

矩陣變換規(guī)律

可以發(fā)現(xiàn),a、b、c、d、e、f這六個矩陣元素分別對應(yīng)以下變換:
a和e控制Scale——縮放變換
b和d控制Skew——錯切變換
a和e控制Trans——平移變換
a、b、d、e共同控制Rotate——旋轉(zhuǎn)變換
通過類似色彩矩陣中模擬矩陣的例子來模擬變形矩陣。在圖形變換矩陣中,同樣是通過一個一維數(shù)組來模擬矩陣,并通過setValues()方法將一個一維數(shù)組轉(zhuǎn)換為圖形變換矩陣,代碼如下所示:

private float[] mImageMatrix = new float[9];
Matrix matrix = new Matrix();
matrix.setValues(mImageMatrix);````

當(dāng)獲得了變換矩陣后,就可以通過以下代碼將一個圖像以這個變換矩陣的形式繪制出來。

     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];

@Override
protected 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);

}

// 初始化顏色矩陣為初始狀態(tài)
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));
    }
}

// 添加EditText
private 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());
    }
}

// 將矩陣值設(shè)置到圖像
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系統(tǒng)同樣提供了一些API來簡化矩陣的運算,我們不必每次都去設(shè)置矩陣的每一個元素值。Android中使用Matrix類來封裝矩陣,并提供了以下幾個操作方法來實現(xiàn)上面的四中變換方式:

matrix.setRotate()——旋轉(zhuǎn)變換
matrix.setTranslate()——平移變換
matrix.setScale()——縮放變換
matrix.setSkew()——錯切變換
matrix.preX和matrix.postY——提供矩陣的前乘和后乘運算

Matrix類的set方法會重置矩陣中的值,而post和pre方法不會,這兩個方法常用來實現(xiàn)矩陣的混合作用。不過要注意的是,矩陣運算不滿足乘法的交換律,所以矩陣乘法的前乘和后乘是兩種不同的運算方式。舉例說明,比如需要實現(xiàn)以下效果:

先旋轉(zhuǎn)45度
再平移到(200, 200)
如果使用后乘運算,表示當(dāng)前矩陣乘上參數(shù)代表的矩陣,代碼如下所示:

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

如果使用前乘運算,表示參數(shù)代表的矩陣乘上當(dāng)前矩陣,代碼如下所示:

    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)

關(guān)鍵的參數(shù)如下:

bitmap:將要扭曲的圖像
meshWidth:需要的橫向網(wǎng)格數(shù)目
meshHeight :需要的縱向網(wǎng)格數(shù)目
verts:網(wǎng)格交叉點坐標數(shù)組
vertOffset:verts數(shù)組中開始跳過的(x, y)坐標對的數(shù)目
要使用drawBitmapMesh()方法就需先將圖片分割為若干個圖像塊。所以,在圖像上橫縱各畫N條線,而這橫縱各N條線就交織成了NxN個點,而每個點的坐標則以x1,y1,x2,y2,...,xn,yn的形式保存在verts數(shù)組中。也就是說verts數(shù)組的每兩位用來保存一個交織點,第一個是橫坐標,第二個是縱坐標。而整個drawBitmapMesh()方法改變圖像的方式,就是靠這些坐標值的改變來重新定義每一個圖像塊,從而達到圖像效果處理的功能。

drawBitmapMesh()方法的功能非常強大,基本上可以實現(xiàn)所有的圖像特效,但使用起來也非常復(fù)雜,其關(guān)鍵就是在于計算、確定新的交叉點的坐標。下面舉例說明如何使用drawBitmapMesh()方法來實現(xiàn)一個旗幟飛揚的效果。

要想達到旗幟飛揚的效果,只需要讓圖片中每個交叉點的橫坐標較之前不發(fā)生變化,而縱坐標較之前坐標呈現(xiàn)一個三角函數(shù)的周期性變化即可。

首先獲取交叉點的坐標,并將坐標保存到orig數(shù)組中,其獲取交叉點坐標的原理就是通過循環(huán)遍歷所有的交叉線,并按比例獲取其坐標,代碼如下所示:

    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()方法中改變交叉點的縱坐標的值,為了實現(xiàn)旗幟飄揚的效果,使用一個正弦函數(shù)sinx來改變交叉點縱坐標的值,而橫坐標不變,并將變化后的值保存到verts數(shù)組中,代碼如下所示:

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    flagWave();
    K += 0.1f;//將K的值增加
    canvas.drawBitmapMesh(mBitmap, WIDTH, HEIGHT, verts, 0, null, 0, null);
    invalidate();
}

/**
 * 按當(dāng)前點所在的橫坐標的位置來確定縱坐標的偏移量,其中A代表正弦函數(shù)中的振幅大小
 */
private void flagWave() {
    for (int j = 0; j <= HEIGHT; j++) {
        for (int i = 0; i <= WIDTH; i++) {
            //在獲取縱坐標的偏移量時,利用正弦函數(shù)的周期性給函數(shù)增加一個周期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;
        }
    }
}

這樣,每次在重繪時,通過改變相位來改變偏移量,從而造成一個動態(tài)的效果,就好象旗幟在風(fēng)中飄揚一樣,效果圖如下。

使用drawBitmapMesh()方法可以創(chuàng)建很多復(fù)雜的圖像效果,但是對它的使用也相對復(fù)雜,需要我們對圖像處理有很深厚的功底。同時,對算法的要求也比較高,需要計算各種特效下不同的坐標點變化規(guī)律,從而設(shè)計出不同的特效。

代碼如下:

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;//表示正弦函數(shù)中的振幅大小
    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();
    }

    /**
     * 按當(dāng)前點所在的橫坐標的位置來確定縱坐標的偏移量,其中A代表正弦函數(shù)中的振幅大小
     */
    private void flagWave() {
        for (int j = 0; j <= HEIGHT; j++) {
            for (int i = 0; i <= WIDTH; i++) {
                //在獲取縱坐標的偏移量時,利用正弦函數(shù)的周期性給函數(shù)增加一個周期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;
            }
        }
    }
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容