库昊天 發表於 2019-5-20 17:27:00

Android开发之图像处理那点事——滤镜

<div>
<div>
<p>在Android开发中,一般对图像的处理就是Bitmap(位图),它包含了图像的全部数据,即点阵和颜色值,点阵就是包含像素点的矩阵,而颜色值就是ARGB,分别代表透明、红色、绿色、蓝色通道,它们共同决定了像素点的颜色,今天我们来讲讲关于改变图像颜色的相关知识点。<br>
先来一张实现效果图:</p>
<div class="image-package">
<div class="image-container" style="max-width: 456px; max-height: 1228px">
<div class="image-view" data-width="456" data-height="800"><img src="//upload-images.jianshu.io/upload_images/2189443-9b6185504adb9339.gif?imageMogr2/auto-orient/strip%7CimageView2/2/w/456"></div>


</div>
<div class="image-caption">滤镜演示</div>


</div>
<h3>颜色矩阵</h3>
<p>对于图像来说,每一个像素点都有一个颜色矩阵分量来保存颜色,即下图的RGBA1(矩阵C,1表示颜色的偏移量),而在Android系统中,颜色矩阵是用一个4*5的数字矩阵来表示的(矩阵A,由一维数组构成)。</p>
<div class="image-package">
<div class="image-container" style="max-width: 700px; max-height: 292px">
<div class="image-view" data-width="726" data-height="292"><img src="//upload-images.jianshu.io/upload_images/2189443-adeaaeaccad3fff3.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/726"></div>


</div>
<div class="image-caption">颜色矩阵</div>


</div>
<p>它们的乘积(矩阵R)即为屏幕上显示的图像颜色,这里的RGBA取值应在0~255之间。</p>


<br>
<div class="image-package">
<div class="image-container" style="max-width: 700px; max-height: 290px">
<div class="image-view" data-width="1414" data-height="290"><img src="//upload-images.jianshu.io/upload_images/2189443-7940dc9628c48051.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1000"></div>


</div>
<div class="image-caption">颜色矩阵乘积</div>


