Android 绘制你最爱的马赛克

前言

我们之前写过《Android 实现LED 展示效果》,在这篇文章中,我们使用了图像分块(或者是分片)的算法,这样做的目的是降低像素扫描的时间复杂度,并且也利于均色采样。其实图像分块是很常见的图像动效处理手段,一般主要用于场景变换,这项技术也和图像光栅化类似。

什么是光栅化

光栅化渲染(Rasterized Rendering)直译过来是栅格化渲染。寻找图像中被几何图形占据的所有像素的过程称为栅格化,因此对象顺序渲染(Object-order rendering)也可以称为栅格化渲染。

我们今天的所要用到的技术也是栅格化和像素采样技术。

LED原理简述

马赛克是一种图像编辑技术,广泛应用于隐私保护和涂鸦渲染,很多手机系统自带了这种效果,那如何才能实现这种技术呢?

了解过我之前的文章的知道,我们制作LED有几个特征

  • 每个LED单元要么亮要么不亮
  • 每个LED单元只有一种颜色
  • 每个LED单元和其他LED单元存在一定的间距
  • 所有LED的单元成网格排列
  • 每个LED单元大小一致

以上相当于顶点坐标信息,我们拿到网格的位置,就能拿到LED整个区域的片段,知道这个区域的片段我们就可以修改其像素。

着色采样:

即便是每个矩形区域,也有很多像素点,如果每个矩形区域每个像素都要进行均色计算的话,那10x10的也要100此,因此为了更快的效率,需要对LED 范围内的像素点采样,求出颜色均值,均值色就是LED最终展示的颜色。

避坑------修改像素

上一篇我们知道,通过Bitmap.setPixel方法修改像素效率是极低的,我曾经写过一篇通过修改像素生成圆形图片的文章,在那篇文章里我们看到,像素本身也是有size的,导致最终的圆形图片存在大量锯齿,主要原因是通过这种方式没法做到双线性过滤(图片放大之后会对边缘优化),还有另一个问题,就是效率极差。 总结一下修改像素的问题:

  • 无法抗锯齿
  • 效率低

避坑------透明色

像素中往往存在 color为0或者alpha通道为0的情况,甚至有的区域因为采样原因导致清晰度急剧下降,甚至出现了透明区域噪点,这些问题主要来自于alpha 通道引发的颜色稀释问题,因此在采样时一定要规避这两种情况,至于会不会失真?答案是如果采用alpha失真只会更严重。

清晰度问题

同样,清晰度也容易受到这olor为0或者alpha通道为0的情况情况干扰,除了这两种就是采样区域的大小了,理论上采样网格密度越密,清晰度越高,越接近原始图片,因此一定要权衡,太清晰不就很原图一样了么,还制作什么LED呢?

马赛克原理

实际上,马赛克原理和LED展示方式类似,为什么这么说呢?从特征来看,几乎一样,马赛克和LED效果只在两部分存在区别

  1. 马赛克网格之间不存在间距
  2. 马赛克采样次数比LED要少

马赛克没有LED间距很好理解,至于次数少的好处第一肯定是效率高,其次是采样太多容易接近原色,而LED是要有一定程度接近原色。

技术实现

本篇我们邀请一位可爱的猫猫,老师们太耀眼的图片就算了,不利于大家阅读。

我们接下来的任务是把给猫脸打上马赛克,了解完这项技术实现后,其实你不仅可以给猫脸打马赛克,自行涂鸦,指哪儿打哪儿。

基本信息

java 复制代码
private Bitmap mBitmap; //猫图
private float blockWidth = 30; //30x30的像素快
private RectF blockRect = new RectF(); //猫头区域
private RectF gridRect = new RectF(); //网格区域

Canvas 包裹Bitmap

主要方便绘制和内存回收

scala 复制代码
static class BitmapCanvas extends Canvas {
    Bitmap bitmap;
    public BitmapCanvas(Bitmap bitmap) {
        super(bitmap);
        this.bitmap = bitmap;
    }
    public Bitmap getBitmap() {
        return bitmap;
    }
}

定位猫头位置

由于时间关系,我没有做TOUCH事件处理,就写了这个猫头区域

less 复制代码
/ 这个区域到时候可以自己调制,想让什么部位有马赛克那就会有马赛克
blockRect.set(bitmapCanvas.bitmap.getWidth() - 400, 0, bitmapCanvas.bitmap.getWidth() - 100, 300);

网格分割

