Android:修圖技術(shù)之瘦臉效果的實現(xiàn)(drawBitmapMesh)

一、初識Canvas.drawBitmapMesh()

1、方法介紹分析

先來看看 Android API 中對 drawBitmapMesh 方法的介紹:


drawBitmapMesh方法

這個方法的參數(shù)還不少, 下面稍微講講幾個比較重要的參數(shù)的意思:

  • bitmap:將要扭曲的圖像
  • meshWidth:控制在橫向上把該圖像劃成多少格
  • meshHeight:控制在縱向上把該圖像劃成多少格
  • verts:網(wǎng)格交叉點坐標數(shù)組,長度為(meshWidth + 1) * (meshHeight + 1) * 2
  • vertOffset:控制verts數(shù)組中從第幾個數(shù)組元素開始才對bitmap進行扭曲

Android 中的 drawBitmapMesh() 方法與操縱像素點來改變色彩的原理類似。只不過是把圖像分成一個個的小塊,然后通過改變每一個圖像塊來改變整個圖像。來看看下面這張經(jīng)典的圖像對比:

drawBitmapMesh效果

如上圖,我們將圖像分割成若干個圖像塊,在圖像上橫縱方向各劃分成 N-1 格,而這橫縱分割線就交織成了N*N個點,而每個點的坐標將以x1,y1,x2,y2,···,xn,yn的形式保存在 verts 數(shù)組里。也就是說,verts 數(shù)組中每兩個元素保存一個交織點的位置,第一個保存橫坐標,第二個保存縱坐標。而 drawBitmapMesh() 方法改變圖像的方式,就是通過改變這個 verts 數(shù)組里的元素的坐標值來重新定位對應(yīng)的圖像塊的位置,從而達到圖像效果處理的功能。從這里我們就可以看得出來,借用 Canvas.drawBitmapMesh() 方法可以實現(xiàn)各種圖像形狀的處理效果,只是實現(xiàn)起來比較復雜,關(guān)鍵在于計算、確定新的交叉點的坐標。

Canvas.drawBitmapMesh()

2、方法代碼實現(xiàn)

首先,我們將要修整的圖片加載進來,然后獲取其交叉點的坐標值,并將坐標值保存到 orig[] 數(shù)組中。其獲取交叉點坐標的原理是通過循環(huán)遍歷所有的交叉線,并按比例獲取其坐標,代碼如下:

    //將圖像分成多少格
    private int WIDTH = 200;
    private int HEIGHT = 200;
    //交點坐標的個數(shù)
    private int COUNT = (WIDTH + 1) * (HEIGHT + 1);
    //用于保存COUNT的坐標
    //x0, y0, x1, y1......
    private float[] verts = new float[COUNT * 2];
    //用于保存原始的坐標
    private float[] orig = new float[COUNT * 2];

    private void initView() {
        int index = 0;
        Bitmap mBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.test00);
        float bmWidth = mBitmap.getWidth();
        float bmHeight = mBitmap.getHeight();

        for (int i = 0; i < HEIGHT + 1; i++) {
            float fy = bmHeight * i / HEIGHT;
            for (int j = 0; j < WIDTH + 1; j++) {
                float fx = bmWidth * j / WIDTH;
                //X軸坐標 放在偶數(shù)位
                verts[index * 2] = fx;
                orig[index * 2] = verts[index * 2];
                //Y軸坐標 放在奇數(shù)位
                verts[index * 2 + 1] = fy;
                orig[index * 2 + 1] = verts[index * 2 + 1];
                index += 1;
            }
        }
    }

然后就是將 verts[] 數(shù)組里面的坐標值進行一系列的自定義的修改。這里對 verts[] 數(shù)組的修改直接體現(xiàn)在圖像的顯示效果,各種圖像特效的處理關(guān)鍵就在于此。比如這篇文章對 verts[] 數(shù)組的修改是實現(xiàn)圖像局部約束變形效果。
接著,我們將在onDraw()方法里,將修改過的 verts[] 數(shù)組重新繪制一遍,代碼如下:

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawBitmapMesh(mBitmap, WIDTH, HEIGHT, verts, 0, null, 0, null);
    }

好,大致講完 Canvas.drawBitmapMesh() 方法之后,我們接下來進入實踐環(huán)節(jié),也是本文的重點環(huán)節(jié)——實現(xiàn)人像瘦臉的功能。

二、實現(xiàn)瘦臉效果

1、算法提及