</div>
<p>可能有点懵,没关系,我们来梳理一下:<br>
1、矩阵C,也就是像素点的颜色分量,这个很简单,RGBA1,分别代表红、绿、蓝、透明度和色彩偏移值。<br>
2、矩阵A,它是由一维数组构成的4 * 5的数字矩阵,其中4(行)分别代表RGBA,而5(列)是用来代表决定4(行)RGBA的RGBA和偏移量,有点绕,举个例子,第1行是R,然后这个R所呈现的形式是由abcde来共同决定的,比如红色,它有多种呈现方式,其它3行也是一样,以此类推即可。</p>
<p>根据线性代数的矩阵乘法,我们可以得出:</p>
<pre class="hljs undefined"><code>   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;
</code></pre>
<p>此时我们得到了新的颜色值R’G’B’A’ ,以第1行R为例,如果我们让a=1,让b、c、d、e=0,此时我们可以推导出R’=1R+0G+0B+0A+0=1R,也就是等于原来的R,依次类推,我们可以构造出下图矩阵,这样使得Android的颜色矩阵乘以像素点的颜色矩阵分量还是等于原来的颜色值,这个矩阵也被称为单位矩阵,一般初始化图像的时候,我们会构建它。</p>
<div class="image-package">
<div class="image-container" style="max-width: 452px; max-height: 278px">
<div class="image-view" data-width="452" data-height="278"><img src="//upload-images.jianshu.io/upload_images/2189443-4f21d358da3d3d10.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/452"></div>
</div>
<div class="image-caption">单位矩阵</div>
</div>
<p>我们上面说了矩阵R是矩阵A和矩阵C的乘积,即为屏幕上所显示的图像颜色,那么如果我们要改变图像的颜色要通过什么方法呢?哈哈,相信你已经知道了,没错,要么改变矩阵A(Android颜色矩阵),要么改变矩阵C(图像颜色矩阵),我们来写个Demo程序,验证一下,我们构建一个界面,由ImageView和20个EditText组成(矩阵AC乘积),如图所示:</p>
<div class="image-package">
<div class="image-container" style="max-width: 700px; max-height: 1100px">
<div class="image-view" data-width="724" data-height="1138"><img src="//upload-images.jianshu.io/upload_images/2189443-19aaa8d77e9b406e.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/724"></div>
</div>
<div class="image-caption">初始图片</div>
</div>
<p>现在我们改变下第一行(R通道)的最后一个数值,也就是R通道的颜色偏移值,我们改成100,看看图片效果:</p>
<div class="image-package">
<div class="image-container" style="max-width: 700px; max-height: 1111px">
<div class="image-view" data-width="722" data-height="1146"><img src="//upload-images.jianshu.io/upload_images/2189443-7dd7efad80b8489c.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/722"></div>
</div>
<div class="image-caption">改变红色通道偏移值</div>
</div>
<p>我们再来改变下第二行(G通道)的最后一个数值,也就是G通道的颜色偏移值,我们改成100,看看图片效果:</p>
<div class="image-package">
<div class="image-container" style="max-width: 700px; max-height: 1103px">
<div class="image-view" data-width="728" data-height="1148"><img src="//upload-images.jianshu.io/upload_images/2189443-3e67647898ade31d.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/728"></div>
</div>
<div class="image-caption">改变绿色通道偏移值</div>
</div>
<p>和我们预想的是一样的,因为在某颜色通道上偏移了值,那么该图片也就会向某颜色通道偏移颜色。</p>
<p>再来看下直接改变颜色系数,我们改变第三行(G通道)的值,把1修改成2,看看效果:</p>
<div class="image-package">
<div class="image-container" style="max-width: 700px; max-height: 1105px">
<div class="image-view" data-width="722" data-height="1140"><img src="//upload-images.jianshu.io/upload_images/2189443-0c0ff8dacb87d091.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/722"></div>
</div>
<div class="image-caption">改变蓝色通道值</div>
</div>
<p>上面的效果验证是成功的,图片偏向了蓝色,当然颜色的改变是可以发生叠加的,比如三原色中,我们知道红色和绿色的混合后是黄色,如下图所示:</p>
<div class="image-package">
<div class="image-container" style="max-width: 700px; max-height: 607px">
<div class="image-view" data-width="878" data-height="762"><img src="//upload-images.jianshu.io/upload_images/2189443-1793ef4f03eff5b5.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/878"></div>
</div>
<div class="image-caption">三原色</div>
</div>
<p>我们试着偏移这两个颜色通道,把R和G的颜色通道都偏移了100,看看效果:</p>
<div class="image-package">
<div class="image-container" style="max-width: 700px; max-height: 1119px">
<div class="image-view" data-width="718" data-height="1148"><img src="//upload-images.jianshu.io/upload_images/2189443-b40a31d53f0afbba.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/718"></div>
</div>
<div class="image-caption">image.png</div>
</div>
<p>非常的nice,验证成功,改变图像颜色的原理其实就是这个,虽然谷歌已经给我们提供了相关的类库,我们只需要简单的调用api就可以实现效果,不过我觉得学东西应该知其然更应该知其所以然,因为在未来的很多扩展中,这些原理性的东西会显得格外的重要,万丈高楼平地起,好了,现在我们可以来看下相关api了,这篇文章就围绕着一个类来讲——ColorMatrix。</p>
<h2>ColorMatrix</h2>
<p>关于这个ColorMatrix,其实它就是我们上面分析的那个4 * 5的数字矩阵,只是谷歌帮我们在这个类中封装好了许多简易的操作方法。<br>
我们先来看下刚才那个Demo程序的实现,我们直接看核心代码:</p>
<pre class="hljs cpp"><code class="cpp">                ColorMatrix colorMatrix = <span class="hljs-keyword">new ColorMatrix();
                colorMatrix.<span class="hljs-built_in">set(<span class="hljs-keyword">float[] src);
                mImageView.setColorFilter(<span class="hljs-keyword">new ColorMatrixColorFilter(colorMatrix));
