Android 图片缩小锯齿处理

本文研究场景为大图标显示为较小图标时,消除锯齿的处理方法,不适用于放大图片的场景。
建议缩小倍数大于两倍时再使用本文提供的工具类。

一、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像素交点只是其中一个像素,交点周围的像素颜色值都基本一致,全是蓝色或全是白色。

最终生成的图像均为蓝色的点或白色的点,相邻的像素点之间色差极大,不是蓝色就是白色,从而出现锯齿的现象。

4*4.png

如果先将图片缩小到8*8像素,相邻像素之间依然具有明显色差,依然会有比较明显的锯齿。

8*8.png

但是进一步将8*8缩小为4*4时,采用双线性采样方法,将会得到处于蓝色和白色之间的一个较为平均的颜色。从而改善了锯齿,但是丢失了细节,等于将图片模糊了。

因为处理的场景是小尺寸图片的使用场景,故图片存在一定程度的模糊是能够被接受的。

三、显示效果

输出如下Demo,加载当前机器中的所有应用图标,均已128*128px大小展示

在不做任何处理时,Android自动压缩,并展示出来,效果如"默认压缩.png"所示。
经过两次压缩后再展示出来,效果如"两次双线性压缩.png"所示。

默认压缩.png
两次双线性压缩.png

在4K电视上或下载后将两个图片放大后,可以看到左侧有些许锯齿,相比较右侧比较平滑一些。

通过两次双线性压缩,虽然看起来图片是平滑了,但其实是将原图进行了模糊的处理,丢失了图片的细节。所以该方法只建议在大图片压缩到非常小的小图时使用。

默认压缩.png
两次双线性压缩.png

四、工具类使用方法

工具类仅适用于缩小图片时的处理,不适用于放大图片。建议缩小倍数大于两倍时再使用该工具类。

传入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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,185评论 6 503
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,652评论 3 393
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,524评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,339评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,387评论 6 391
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,287评论 1 301
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,130评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,985评论 0 275
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,420评论 1 313
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,617评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,779评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,477评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,088评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,716评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,857评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,876评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,700评论 2 354

推荐阅读更多精彩内容