小弟這里用到的平滑過渡可交互的瘦臉算法是 Andreas Gustafsson 的 Interactive Image Warping 文獻里提及的Uwarp's local mapping functions。截個圖大家看看:




有一點興趣的同學可以翻譯一下這段。
有很大的興趣的同學可以通篇看看這個文獻 http://www.gson.org/thesis/warping-thesis.pdf

好了,接下來大家還是看看我的理解吧。

2、算法分析


看上圖,這個坐標系對應(yīng)著我們 Android 屏幕上的繪圖坐標,點 C 就是我們手指觸摸按下的坐標點,半徑為 rmax 的圓形范圍就是我們要平滑變形的區(qū)域,當我們在 C 位置按下屏幕并拖動到點 M 位置時,半徑為 rmax 的變形區(qū)域內(nèi)的每一個像素點將按照上述提及的算法公式進行位移,效果就是點 U 移動到點 X 的位置。所以,關(guān)鍵就是找到上面這個變換的逆變換——給出點 X 時,可以求出它變換前的坐標 U,然后用變化前圖像在 U 點附近的像素進行插值,求出U的像素值。如此對圓形選區(qū)內(nèi)的每一個像素進行求值,便可得出變換后的圖像。在這里,就是求出點 U 的在 verts 數(shù)組對應(yīng)的坐標值,并將此坐標值賦給 X 點在 verts 數(shù)組對應(yīng)的元素,然后重新繪制,就可以得到我們想要的變形后的圖像。

說白了就是需要我們實現(xiàn)以下特點:

  • 只有圓形選區(qū)內(nèi)的圖像才進行變形(這里需要自己用代碼控制一下)
  • 拖動距離 MC 越大變形效果越明顯(這里需要自己用代碼控制一下,下面我會給大家講講)
  • 越靠近圓心,變形越大,越靠近邊緣的變形越小,邊界處無變形(算法公式已經(jīng)實現(xiàn))
  • 變形是平滑的(算法公式已經(jīng)實現(xiàn))

那有同學會注意到,文獻中講到的公式是向量的計算,這算法公式并不能直接用??!且看我們中學的數(shù)學知識:

坐標系解向量加減法:
在直角坐標系里面,定義原點為向量的起點.兩個向量和與差的坐標分別等于這兩個向量相應(yīng)坐標的和與差若向量的表示為(x,y)形式,
A(X1,Y1) ; B(X2,Y2),則:
A+B=(X1+X2,Y1+Y2),A-B=(X1-X2,Y1-Y2)

這樣,我們可以從橫縱坐標入手。話不多說,來實現(xiàn)吧。

3、算法的代碼實現(xiàn)

首先通過 onTouchEvent() 方法獲取到觸摸按下時的點 C 的坐標,以及拖動結(jié)束時的點 M 的坐標:

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                startX = event.getX();
                startY = event.getY();
                break;
            case MotionEvent.ACTION_UP:
                //調(diào)用warp方法根據(jù)觸摸屏事件的坐標點來扭曲verts數(shù)組
                warp(startX, startY, event.getX(), event.getY());
                break;
        }
        return true;
    }

定義一下我們局部變形的作用半徑 rmax

//作用范圍半徑
private int r = 100;

接著就是最關(guān)鍵的代碼,這里是將圓形范圍內(nèi)的每一個交叉點的橫縱坐標分別求出其逆變換的坐標,并將求得的值重新賦給這個交叉點,下面將算法轉(zhuǎn)換成java代碼:

    private void warp(float startX, float startY, float endX, float endY) {

        //計算拖動距離
        float ddPull = (endX - startX) * (endX - startX) + (endY - startY) * (endY - startY);
        float dPull = (float) Math.sqrt(ddPull);
        //文獻中提到的算法,并不能很好的實現(xiàn)拖動距離 MC 越大變形效果越明顯的功能,下面這行代碼則是我對該算法的優(yōu)化
        dPull = screenWidth - dPull >= 0.0001f ? screenWidth - dPull : 0.0001f;

        for (int i = 0; i < COUNT * 2; i += 2) {
            //計算每個坐標點與觸摸點之間的距離
            float dx = verts[i] - startX;
            float dy = verts[i + 1] - startY;
            float dd = dx * dx + dy * dy;
            float d = (float) Math.sqrt(dd);

            //文獻中提到的算法同樣不能實現(xiàn)只有圓形選區(qū)內(nèi)的圖像才進行變形的功能,這里需要做一個距離的判斷
            if (d < r) {
                //變形系數(shù),扭曲度
                double e = (r * r - dd) * (r * r - dd) / ((r * r - dd + dPull * dPull) * (r * r - dd + dPull * dPull));
                double pullX = e * (endX - startX);
                double pullY = e * (endY - startY);
                verts[i] = (float) (verts[i] + pullX);
                verts[i + 1] = (float) (verts[i + 1] + pullY);
            }
        }
        invalidate();
    }

