自从苹果开始在iOS系统里采用毛玻璃这种UI效果后,很多手机应用的界面设计都开始引入毛玻璃效果了。其实对于移动端开发人员而言,实现一个毛玻璃效果并不难,Android端有RenderScript、StackBlur等,网上很多经验丰富的工程师也给出了各种解决方案(Android 5.0 下毛玻璃(磨砂)效果如何实现?),所以今天这篇博客不会着重介绍怎样又快又好地实现毛玻璃效果,只会介绍一下模糊效果的大概原理。急着实现效果的同学可以移步Github,哈哈。
首先,简单说几个概念,我们平时实现的毛玻璃效果,基本都是通过对一幅图片进行预处理、高斯模糊实现的。也正如我们所看到的毛玻璃效果那样,实现毛玻璃效果的关键就是让图片变模糊。
朴素的模糊思想
相信大部分人都知道一个图片在Android端是怎样表示的,就是Drawable。其中Drawable作为一个抽象基类下面又派生出很多具体的图片类,如ColorDrawable、BitmapDrawable、LayerDrawable、ShapeDrawable等等,其中BitmapDrawable是根据一个像素矩阵显示对应的图像的,也是今天进行模糊处理的主角。
每个BitmapDrawable内部都持有一个Bitmap引用,这个Bitmap类盛放着像素矩阵。其中每个像素有自己的颜色,而这个像素点的具体颜色的表示形式又取决于Bitmap的Config。
Bitmap的Config有以下几种:
- ARGB_8888
大部分图像的像素点的存储形式都是ARGB_8888。这种格式的图像中每个像素点有4个颜色通道,其中A表示alpha,也就是透明通道,另外3个通道RGB则分别表示三原色:红、绿和蓝。这4个通道每个各占8bit,也就是一个字节。4个通道一共占4个字节,也就是一个int类型的大小。所以对于ARGB型的Bitmap,我们可以用一个int型的变量来表示其中的一个像素点。 - ARGB_4444
每个通道占4bit,一个像素点占2个字节。已经被Google标注为Deprecated,理由是图像质量太差。 - RGB_565
透明通道被去掉,3个通道共占2个字节。这个格式还是会用到的,因为大部分图片是不会用到透明通道的,我们可以通过使用这种格式来降低图片尺寸,进一步降低内存占用率。 - ALPHA_8
没有红绿蓝的通道,一般用于表示掩码。
以上是Bitmap.Config的枚举值,实际在图像处理中我们还会经常用到另一种图像,就是灰度图。对于一副灰度图,每个像素点只有一个灰度通道,灰度值的取值范围从0到255,从一副RGB图转化到灰度图很简单,就是每个像素点的灰度取R、G、B的平均值。比如一个绿色的像素点0xFF00FF00,对应的灰度值就是
(0 + 255 + 0) / 3 = 85
以ARGB_8888为例,下图说明一个像素点各通道是怎样存储的。
从上图可以看到,一个像素周围有8个邻接像素。那么,想要使图片变得模糊,一个很简单的想法就是:
让这个像素的颜色值取周围9个像素的平均值。
那么对于原图片P到模糊之后的图片Q,对于灰度图,我们可以有以下公式:
q(i,j)表示第i行,第j列被模糊化处理之后的像素。p(k,l)表示原图中的像素点。
说白了就是对上图中红框框里的像素点(也把红框框叫”盒子“)取平均值,得到的值就是3x3的小方块中心那个像素点的新的灰度值。刚才说的是对灰度图的处理,对于ARGB图像的处理也相似,可以分别对每个通道求平均值,然后合在一起。
还有一个问题,对于一个Bitmap非边界处的像素点,可以找出3x3的小方块并求出平均值,但是对于边界处,3x3的小方块必然会越界,可以有多种处理方案,一是收缩小方块,比如对于q11,把方块缩小到2x2。也可以在越界处复制一些"虚拟像素",将越界的像素赋值为方块中心的像素的颜色值。
按照以上的想法,可以在Android平台写出以下代码来对一个ImageView所包含的BitmapDrawable进行模糊化处理:
private class MeanFilterTask extends AsyncTask{
private Bitmap bitmap;//源Bitmap
private Bitmap target;//模糊后的Bitmap
public MeanFilterTask(ImageView imageViewTarget){
super();
Drawable drawable = imageViewTarget.getDrawable();
bitmap = ((BitmapDrawable) drawable).getBitmap();
bitmap = Bitmap.createScaledBitmap(bitmap, bitmap.getWidth() / 10, bitmap.getHeight() / 10, false);//缩小图片,减少运算量,改善模糊效果
}
@Override
protected void onPreExecute() {
super.onPreExecute();
target = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(), bitmap.getConfig());
}
@Override
protected Void doInBackground(Void... params) {
meanBlur(bitmap, target);//均值模糊
return null;
}
@Override
protected void onProgressUpdate(Double... values) {
super.onProgressUpdate(values);
}
@Override
protected void onPostExecute(Void aVoid) {
super.onPostExecute(aVoid);
imageTarget.setImageDrawable(new BitmapDrawable(getResources(), target));
}
}
/**
* 均衡模糊
* @param src 源Bitmap
* @param target 目标Bitmap
*/
private void meanBlur(Bitmap src, Bitmap target) {
if (src.getConfig() != Bitmap.Config.ARGB_8888) {//简化考虑,只支持ARGB8888
return;
}
int boxWidth = 3;// 定义一个3x3的盒子,对盒子内的像素点取平均
int boxHeight = 3;
for (int i = 0; i < src.getHeight(); i ++){
for (int j = 0; j < src.getWidth(); j ++){
int meanPixel = filter(boxWidth, boxHeight, i, j, src, new MeanFilter(boxHeight, boxWidth));// 求平均值
target.setPixel(j, i, meanPixel);// 写入像素点
}
}
}
/**
* 根据滤波模板进行滤波
* @param boxWidth 盒子宽度(此处为3)
* @param boxHeight 盒子高度(此处为3)
* @param rowIndex targetBitmap的目标像素点在第xx行
* @param colIndex targetBitmap的目标像素点在第xx列
* @param src 源Bitmap
* @param filter 滤波模板
* @return
*/
private int filter(int boxWidth, int boxHeight, int rowIndex, int colIndex, Bitmap src, Filter filter){
if ( boxWidth % 2 == 0 || boxHeight % 2 == 0)
return 0;
int targetPixel = 0xff000000;//计算的结果
int redSum = 0;
int greenSum = 0;
int blueSum = 0;
int temp;
for (int i = rowIndex - boxHeight / 2, boxRow = 0; i <= rowIndex + boxHeight / 2; i ++, boxRow ++){
for (int j = colIndex - boxWidth / 2, boxCol = 0; j <= colIndex + boxWidth / 2; j ++, boxCol ++){
if (i < 0 || i >= src.getHeight() || j < 0 || j >= src.getWidth()) //越界
temp = src.getPixel(colIndex, rowIndex);
else
temp = src.getPixel(j, i);//依次取出盒子内的像素点
redSum += ((temp & 0x00ff0000) >> 16) * filter.weight(boxRow, boxCol);//求均值,先计算sum,对于 均值模糊, 这里的weight为1
greenSum += ((temp & 0x0000ff00) >> 8) * filter.weight(boxRow, boxCol);//求均值,先计算sum,对于 均值模糊, 这里的weight为1
blueSum += (temp & 0x000000ff) * filter.weight(boxRow, boxCol);//求均值,先计算sum,对于 均值模糊, 这里的weight为1
}
}
int meanRed = ((int) (redSum * 1.0 / filter.total() )) << 16;//ARGB red通道需要左移16bit归位,对于 均值模糊, 这里的total为9
int meanGreen = ((int) (greenSum * 1.0 / filter.total() )) << 8;//ARGB green通道需要左移8bit归位,对于 均值模糊, 这里的total为9
int meanBlue = ((int) (blueSum * 1.0 / filter.total() ));//,对于 均值模糊, 这里的total为9
targetPixel = (targetPixel | meanRed | meanGreen | meanBlue);//或运算 将3个求均值的结果合一
return targetPixel;
}
以上代码写了注释,相信不难看懂。这里面我又引入了一个滤波模板的概念,也就是Filter接口。相关代码如下:
/**
* Created by zjl on 2016/12/13.
* 滤波模板
*/
public interface Filter {
int weight(int rowIndex, int colIndex);//盒子元素与滤波模板元素做乘积最后求平均,在均值模糊中,这里的weight为1
int total();//前面求出sum然后除以total求出最后的平均颜色值
}
/**
* Created by zjl on 2016/12/13.
* 平均模糊的滤波模板
*/
class MeanFilter implements Filter {
private int width;//盒子的宽度,文章假设为3x3的盒子
private int height;
public MeanFilter(int boxHeight, int boxWidth) {
this.height = boxHeight;
this.width = boxWidth;
}
@Override
public int weight(int rowIndex, int colIndex) {
return 1;
}
@Override
public int total() {
return width * height;
}
}
等会再说滤波模板干啥用的,先看一下这种朴素的模糊方法的效果:
改良模糊效果
可以看到,模糊确实是有效果了,但是感觉还没有到毛玻璃的那种效果。实际上,这种模糊对盒子里的所有像素点一视同仁,这种模糊方法有些”粗暴“,为了得到更自然的平滑效果,我们可以适当加大盒子中心点的权重,降低盒子边界处像素点的权重。换言之,本来我们对盒子里的所有像素点取平均值,现在我们对每个像素点区别对待,给每个像素点乘上一个权重,靠近盒子中心的权重更大,远离中心的权重更小,最后再除以权重的总和来求出一个平均。
上面的公式看起来比较抽象,这里以一个3x3的模板举个例子:
这里给出的滤波模板的第一行是1 2 1,第二行是 2 4 2, 第三行是 1 2 1。其实这就是最常见的3x3的高斯模糊的模板。为什么叫高斯模糊呢,因为这个模板其实是二维高斯分布(也叫二维正态分布)在离散域的近似。二维高斯分布的公式长这个样子:
对应函数图像:
本来如果要按照这个公式实现高斯模糊的话,运算量就太大了,又有除法运算又有乘方运算的,但 我们可以用二项分布去近似逼近高斯分布(正态分布)。现在返回去观察那个3x3的高斯滤波模板,其中第一行的1 2 1就是一个二项式展开。而整个3x3的模板的由来,其实就是一个二项式展开式的各个部分分别乘以这个展开式本身,我们把这个过程叫做一维滤波模板的 卷积。
有了这个近似逼近的方法就方便编程实现了。刚才在上面定义了一个滤波器接口,接下来需要写一个高斯滤波器类来实现那个接口。对于weight()方法,可以通过对二项展开式的卷积得到,对于total()方法,可以通过二项式的定理得到是一个二次幂:
以下是高斯模糊的代码实现:
private class GaussFilterTask extends AsyncTask{
private Bitmap bitmap;
private Bitmap target;
public GaussFilterTask(){
super();
Drawable drawable = imageTarget.getDrawable();
bitmap = ((BitmapDrawable) drawable).getBitmap();
bitmap = Bitmap.createScaledBitmap(bitmap, bitmap.getWidth()/20, bitmap.getHeight()/20,false);
}
@Override
protected void onPreExecute() {
super.onPreExecute();
target = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(), bitmap.getConfig());
}
@Override
protected Void doInBackground(Void... params) {
gaussBlur(bitmap, target);//调用高斯模糊方法
return null;
}
@Override
protected void onProgressUpdate(Double... values) {
super.onProgressUpdate(values);
}
@Override
protected void onPostExecute(Void aVoid) {
super.onPostExecute(aVoid);
imageTarget.setImageDrawable(new BitmapDrawable(getResources(), target));
}
}
private void gaussBlur(Bitmap src, Bitmap target) {
if (src.getConfig() != Bitmap.Config.ARGB_8888) {//简化考虑,只支持ARGB8888格式的Bitmap,即 透明、红、绿、蓝四个通道各占一个字节
return;
}
int boxWidth = 7;//盒子大小为7x7
int boxHeight = 7;
GaussFilter filter = new GaussFilter(boxHeight, boxWidth);//实例化GaussFilter,并传递给filter()方法
for (int i = 0; i < src.getHeight(); i ++){
for (int j = 0; j < src.getWidth(); j ++){
int meanPixel = filter(boxWidth, boxHeight, i, j, src, filter);
target.setPixel(j, i, meanPixel);
}
}
}
/**
* Created by zjl on 2016/12/13.
* 使用二项式分布逼近的高斯滤波器
*/
class GaussFilter implements Filter {
private int width;
private int height;
public GaussFilter(int boxHeight, int boxWidth) {
this.height = boxHeight;
this.width = boxWidth;
}
@Override
public int weight(int rowIndex, int colIndex) {
int me = C(width - 1, colIndex);
return me * C(height - 1, rowIndex);
}
@Override
public int total() {
int result = (int) Math.pow(2, width + height - 2);
return result;
}
private int C(int n, int k){ //n次二项展开式,第k项
if (k <= 0)
return 1;
if (k > n / 2)
return C(n, n - k);
int result = 1;
for (int i = 1; i <= k; i++)
result *= (n - i + 1);
for (int i = 1; i <= k; i++)
result /= i;
return result;
}
}
以上代码中,把模糊操作放到AsyncTask中是因为这是个耗时任务,图像的size一大很容易ANR。
在盒子大小为3x3时,缩放10倍时的效果:
在盒子大小为3x3时,缩放20倍时的效果:
在盒子大小为7x7时,缩放20倍时的效果:
可以看到:
- 对图像进行一定程度的缩放可以很大程度改善高斯模糊的效果。同时降低运算量。
- 盒子大小则表示局部模糊的范围,盒子越小,对布局细节的保留相对越多,盒子越大,细节丢失地越严重。
- 高斯模糊比起最初的均值模糊,模糊效果过渡得更”平滑“,不那么突兀。我们平时用得毛玻璃的效果的底层实现,基本上是高斯模糊或优化后的高斯模糊算法。
说明
最后再说明一下,这篇文章主要是为了介绍了模糊算法的一些基本原理,以上代码没有经过优化,不仅运算速度慢,而且容易出现OOM,完全没法用在工程中,只是演示一下效果罢了。