</span></span></span></span></code></pre>
<p>首先我们构建出ColorMatrix对象,然后通过set方法把对应的4 * 5数字矩阵(一维数组)传入,根据它我们就可以构建出颜色过滤器ColorMatrixColorFilter,再通过ImageView设置即可,其实重点就在于这个4 * 5数组矩阵的数值,要搭配出好看的颜色滤镜,这个就需要专业做视觉效果和算法的同学提供了,我在Demo程序中整理了一些,文章末尾会给出Demo下载地址。</p>
<p>除了以上简单粗暴的set对应的颜色矩阵,ColorMatrix类中还提供了许多方法,比如可以调整图像的色相、饱和度、灰度等,还是以Demo的形式展开:</p>
<div class="image-package">
<div class="image-container" style="max-width: 376px; max-height: 538px">
<div class="image-view" data-width="376" data-height="538"><img src="//upload-images.jianshu.io/upload_images/2189443-83af580a15156e5a.gif?imageMogr2/auto-orient/strip%7CimageView2/2/w/376"></div>
</div>
<div class="image-caption">调整色相、饱和度、灰度</div>
</div>
<p>来看下核心代码:</p>
<pre class="hljs java"><code class="java">    <span class="hljs-comment">/**
   * 调整图片的色相,饱和度,灰度
   *
   * <span class="hljs-doctag">@param srcBitmap
   * <span class="hljs-doctag">@param rotate
   * <span class="hljs-doctag">@param saturation
   * <span class="hljs-doctag">@param scale
   * <span class="hljs-doctag">@return
   */
    <span class="hljs-function"><span class="hljs-keyword">public <span class="hljs-keyword">static Bitmap <span class="hljs-title">beautyImage<span class="hljs-params">(Bitmap srcBitmap, <span class="hljs-keyword">float rotate, <span class="hljs-keyword">float saturation, <span class="hljs-keyword">float scale) {

      <span class="hljs-comment">//调整色相
      ColorMatrix rotateMatrix = <span class="hljs-keyword">new ColorMatrix();
      rotateMatrix.setRotate(<span class="hljs-number">0, rotate);
      rotateMatrix.setRotate(<span class="hljs-number">1, rotate);
      rotateMatrix.setRotate(<span class="hljs-number">2, rotate);

      <span class="hljs-comment">//调整色彩饱和度
      ColorMatrix saturationMatrix = <span class="hljs-keyword">new ColorMatrix();
      saturationMatrix.setSaturation(saturation);

      <span class="hljs-comment">//调整灰度
      ColorMatrix scaleMatrix = <span class="hljs-keyword">new ColorMatrix();
      scaleMatrix.setScale(scale, scale, scale, <span class="hljs-number">1);

      <span class="hljs-comment">//叠加效果
      ColorMatrix colorMatrix = <span class="hljs-keyword">new ColorMatrix();
      colorMatrix.postConcat(rotateMatrix);
      colorMatrix.postConcat(saturationMatrix);
      colorMatrix.postConcat(scaleMatrix);

      <span class="hljs-comment">//创建一个大小相同的空白Bitmap
      Bitmap dstBitmap = Bitmap.createBitmap(srcBitmap.getWidth(), srcBitmap.getHeight(), Bitmap.Config.ARGB_8888);
      <span class="hljs-comment">//载入Canvas,Paint
      Canvas canvas = <span class="hljs-keyword">new Canvas(dstBitmap);
      Paint paint = <span class="hljs-keyword">new Paint(Paint.ANTI_ALIAS_FLAG);
      paint.setColorFilter(<span class="hljs-keyword">new ColorMatrixColorFilter(colorMatrix));
      <span class="hljs-comment">//绘图
      canvas.drawBitmap(srcBitmap, <span class="hljs-number">0, <span class="hljs-number">0, paint);

      <span class="hljs-keyword">return dstBitmap;
    }
</span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></code></pre>
<p>依葫芦画瓢,我们构建出ColorMatrix对象,这边提供了三个方法:setRotate、saturationMatrix、scaleMatrix分别代表设置图象的色相、灰度、饱和度,来解释下这3个词:<br>
<strong>色相:</strong>物体传播的颜色。<br>
<strong>饱和度:</strong>颜色的纯度,从0到100。<br>
<strong>灰度:</strong>颜色相对的明暗程度。</p>
<p>我们挑其中一个方法来讲:</p>
<pre class="hljs cpp"><code class="cpp">    <span class="hljs-comment">/**
   * Set the rotation on a color axis by the specified values.
   * &lt;p&gt;
   * &lt;code&gt;axis=0&lt;/code&gt; correspond to a rotation around the RED color
   * &lt;code&gt;axis=1&lt;/code&gt; correspond to a rotation around the GREEN color
   * &lt;code&gt;axis=2&lt;/code&gt; correspond to a rotation around the BLUE color
   * &lt;/p&gt;
   */
    <span class="hljs-function"><span class="hljs-keyword">public <span class="hljs-keyword">void <span class="hljs-title">setRotate<span class="hljs-params">(<span class="hljs-keyword">int axis, <span class="hljs-keyword">float degrees) {
      reset();
      <span class="hljs-keyword">double radians = degrees * Math.PI / <span class="hljs-number">180d;
      <span class="hljs-keyword">float cosine = (<span class="hljs-keyword">float) Math.<span class="hljs-built_in">cos(radians);
      <span class="hljs-keyword">float sine = (<span class="hljs-keyword">float) Math.<span class="hljs-built_in">sin(radians);
      <span class="hljs-keyword">switch (axis) {
      <span class="hljs-comment">// Rotation around the red color
      <span class="hljs-keyword">case <span class="hljs-number">0:
            mArray[<span class="hljs-number">6] = mArray[<span class="hljs-number">12] = cosine;
            mArray[<span class="hljs-number">7] = sine;
            mArray[<span class="hljs-number">11] = -sine;
            <span class="hljs-keyword">break;
      <span class="hljs-comment">// Rotation around the green color
      <span class="hljs-keyword">case <span class="hljs-number">1:
            mArray[<span class="hljs-number">0] = mArray[<span class="hljs-number">12] = cosine;
            mArray[<span class="hljs-number">2] = -sine;
            mArray[<span class="hljs-number">10] = sine;
            <span class="hljs-keyword">break;
      <span class="hljs-comment">// Rotation around the blue color
      <span class="hljs-keyword">case <span class="hljs-number">2:
            mArray[<span class="hljs-number">0] = mArray[<span class="hljs-number">6] = cosine;
            mArray[<span class="hljs-number">1] = sine;
            mArray[<span class="hljs-number">5] = -sine;
            <span class="hljs-keyword">break;
      <span class="hljs-keyword">default:
            <span class="hljs-keyword">throw <span class="hljs-keyword">new RuntimeException();
      }
    }
