1. 程式人生 > >Android開發之影象處理那點事——濾鏡

Android開發之影象處理那點事——濾鏡

code小生,一個專注 Android 領域的技術平臺

公眾號回覆 Android 加入我的安卓技術群

作者:李晨瑋
連結:https://www.jianshu.com/p/0a291bdf72c2
宣告:本文已獲
李晨瑋授權發表,轉發等請聯絡原作者授權

在 Android 開發中,一般對影象的處理就是 Bitmap(點陣圖),它包含了影象的全部資料,即點陣和顏色值,點陣就是包含畫素點的矩陣,而顏色值就是ARGB,分別代表透明、紅色、綠色、藍色通道,它們共同決定了畫素點的顏色,今天我們來講講關於改變影象顏色的相關知識點。
先來一張實現效果圖:

640

濾鏡演示

顏色矩陣

對於影象來說,每一個畫素點都有一個顏色矩陣分量來儲存顏色,即下圖的RGBA1(矩陣C,1表示顏色的偏移量),而在Android系統中,顏色矩陣是用一個4*5的數字矩陣來表示的(矩陣A,由一維陣列構成)。

640?wx_fmt=other
顏色矩陣

它們的乘積(矩陣R)即為螢幕上顯示的影象顏色,這裡的RGBA取值應在0~255之間。

640?wx_fmt=other
顏色矩陣乘積

可能有點懵,沒關係,我們來梳理一下:
1、矩陣C,也就是畫素點的顏色分量,這個很簡單,RGBA1,分別代表紅、綠、藍、透明度和色彩偏移值。
2、矩陣A,它是由一維陣列構成的4 * 5的數字矩陣,其中4(行)分別代表RGBA,而5(列)是用來代表決定4(行)RGBA的RGBA和偏移量,有點繞,舉個例子,第1行是R,然後這個R所呈現的形式是由abcde來共同決定的,比如紅色,它有多種呈現方式,其它3行也是一樣,以此類推即可。

根據線性代數的矩陣乘法,我們可以得出:

   R’ = a * R + b * G + c * B + d *
 A + e;
   G’ = f * R + g * G + h * B + i * A + j;
   B’ = k * R + l * G + m * B + n * A + o;
   A’ = p * R + q * G + r * B + s * A + t;

此時我們得到了新的顏色值R’G’B’A’ ,以第1行R為例,如果我們讓a=1,讓b、c、d、e=0,此時我們可以推匯出R’=1R+0G+0B+0A+0=1R,也就是等於原來的R,依次類推,我們可以構造出下圖矩陣,這樣使得Android的顏色矩陣乘以畫素點的顏色矩陣分量還是等於原來的顏色值,這個矩陣也被稱為單位矩陣,一般初始化影象的時候,我們會構建它。

640?wx_fmt=other
單位矩陣

我們上面說了矩陣R是矩陣A和矩陣C的乘積,即為螢幕上所顯示的影象顏色,那麼如果我們要改變影象的顏色要通過什麼方法呢?哈哈,相信你已經知道了,沒錯,要麼改變矩陣A(Android顏色矩陣),要麼改變矩陣C(影象顏色矩陣),我們來寫個Demo程式,驗證一下,我們構建一個介面,由ImageView和20個EditText組成(矩陣AC乘積),如圖所示:

640?wx_fmt=other
初始圖片

現在我們改變下第一行(R通道)的最後一個數值,也就是R通道的顏色偏移值,我們改成100,看看圖片效果:

640?wx_fmt=other
改變紅色通道偏移值

我們再來改變下第二行(G通道)的最後一個數值,也就是G通道的顏色偏移值,我們改成100,看看圖片效果:

640?wx_fmt=other
改變綠色通道偏移值

和我們預想的是一樣的,因為在某顏色通道上偏移了值,那麼該圖片也就會向某顏色通道偏移顏色。

再來看下直接改變顏色系數,我們改變第三行(G通道)的值,把1修改成2,看看效果:

640?wx_fmt=other
改變藍色通道值

上面的效果驗證是成功的,圖片偏向了藍色,當然顏色的改變是可以發生疊加的,比如三原色中,我們知道紅色和綠色的混合後是黃色,如下圖所示:

640?wx_fmt=other
三原色

我們試著偏移這兩個顏色通道,把R和G的顏色通道都偏移了100,看看效果:

640?wx_fmt=other
image.png

非常的nice,驗證成功,改變影象顏色的原理其實就是這個,雖然谷歌已經給我們提供了相關的類庫,我們只需要簡單的呼叫api就可以實現效果,不過我覺得學東西應該知其然更應該知其所以然,因為在未來的很多擴充套件中,這些原理性的東西會顯得格外的重要,萬丈高樓平地起,好了,現在我們可以來看下相關api了,這篇文章就圍繞著一個類來講——ColorMatrix。

