本文研究场景为大图标显示为较小图标时,消除锯齿的处理方法,不适用于放大图片的场景。
建议缩小倍数大于两倍时再使用本文提供的工具类。
一、Android图片缩放
1.1 邻近采样(Nearest Neighbour Resampling)
邻近采样是Android中默认的图片压缩方法,原理是缩放之后,选取左右两边最近的一个像素点的颜色值,另一个像素直接抛弃。
举一个最简单的例子:
3*3 的256级灰度图。假如图像的象素矩阵如下图所示(这个原始图把它叫做源图,Source):
| 234 | 38 | 22 |
| 67 | 44 | 12 |
| 89 | 65 | 63 |
建立一个坐标系,以(0,0)表示234,(1,0)表示38
如果想要放大为44的图,需要依次确定每一个坐标上的像素值。先建立一个44的矩阵,里面每个像素值都是未知数。(这个将要被填充的图的叫做目标图,Destination)
| ? | ? | ? | ? |
| ? | ? | ? | ? |
| ? | ? | ? | ? |
| ? | ? | ? | ? |
然后要往目标图中填值,根据公式 srcX=dstX* (srcWidth/dstWidth) , srcY = dstY * (srcHeight/dstHeight)
我们要计算目标图(0,0)的像素值,即dstX =0,dstY=0,对应的源图的坐标即为:(0(3/4),0(3/4))=>(00.75,00.75)=>(0,0),所以目标图中(0,0)位置的像素值,要选取源图的(0,0)的像素值,即234
再计算目标图(1,0)的像素值,即dstX =1,dstY=0,对应的源图的坐标即为:(1(3/4),0(3/4))=>(10.75,00.75)=>(0.75,0),所以目标图中(0,1)位置的像素值,要选取源图的(0.75,0)的像素值,但源图没有0.75的坐标,采取四舍五入,使用(1,0),即38
……
最终得到目标图
| 234 | 38 | 22 | 22 |
| 67 | 44 | 12 | 12 |
| 89 | 65 | 63 | 63 |
| 89 | 65 | 63 | 63 |
通过邻近采样,放大后的图像有很严重的马赛克,缩小后的图像有很严重的失真
邻近采样在Android中的使用:
BitmapFactory.Options options = new BitmapFactory.Options();
//或者 inDensity 搭配 inTargetDensity 使用,算法和 inSampleSize 一样
options.inSampleSize = 2;
Bitmap bitmap = BitmapFactory.decodeFile("/sdcard/test.png");
Bitmap compress = BitmapFactory.decodeFile("/sdcard/test.png", options);
1.2 双线性采样(Bilinear Resampling)
双线性采样(Bilinear Resampling)是一种比较好的图像缩放算法,它充分的利用了源图中虚拟点四周的四个真实存在的像素值来共同决定目标图中的一个像素值,因此缩放效果比简单的最邻近插值要好很多。
比如,像刚才的例子,现在假如目标图的象素坐标为(1,1),那么反推得到的对应于源图的坐标是(0.75 , 0.75), 这其实只是一个概念上的虚拟象素。
实际在源图中并不存在这样一个象素,那么目标图的象素(1,1)的取值不能够由这个虚拟象素来决定,而只能由源图的这四个象素共同决定:(0,0)(0,1)(1,0)(1,1)
而由于(0.75,0.75)离(1,1)要更近一些,那么(1,1)所起的决定作用更大一些,而(0.75,0.75)离(0,0)最远,所以(0,0)所起的决定作用就要小一些
双线性采样在Android中的使用:
Bitmap bitmap = BitmapFactory.decodeFile("/sdcard/test.png");
Bitmap compress = Bitmap.createScaledBitmap(bitmap, bitmap.getWidth()/2,
bitmap.getHeight()/2, true);
或者直接使用 matrix 进行缩放
Bitmap bitmap = BitmapFactory.decodeFile("/sdcard/test.png");
Matrix matrix = new Matrix();
matrix.setScale(0.5f, 0.5f);
bm = Bitmap.createBitmap(bitmap, 0, 0, bit.getWidth(), bit.getHeight(), matrix, true);
1.3 双立方/双三次采样(Bicubic Resampling)
双立方/双三次采样使用的是双立方/双三次插值算法。邻近点插值算法的目标像素值由源图上单个像素决定,双线性內插值算法由源像素某点周围 2x2 个像素点按一定权重获得,而双立方/双三次插值算法更进一步参考了源像素某点周围 4x4 个像素。
这个算法在 Android 中并没有原生支持。如果需要使用,可以通过手动编写算法或者引用第三方算法库。
该算法在 ffmpeg 中已经给到了支持,具体的实现在 libswscale/swscale.c 文件中:FFmpeg Scaler Documentation
因引入ffmpeg,可能有so库的兼容问题,暂不使用该方法。
对图片缩放要求较高的应用可以考虑采用该方法。
1.4 Lanczos Resampling
Lanczos 采样和 Lanczos 过滤是 Lanczos 算法的两种常见应用,它可以用作低通滤波器或者用于平滑地在采样之间插入数字信号,Lanczos 采样一般用来增加数字信号的采样率,或者间隔采样来降低采样率。
Lanczos 采样使用的 Lanczos 算法也可以用来作为图片的缩放,Lanczos 算法和双三次插值算法都是使用卷积核来通过输入像素计算输出像素,只不过在算法表现上稍有不同。
Lanczos 从算法角度讲理论上会比双三次/双立方插值算法更好一点
Lanczos 算法在 ffmpeg 的 libswscale/swscale.c 中也有实现。
因引入ffmpeg,可能有so库的兼容问题,暂不使用该方法。
对图片缩放要求较高的应用可以考虑采用该方法。
二、Android图片缩放锯齿
2.1 Android提供的抗锯齿方法
//缩放抗锯齿
Bitmap.createScaledBitmap(bitmap, width, height, true);
Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);
//画布抗锯齿
PaintFlagsDrawFilter pfd = new PaintFlagsDrawFilter(0, Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG); //画图片时设置画布抗锯齿
canvas.setDrawFilter(pfd);
//画笔抗锯齿
Paint p = new Paint(Paint.FILTER_BITMAP_FLAG);
p.setAntiAlias(true);
//缩略图工具类
ThumbnailUtils.extractThumbnail(bitmap, width, height);
以上方法,在将图片缩小到较小的尺寸时,均会有明显的锯齿
2.2 缩小出现锯齿的原因及解决方案
图片缩小时,为下采样,以如下图标为例。假设将其缩小到4*4像素的尺寸。
目标图的像素点会采用源图中网格相交处邻近的像素值。
邻近采样会采用交点的像素左或右的像素值,双线性采样会根据交点的上下左右计算出一个平均值,但源图是320*320像素交点只是其中一个像素,交点周围的像素颜色值都基本一致,全是蓝色或全是白色。
最终生成的图像均为蓝色的点或白色的点,相邻的像素点之间色差极大,不是蓝色就是白色,从而出现锯齿的现象。
如果先将图片缩小到8*8像素,相邻像素之间依然具有明显色差,依然会有比较明显的锯齿。
但是进一步将8*8缩小为4*4时,采用双线性采样方法,将会得到处于蓝色和白色之间的一个较为平均的颜色。从而改善了锯齿,但是丢失了细节,等于将图片模糊了。
因为处理的场景是小尺寸图片的使用场景,故图片存在一定程度的模糊是能够被接受的。
三、显示效果
输出如下Demo,加载当前机器中的所有应用图标,均已128*128px大小展示
在不做任何处理时,Android自动压缩,并展示出来,效果如"默认压缩.png"所示。
经过两次压缩后再展示出来,效果如"两次双线性压缩.png"所示。
在4K电视上或下载后将两个图片放大后,可以看到左侧有些许锯齿,相比较右侧比较平滑一些。
通过两次双线性压缩,虽然看起来图片是平滑了,但其实是将原图进行了模糊的处理,丢失了图片的细节。所以该方法只建议在大图片压缩到非常小的小图时使用。
四、工具类使用方法
工具类仅适用于缩小图片时的处理,不适用于放大图片。建议缩小倍数大于两倍时再使用该工具类。
传入Drawable,及预期显示大小返回Drawable对象
public static Drawable zoomDrawable(Drawable drawable, int targetWidthPx, int targetHeightPx)
传入资源ID,及预期显示大小返回Drawable对象</pre>
public static Drawable zoomImageResources(Context context, int resId, int targetWidthPx, int targetHeightPx)
传入Bitmap ,及预期显示大小返回Bitmap对象</pre>
public static Bitmap zoomBitmap(Bitmap bitmap,int targetWidthPx, int targetHeightPx)
源码如下:
/**
* @author zsy on 2021/1/21.
*/
public class ZoomImageUtils {
public static Drawable zoomDrawable(Drawable drawable, int targetWidthPx, int targetHeightPx){
Bitmap oldBmp;
if (drawable instanceof BitmapDrawable){
oldBmp = ((BitmapDrawable) drawable).getBitmap();
}else {
oldBmp = drawableToBitmap(drawable);
}
return new BitmapDrawable(zoomBitmap(oldBmp,targetWidthPx,targetHeightPx));
}
public static Drawable zoomImageResources(Context context, int resId, int targetWidthPx, int targetHeightPx){
Resources res = context.getResources();
Bitmap oldBmp = BitmapFactory.decodeResource(res, resId);
return new BitmapDrawable(zoomBitmap(oldBmp,targetWidthPx,targetHeightPx));
}
public static Bitmap zoomBitmap(Bitmap bitmap,int targetWidthPx, int targetHeightPx){
if (targetWidthPx <=0 || targetHeightPx <= 0){
return null;
}
Bitmap newBmp;
final int two = 2;
int oldWidth = bitmap.getWidth();
int oldHeight = bitmap.getWidth();
if (oldWidth / two > targetWidthPx && oldHeight / two > targetHeightPx) {
//大图标压缩为小图标时,直接压缩为小图标锯齿明显,先压缩至2倍预期大小,再压缩至预期大小,可减少锯齿
Bitmap tempBmp = Bitmap.createScaledBitmap(bitmap, targetWidthPx * 2, targetHeightPx * 2, true);
newBmp = Bitmap.createScaledBitmap(tempBmp, targetWidthPx, targetHeightPx, true);
} else {
newBmp = Bitmap.createScaledBitmap(bitmap, targetWidthPx, targetHeightPx, true);
}
return newBmp;
}
/**
* 将Drawable转换为Bitmap
* @param drawable
* @return
*/
private static Bitmap drawableToBitmap(Drawable drawable) {
//取drawable的宽高
int width = drawable.getIntrinsicWidth();
int height = drawable.getIntrinsicHeight();
//取drawable的颜色格式
Bitmap.Config config = drawable.getOpacity() != PixelFormat.OPAQUE
? Bitmap.Config.ARGB_8888
: Bitmap.Config.RGB_565;
//创建对应的bitmap
Bitmap bitmap = Bitmap.createBitmap(width, height, config);
//创建对应的bitmap的画布
Canvas canvas = new Canvas(bitmap);
drawable.setBounds(0, 0, width, height);
//把drawable内容画到画布中
drawable.draw(canvas);
return bitmap;
}
}
参考文章:
https://cloud.tencent.com/developer/article/1006352
https://blog.csdn.net/andrew659/article/details/4818988