ini 复制代码
//根据平分猫头矩形区域
int col = (int) (blockRect.width() / blockWidth);
int row = (int) (blockRect.height() / blockWidth);

网格定位

ini 复制代码
float startX = blockRect.left;
float startY = blockRect.top;
for (int i = 0; i < row * col; i++) {
    int x = i % col;
    int y = (i / col);
    gridRect.set(startX + x * blockWidth, startY + y * blockWidth, startX + x *     blockWidth + blockWidth, startY + y * blockWidth + blockWidth);
  
}

采样和着色

ini 复制代码
float startX = blockRect.left;
float startY = blockRect.top;
for (int i = 0; i < row * col; i++) {
    int x = i % col;
    int y = (i / col);
    gridRect.set(startX + x * blockWidth, startY + y * blockWidth, startX + x * blockWidth + blockWidth, startY + y * blockWidth + blockWidth);
   //采样
    int sampleColor = mBitmap.getPixel((int) gridRect.centerX(), (int) gridRect.centerY());
    mCommonPaint.setColor(sampleColor);
    //着色,我们这里不修改像素,而是drawRect,避免性能问题和锯齿问题
    bitmapCanvas.drawRect(gridRect, mCommonPaint);
}

渲染到View上

csharp 复制代码
canvas.drawBitmap(bitmapCanvas.bitmap, null, mainRect, mCommonPaint);

效果预览

避坑点

网格区间不易过小,和LED一样,越小清晰度越高,就会失去了处理的意义。

全部代码

ini 复制代码
public class MosaicView extends View {
    private final DisplayMetrics mDM;
    private TextPaint mCommonPaint;
    private RectF mainRect = new RectF();

    private BitmapCanvas bitmapCanvas; //Canvas 封装的
    private Bitmap mBitmap; //猫图
    private float blockWidth = 30; //30x30的像素快
    private RectF blockRect = new RectF(); //猫头区域
    private RectF gridRect = new RectF(); //网格区域
    private boolean showMask = false;

    public MosaicView(Context context) {
        this(context, null);
    }
    public MosaicView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }
    public MosaicView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mDM = getResources().getDisplayMetrics();
        initPaint();
    }

    private void initPaint() {
        //否则提供给外部纹理绘制
        mCommonPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
        mCommonPaint.setAntiAlias(true);
        mCommonPaint.setStyle(Paint.Style.FILL_AND_STROKE);
        mCommonPaint.setStrokeCap(Paint.Cap.ROUND);
        mBitmap = decodeBitmap(R.mipmap.ic_cat);

    }
    private Bitmap decodeBitmap(int resId) {
        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inMutable = true;
        return BitmapFactory.decodeResource(getResources(), resId, options);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);

        if (widthMode != MeasureSpec.EXACTLY) {
            widthSize = mDM.widthPixels / 2;
        }
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        if (heightMode != MeasureSpec.EXACTLY) {
            heightSize = widthSize / 2;
        }
        setMeasuredDimension(widthSize, heightSize);
        mBitmap = decodeBitmap(R.mipmap.ic_cat);

    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        if (bitmapCanvas != null && bitmapCanvas.bitmap != null && !bitmapCanvas.bitmap.isRecycled()) {
            bitmapCanvas.bitmap.recycle();
        }
        bitmapCanvas = null;

    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        int width = getWidth();
        int height = getHeight();
        if (width < 1 || height < 1) {
            return;
        }
        if (bitmapCanvas == null || bitmapCanvas.bitmap == null) {
            bitmapCanvas = new BitmapCanvas(Bitmap.createBitmap(mBitmap.getWidth(), mBitmap.getHeight(), Bitmap.Config.ARGB_8888));
        } else {
            bitmapCanvas.bitmap.eraseColor(Color.TRANSPARENT);
        }
        float radius = Math.min(width / 2f, height / 2f);

      //关闭双线性过滤
      //  int flags = mCommonPaint.getFlags();
      //  mCommonPaint.setFlags(flags &~ Paint.FILTER_BITMAP_FLAG);
      //  mCommonPaint.setFilterBitmap(false);

        int save = bitmapCanvas.save();
        bitmapCanvas.drawBitmap(mBitmap, 0, 0, mCommonPaint);


        // 这个区域到时候可以自己调制,想让什么部位有马赛克那就会有马赛克
        blockRect.set(bitmapCanvas.bitmap.getWidth() - 560, 10, bitmapCanvas.bitmap.getWidth() - 100, 410);

        if(showMask) {
            //根据平分猫头矩形区域
            int col = (int) (blockRect.width() / blockWidth);
            int row = (int) (blockRect.height() / blockWidth);

            float startX = blockRect.left;
            float startY = blockRect.top;

            for (int i = 0; i < row * col; i++) {
                int x = i % col;
                int y = (i / col);
                gridRect.set(startX + x * blockWidth, startY + y * blockWidth, startX + x * blockWidth + blockWidth, startY + y * blockWidth + blockWidth);
                //采样
                int sampleColor = mBitmap.getPixel((int) gridRect.centerX(), (int) gridRect.centerY());
                mCommonPaint.setColor(sampleColor);
                //着色,我们这里不修改像素,而是drawRect,避免性能问题和锯齿问题
                bitmapCanvas.drawRect(gridRect, mCommonPaint);
            }
        }else{
            Paint.Style style = mCommonPaint.getStyle();
            mCommonPaint.setStyle(Paint.Style.STROKE);
            mCommonPaint.setColor(Color.MAGENTA);
            mCommonPaint.setStrokeWidth(8);
            bitmapCanvas.drawRect(blockRect, mCommonPaint);
            mCommonPaint.setStyle(style);

        }

        bitmapCanvas.restoreToCount(save);
        int saveCount = canvas.save();
        canvas.translate(width / 2f, height / 2f);
        mainRect.set(-radius, -radius, radius, radius);
        canvas.drawBitmap(bitmapCanvas.bitmap, null, mainRect, mCommonPaint);
        canvas.restoreToCount(saveCount);

    }

    public void openMask() {
        showMask = true;
        postInvalidate();
    }

    public void closeMask() {
        showMask = false;
        postInvalidate();

    }

    static class BitmapCanvas extends Canvas {
        Bitmap bitmap;
        public BitmapCanvas(Bitmap bitmap) {
            super(bitmap);
            //继承在Canvas的绘制是软绘制,因此理论上可以绘制出阴影
            this.bitmap = bitmap;
        }
        public Bitmap getBitmap() {
            return bitmap;
        }
    }
}