好了,代碼寫完了。
說了半天,無圖無真相啊。還是看看我的 Demo 的實現(xiàn)效果吧,看看下面的對比圖,胖哥的腮幫是不是瘦了,當然,本來P圖就是個技術(shù)活,我這里只是隨手推了推胖哥的臉,難免顯得不專業(yè),感興趣的同學可以到文末下載我的 Demo 玩一玩:


Demo效果

寫到這里,大家已經(jīng)可以動手做一個修圖APP出來了,結(jié)合我上一篇文章提到的濾鏡效果,相信大家可以的。

4、補充

我的 Demo 里面加了作用范圍圓形的顯示和瘦臉拖動方向的顯示,以及一鍵復原的按鈕,方便同學們更加直觀的理解和使用。

4.1.添加作用范圍圓形的顯示和瘦臉拖動方向的顯示

在 onDraw() 方法里加上繪制圓形和直線的代碼,如下:

    //是否顯示變形圓圈
    private boolean showCircle;
    //是否顯示變形方向
    private boolean showDirection;

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawBitmapMesh(mBitmap, WIDTH, HEIGHT, verts, 0, null, 0, null);
        if (showCircle) {
            canvas.drawCircle(startX, startY, r, circlePaint);
        }
        if (showDirection) {
            canvas.drawLine(startX, startY, moveX, moveY, directionPaint);
        }
    }

接著重新寫寫 onTouchEvent() 方法里的代碼,在 MotionEvent.ACTION_DOWN 中繪制變形區(qū)域,在 MotionEvent.ACTION_MOVE 中繪制變形方向直線,在 MotionEvent.ACTION_UP 中 去掉變形區(qū)域和變形方向直線,代碼如下:

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                //繪制變形區(qū)域
                startX = event.getX();
                startY = event.getY();
                showCircle = true;
                invalidate();
                break;
            case MotionEvent.ACTION_MOVE:
                //繪制變形方向
                moveX = event.getX();
                moveY = event.getY();
                showDirection = true;
                invalidate();
                break;
            case MotionEvent.ACTION_UP:
                showCircle = false;
                showDirection = false;

                //調(diào)用warp方法根據(jù)觸摸屏事件的坐標點來扭曲verts數(shù)組
                warp(startX, startY, event.getX(), event.getY());
                break;
        }
        return true;
    }
4.2.添加一鍵復原的按鈕

還記得上面提到最初獲取分割圖片的交叉點的坐標,我們將原始坐標保存在了 orig[] 數(shù)組中。這里,當我們點擊復原按鈕,我們就將 orig[] 數(shù)組的值賦給 verts[] 數(shù)組,然后重新繪制即可,很簡單,添加一個接口監(jiān)聽即可,然后在 MainActivity 中調(diào)用一下,代碼如下:

    /**
     * 一鍵恢復
     */
    public void resetView() {
        for (int i = 0; i < verts.length; i++) {
            verts[i] = orig[i];
        }
        onStepChangeListener.onStepChange(true);
        invalidate();
    }

    public void setOnStepChangeListener(IOnStepChangeListener onStepChangeListener) {
        this.onStepChangeListener = onStepChangeListener;
    }

    public interface IOnStepChangeListener {
        void onStepChange(boolean isEmpty);
    }

最后按照慣例,上一個Demo的動態(tài)圖給大家看看吧,我這里就直接將拖動距離加大,好讓大家直觀地看到效果:


MyDrawBitmapMeshDemo

后續(xù)

  1. Demo中還有很多可以完善的細節(jié),這里只做原理分析,感興趣的同學可以繼續(xù)完善,比如,變形區(qū)域的動態(tài)設(shè)置,記錄每一次變形的數(shù)組值用于撤銷上一步操作,等等。同樣的,這里不僅僅可以瘦臉,還可以瘦各種地方。如果需要做拉伸處理,只需要將 verts[] 數(shù)組里的元素做相應(yīng)的處理即可。

  2. 如果對圖像濾鏡效果感興趣,可以看看我的上一篇文章 Android:修圖技術(shù)之濾鏡效果實現(xiàn)及原理分析——ColorMatrix

  3. Demo 下載地址

最后編輯于
?著作權(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)容