</span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></code></pre>
<p>上面是setRotate的源码,根据注释我们可以知道第1个参数需要传入0、1、2,分别代表了RGB三种颜色通道,第2个参数是指程度的大小。从源码中我们可以发现,它的原理就是我们文章前面所提到的4 * 5的颜色矩阵,中间涉及到了一些角度的旋转,大概了解一下,把RGB用三维坐标系来表示,单位长度均为1,然后根据设置第1个参数值和第2个参数值来决定围绕哪个点旋转和旋转多少角度,根据三角函数的换算就可以得到对一个的偏移值了,这个我们了解即可,有兴趣的朋友可以自行查阅资料。剩下的2个方法saturationMatrix,scaleMatrix原理都是一样的,都是对颜色矩阵的数值进行操作而已,大家自行查阅,这里就不贴源码了。<br>
附上滑竿传递值的代码(滑竿值的换算公式,这个是比较经典的做法,我也是通过查资料得知,了解即可):</p>
<pre class="hljs java"><code class="java">    <span class="hljs-meta">@Override
    <span class="hljs-function"><span class="hljs-keyword">public <span class="hljs-keyword">void <span class="hljs-title">onProgressChanged<span class="hljs-params">(SeekBar seekBar, <span class="hljs-keyword">int progress, <span class="hljs-keyword">boolean fromUser) {
      <span class="hljs-keyword">switch (seekBar.getId()) {
            <span class="hljs-keyword">case R.id.seekBar_rotate:
                mRotate = (mRotateSeekBar.getProgress() - <span class="hljs-number">128f) * <span class="hljs-number">1.0f / <span class="hljs-number">128f * <span class="hljs-number">180;
                <span class="hljs-keyword">break;
            <span class="hljs-keyword">case R.id.seekBar_saturation:
                mSaturation = mSaturationSeekBar.getProgress() / <span class="hljs-number">128f;
                <span class="hljs-keyword">break;
            <span class="hljs-keyword">case R.id.seekBar_scale:
                mScale = mScaleSeekBar.getProgress() / <span class="hljs-number">128f;
                <span class="hljs-keyword">break;
      }

      <span class="hljs-keyword">if (mBitmap != <span class="hljs-keyword">null) {
            Bitmap bitmap = BeautyUtil.beautyImage(mBitmap, mRotate, mSaturation, mScale);
            mImageView.setImageBitmap(bitmap);
      }

    }