总结

实际上还有另一种方法,我们绘制图片时关闭双线性过滤

ini 复制代码
//关闭双线性过滤
//  int flags = mCommonPaint.getFlags();
//  mCommonPaint.setFlags(flags &~ Paint.FILTER_BITMAP_FLAG);
//  mCommonPaint.setFilterBitmap(false);

然后将图片放到很大,这个时候你的图片就会产生一定的网格区域,截图然后进行一系列矩阵转换,最后把图贴到原处就出现了马赛克,但是这个有个问题,超高像素的图片得先缩小,然后再放大,显然处理步骤比较多。

下图是先缩小20倍然后画到原来大小的效果

实现代码

本来不打算放代码的,想想还是放上吧

ini 复制代码
public class BitmapMosaicView extends View {
    private final DisplayMetrics mDM;
    private TextPaint mCommonPaint;
    private RectF mainRect = new RectF();
    private BitmapCanvas bitmapCanvas; //Canvas 封装的
    private BitmapCanvas srcThumbCanvas; //Canvas 封装的
    private Bitmap mBitmap; //猫图
    private RectF blockRect = new RectF(); //猫头区域

    public BitmapMosaicView(Context context) {
        this(context, null);
    }
    public BitmapMosaicView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }
    public BitmapMosaicView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mDM = getResources().getDisplayMetrics();
        initPaint();
    }

    private void initPaint() {
        //否则提供给外部纹理绘制
        mCommonPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
        mCommonPaint.setAntiAlias(true);
        mCommonPaint.setStyle(Paint.Style.FILL_AND_STROKE);
        mCommonPaint.setStrokeCap(Paint.Cap.ROUND);
        mBitmap = decodeBitmap(R.mipmap.ic_cat);

    }
    private Bitmap decodeBitmap(int resId) {
        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inMutable = true;
        return BitmapFactory.decodeResource(getResources(), resId, options);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);

        if (widthMode != MeasureSpec.EXACTLY) {
            widthSize = mDM.widthPixels / 2;
        }
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        if (heightMode != MeasureSpec.EXACTLY) {
            heightSize = widthSize / 2;
        }
        setMeasuredDimension(widthSize, heightSize);

    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        if (bitmapCanvas != null && bitmapCanvas.bitmap != null && !bitmapCanvas.bitmap.isRecycled()) {
            bitmapCanvas.bitmap.recycle();
        }
        if (srcThumbCanvas != null && srcThumbCanvas.bitmap != null && !srcThumbCanvas.bitmap.isRecycled()) {
            srcThumbCanvas.bitmap.recycle();
        }
        bitmapCanvas = null;

    }

    private Rect srcRectF = new Rect();
    private Rect dstRectF = new Rect();

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        int width = getWidth();
        int height = getHeight();
        if (width < 1 || height < 1) {
            return;
        }
        if (bitmapCanvas == null || bitmapCanvas.bitmap == null) {
            bitmapCanvas = new BitmapCanvas(Bitmap.createBitmap(mBitmap.getWidth(), mBitmap.getHeight(), Bitmap.Config.ARGB_8888));
        } else {
            bitmapCanvas.bitmap.eraseColor(Color.TRANSPARENT);
        }
        if (srcThumbCanvas == null || srcThumbCanvas.bitmap == null) {
            srcThumbCanvas = new BitmapCanvas(Bitmap.createBitmap(mBitmap.getWidth()/35, mBitmap.getHeight()/35, Bitmap.Config.ARGB_8888));
        } else {
            srcThumbCanvas.bitmap.eraseColor(Color.TRANSPARENT);
        }
        float radius = Math.min(width / 2f, height / 2f);

      //关闭双线性过滤
        int flags = mCommonPaint.getFlags();
        mCommonPaint.setFlags(flags &~ Paint.FILTER_BITMAP_FLAG);
        mCommonPaint.setFilterBitmap(false);
        mCommonPaint.setDither(false);


        srcRectF.set(0,0,mBitmap.getWidth(),mBitmap.getHeight());
        dstRectF.set(0,0, srcThumbCanvas.bitmap.getWidth(), srcThumbCanvas.bitmap.getHeight());

        int save = bitmapCanvas.save();
        srcThumbCanvas.drawBitmap(mBitmap, srcRectF, dstRectF, mCommonPaint);

        srcRectF.set(dstRectF);
        dstRectF.set(0,0,bitmapCanvas.bitmap.getWidth(),bitmapCanvas.bitmap.getHeight());
        bitmapCanvas.drawBitmap(srcThumbCanvas.bitmap, srcRectF,dstRectF, mCommonPaint);
        // 这个区域到时候可以自己调制,想让什么部位有马赛克那就会有马赛克
        blockRect.set(bitmapCanvas.bitmap.getWidth() - 560, 10, bitmapCanvas.bitmap.getWidth() - 100, 410);
        bitmapCanvas.restoreToCount(save);
        int saveCount = canvas.save();
        canvas.translate(width / 2f, height / 2f);
        mainRect.set(-radius, -radius, radius, radius);
        canvas.drawBitmap(bitmapCanvas.bitmap, null, mainRect, mCommonPaint);
        canvas.restoreToCount(saveCount);

    }
    static class BitmapCanvas extends Canvas {
        Bitmap bitmap;
        public BitmapCanvas(Bitmap bitmap) {
            super(bitmap);
            //继承在Canvas的绘制是软绘制,因此理论上可以绘制出阴影
            this.bitmap = bitmap;
        }
        public Bitmap getBitmap() {
            return bitmap;
        }
    }
}

总结下本文分享技术特点:

  • 网格化
  • 采样
  • canvas着色,不要去修改像素
相关推荐
zaim134 分钟前
计算机的错误计算(一百一十四)
java·c++·python·rust·go·c·多项式
500了2 小时前
Kotlin基本知识
android·开发语言·kotlin
hong_zc2 小时前
算法【Java】—— 二叉树的深搜
java·算法
进击的女IT3 小时前
SpringBoot上传图片实现本地存储以及实现直接上传阿里云OSS
java·spring boot·后端
人工智能的苟富贵3 小时前
Android Debug Bridge(ADB)完全指南
android·adb
Мартин.3 小时前
[Meachines] [Easy] Sea WonderCMS-XSS-RCE+System Monitor 命令注入
前端·xss
Miqiuha3 小时前
lock_guard和unique_lock学习总结
java·数据库·学习
一 乐4 小时前
学籍管理平台|在线学籍管理平台系统|基于Springboot+VUE的在线学籍管理平台系统设计与实现(源码+数据库+文档)
java·数据库·vue.js·spring boot·后端·学习
昨天;明天。今天。4 小时前
案例-表白墙简单实现
前端·javascript·css
数云界4 小时前
如何在 DAX 中计算多个周期的移动平均线
java·服务器·前端