ColorMatrix

關於這個ColorMatrix,其實它就是我們上面分析的那個4 * 5的數字矩陣,只是谷歌幫我們在這個類中封裝好了許多簡易的操作方法。
我們先來看下剛才那個Demo程式的實現,我們直接看核心程式碼:

ColorMatrix colorMatrix = new ColorMatrix();
colorMatrix.set(float[] src);
mImageView.setColorFilter(new ColorMatrixColorFilter(colorMatrix));

首先我們構建出ColorMatrix物件,然後通過set方法把對應的4 * 5數字矩陣(一維陣列)傳入,根據它我們就可以構建出顏色過濾器ColorMatrixColorFilter,再通過ImageView設定即可,其實重點就在於這個4 * 5陣列矩陣的數值,要搭配出好看的顏色濾鏡,這個就需要專業做視覺效果和演算法的同學提供了,我在Demo程式中整理了一些,文章末尾會給出Demo下載地址。

除了以上簡單粗暴的set對應的顏色矩陣,ColorMatrix類中還提供了許多方法,比如可以調整影象的色相、飽和度、灰度等,還是以Demo的形式展開:

640?wx_fmt=other
調整色相、飽和度、灰度

來看下核心程式碼:

    /**
     * 調整圖片的色相,飽和度,灰度  
     *
     * @param srcBitmap
     * @param rotate
     * @param saturation
     * @param scale
     * @return
     */

    public static Bitmap beautyImage(Bitmap srcBitmap, float rotate, float saturation, float scale) {

        //調整色相
        ColorMatrix rotateMatrix = new ColorMatrix();
        rotateMatrix.setRotate(0, rotate);
        rotateMatrix.setRotate(1, rotate);
        rotateMatrix.setRotate(2, rotate);

        //調整色彩飽和度
        ColorMatrix saturationMatrix = new ColorMatrix();
        saturationMatrix.setSaturation(saturation);

        //調整灰度
        ColorMatrix scaleMatrix = new ColorMatrix();
        scaleMatrix.setScale(scale, scale, scale, 1);

        //疊加效果
        ColorMatrix colorMatrix = new ColorMatrix();
        colorMatrix.postConcat(rotateMatrix);
        colorMatrix.postConcat(saturationMatrix);
        colorMatrix.postConcat(scaleMatrix);

        //建立一個大小相同的空白Bitmap
        Bitmap dstBitmap = Bitmap.createBitmap(srcBitmap.getWidth(), srcBitmap.getHeight(), Bitmap.Config.ARGB_8888);
        //載入Canvas,Paint
        Canvas canvas = new Canvas(dstBitmap);
        Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
        paint.setColorFilter(new ColorMatrixColorFilter(colorMatrix));
        //繪圖
        canvas.drawBitmap(srcBitmap, 00, paint);

        return dstBitmap;
    }

依葫蘆畫瓢,我們構建出 ColorMatrix物件,這邊提供了三個方法:setRotate、saturationMatrix、scaleMatrix分別代表設定圖象的色相、灰度、飽和度,來解釋下這3個詞:
色相:物體傳播的顏色。
飽和度:顏色的純度,從0到100。
灰度:顏色相對的明暗程度。

我們挑其中一個方法來講:

    /**
     * Set the rotation on a color axis by the specified values.
     * <p>
     * <code>axis=0</code> correspond to a rotation around the RED color
     * <code>axis=1</code> correspond to a rotation around the GREEN color
     * <code>axis=2</code> correspond to a rotation around the BLUE color
     * </p>
     */

    public void setRotate(int axis, float degrees) {
        reset();
        double radians = degrees * Math.PI / 180d;
        float cosine = (float) Math.cos(radians);
        float sine = (float) Math.sin(radians);
        switch (axis) {
        // Rotation around the red color
        case 0:
            mArray[6] = mArray[12] = cosine;
            mArray[7] = sine;
            mArray[11] = -sine;
            break;
        // Rotation around the green color
        case 1:
            mArray[0] = mArray[12] = cosine;
            mArray[2] = -sine;
            mArray[10] = sine;
            break;
        // Rotation around the blue color
        case 2:
            mArray[0] = mArray[6] = cosine;
            mArray[1] = sine;
            mArray[5] = -sine;
            break;
        default:
            throw new RuntimeException();
        }
    }