</span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></code></pre>
<h3>更改像素点的RGBA</h3>
<p>要改变图片的颜色值,这里还有一种更加精确的做法,即对每个像素点进行RGBA的修改,我们以实现底片的效果为例,来看下核心代码:</p>
<pre class="hljs java"><code class="java">    <span class="hljs-comment">/**
   * 通过更改图片像素点的RGBA值,生成底片效果
   * <span class="hljs-doctag">@param scrBitmap
   * <span class="hljs-doctag">@return
   */
    <span class="hljs-function"><span class="hljs-keyword">public <span class="hljs-keyword">static Bitmap <span class="hljs-title">beautyImage<span class="hljs-params">(Bitmap scrBitmap) {

      <span class="hljs-keyword">int width = scrBitmap.getWidth();
      <span class="hljs-keyword">int height = scrBitmap.getHeight();
      <span class="hljs-keyword">int count = width * height;

      <span class="hljs-keyword">int[] oldPixels = <span class="hljs-keyword">new <span class="hljs-keyword">int;
      <span class="hljs-keyword">int[] newPixels = <span class="hljs-keyword">new <span class="hljs-keyword">int;

      scrBitmap.getPixels(oldPixels, <span class="hljs-number">0, width, <span class="hljs-number">0, <span class="hljs-number">0, width, height);

      <span class="hljs-keyword">for (<span class="hljs-keyword">int i = <span class="hljs-number">0; i &lt; oldPixels.length; i++) {
            <span class="hljs-keyword">int pixel = oldPixels;
            <span class="hljs-keyword">int r = Color.red(pixel);
            <span class="hljs-keyword">int g = Color.green(pixel);
            <span class="hljs-keyword">int b = Color.blue(pixel);

            r = <span class="hljs-number">255 - r;
            g = <span class="hljs-number">255 - g;
            b = <span class="hljs-number">255 - b;

            <span class="hljs-keyword">if (r &gt; <span class="hljs-number">255) {
                r = <span class="hljs-number">255;
            }
            <span class="hljs-keyword">if (g &gt; <span class="hljs-number">255) {
                g = <span class="hljs-number">255;
            }
            <span class="hljs-keyword">if (b &gt; <span class="hljs-number">255) {
                b = <span class="hljs-number">255;
            }

            <span class="hljs-keyword">if (r &lt; <span class="hljs-number">0) {
                r = <span class="hljs-number">0;
            }
            <span class="hljs-keyword">if (g &lt; <span class="hljs-number">0) {
                g = <span class="hljs-number">0;
            }
            <span class="hljs-keyword">if (b &lt; <span class="hljs-number">0) {
                b = <span class="hljs-number">0;
            }

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

      }

      Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
      bitmap.setPixels(newPixels, <span class="hljs-number">0, width, <span class="hljs-number">0, <span class="hljs-number">0, width, height);

      <span class="hljs-keyword">return bitmap;
    }
</span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></code></pre>
<p>我们需要知道生成底片的公式(网上有很多成熟的效果公式,有兴趣的朋友可以自行搜索下,这边只是举例,重点讲原理):</p>
<pre class="hljs undefined"><code>r=255-r;
g=255-g;
b=255-b;
</code></pre>
<p>首先我们调用Bitmap的getPixels方法,把像素数据存放在int数组里,然后根据Color.red,Color.green,Color.blue方法取出这些像素点的RGB值,然后进行公式的换算和边界值处理,再调用setPixels设置回创建的新的Bitmap,这样就完成了我们的底片效果了,来看下效果图:</p>
<div class="image-package">
<div class="image-container" style="max-width: 700px; max-height: 444px">
<div class="image-view" data-width="724" data-height="444"><img src="//upload-images.jianshu.io/upload_images/2189443-65967931af11c981.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/724"></div>
</div>
<div class="image-caption">底片效果图</div>
</div>
<h3>补充</h3>
<p>到这里文章就结束了,以上的效果因为只是Demo演示,有些细节是没有做处理的,在实际开发中,大家还是内存相关的问题,这边最后再说几句:<br>
1、Bitmap的压缩加载以及回收要注意场合,不是所有时候都需要加载原图的,请根据业务场景合理规划。<br>
2、ColorMatrix只是简单的对RGBA通道做了值的调整,如果想实现更加精细化的效果,还是需要配合底层C++来实现会更好。<br>
3、市面上的一些美颜APP,其实并不是用如上方法来实现的,滤镜是个很复杂的东西,为了更好的渲染性能,达到实时美颜的效果,一半我们会采用GPU(OpenGL)来绘制,GPU可以达到像素点近毫秒级的并发处理,后续我也会写一些相关的文章。</p>
<p>最后来张最近被玩坏的王校长,IG牛逼~</p>
<div class="image-package">
<div class="image-container" style="max-width: 551px; max-height: 1016px">
<div class="image-view" data-width="551" data-height="800"><img src="//upload-images.jianshu.io/upload_images/2189443-2467a1908225cd66.gif?imageMogr2/auto-orient/strip%7CimageView2/2/w/551"></div>


</div>
<div class="image-caption">IG牛逼</div>


</div>
<h3>源码下载:</h3>
<p>这里附上源码地址(欢迎Star,欢迎Fork):BeautyImageDemo</p>

</div>

</div><br><br>
来源:https://www.cnblogs.com/Free-Thinker/p/10895353.html
頁: [1]
查看完整版本: Android开发之图像处理那点事——滤镜