上面是 setRotate 的原始碼,根據註釋我們可以知道第1個引數需要傳入0、1、2,分別代表了RGB三種顏色通道,第2個引數是指程度的大小。從原始碼中我們可以發現,它的原理就是我們文章前面所提到的4 * 5的顏色矩陣,中間涉及到了一些角度的旋轉,大概瞭解一下,把RGB用三維座標系來表示,單位長度均為1,然後根據設定第1個引數值和第2個引數值來決定圍繞哪個點旋轉和旋轉多少角度,根據三角函式的換算就可以得到對一個的偏移值了,這個我們瞭解即可,有興趣的朋友可以自行查閱資料。剩下的2個方法saturationMatrix,scaleMatrix原理都是一樣的,都是對顏色矩陣的數值進行操作而已,大家自行查閱,這裡就不貼原始碼了。
附上滑竿傳遞值的程式碼(滑竿值的換算公式,這個是比較經典的做法,我也是通過查資料得知,瞭解即可):

    @Override
    public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
        switch (seekBar.getId()) {
            case R.id.seekBar_rotate:
                mRotate = (mRotateSeekBar.getProgress() - 128f) * 1.0f / 128f * 180;
                break;
            case R.id.seekBar_saturation:
                mSaturation = mSaturationSeekBar.getProgress() / 128f;
                break;
            case R.id.seekBar_scale:
                mScale = mScaleSeekBar.getProgress() / 128f;
                break;
        }

        if (mBitmap != null) {
            Bitmap bitmap = BeautyUtil.beautyImage(mBitmap, mRotate, mSaturation, mScale);
            mImageView.setImageBitmap(bitmap);
        }
    }

更改畫素點的 RGBA

要改變圖片的顏色值,這裡還有一種更加精確的做法,即對每個畫素點進行RGBA的修改,我們以實現底片的效果為例,來看下核心程式碼:

    /**
     * 通過更改圖片畫素點的RGBA值,生成底片效果
     * @param scrBitmap
     * @return
     */

    public static Bitmap beautyImage(Bitmap scrBitmap) {

        int width = scrBitmap.getWidth();
        int height = scrBitmap.getHeight();
        int count = width * height;

        int[] oldPixels = new int[count];
        int[] newPixels = new int[count];

        scrBitmap.getPixels(oldPixels, 0, width, 00, width, height);

        for (int i = 0; i < oldPixels.length; i++) {
            int pixel = oldPixels[i];
            int r = Color.red(pixel);
            int g = Color.green(pixel);
            int b = Color.blue(pixel);

            r = 255 - r;
            g = 255 - g;
            b = 255 - b;

            if (r > 255) {
                r = 255;
            }
            if (g > 255) {
                g = 255;
            }
            if (b > 255) {
                b = 255;
            }

            if (r < 0) {
                r = 0;
            }
            if (g < 0) {
                g = 0;
            }
            if (b < 0) {
                b = 0;
            }

            newPixels[i] = Color.rgb(r, g, b);

        }

        Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
        bitmap.setPixels(newPixels, 0, width, 00, width, height);

        return bitmap;
    }

我們需要知道生成底片的公式(網上有很多成熟的效果公式,有興趣的朋友可以自行搜尋下,這邊只是舉例,重點講原理):

r=255-r;
g=255-g;
b=255-b;

首先我們呼叫Bitmap的getPixels方法,把畫素資料存放在int數組裡,然後根據Color.red,Color.green,Color.blue方法取出這些畫素點的RGB值,然後進行公式的換算和邊界值處理,再呼叫setPixels設定回建立的新的Bitmap,這樣就完成了我們的底片效果了,來看下效果圖:

640?wx_fmt=other
底片效果圖

補充

到這裡文章就結束了,以上的效果因為只是Demo演示,有些細節是沒有做處理的,在實際開發中,大家還是記憶體相關的問題,這邊最後再說幾句:
1、Bitmap的壓縮載入以及回收要注意場合,不是所有時候都需要載入原圖的,請根據業務場景合理規劃。
2、ColorMatrix只是簡單的對RGBA通道做了值的調整,如果想實現更加精細化的效果,還是需要配合底層C++來實現會更好。
3、市面上的一些美顏APP,其實並不是用如上方法來實現的,濾鏡是個很複雜的東西,為了更好的渲染效能,達到實時美顏的效果,一半我們會採用GPU(OpenGL)來繪製,GPU可以達到畫素點近毫秒級的併發處理,後續我也會寫一些相關的文章。

最後來張最近被玩壞的王校長,IG牛逼~

640?wx_fmt=other
IG牛逼

原始碼下載:
這裡附上原始碼地址:BeautyImageDemo
https://github.com/Lichenwei-Dev/BeautyImageDemo

640

分享技術我是認真的