注册

Android 绘制你最爱的马赛克

前言


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


什么是光栅化


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


Qu-es-la-rasterizacin-y-cual-es-su-diferencia-con-el-Ray-Tracing.jpg


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


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是要有一定程度接近原色。


技术实现


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


ic_cat.png


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


基本信息


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

Canvas 包裹Bitmap


主要方便绘制和内存回收


static class BitmapCanvas extends Canvas {
Bitmap bitmap;
public BitmapCanvas(Bitmap bitmap) {
super(bitmap);
this.bitmap = bitmap;
}
public Bitmap getBitmap() {
return bitmap;
}
}


定位猫头位置


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


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

网格分割


//根据平分猫头矩形区域
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);

}

采样和着色


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上


canvas.drawBitmap(bitmapCanvas.bitmap, null, mainRect, mCommonPaint);

效果预览


fire_56.gif


避坑点


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


全部代码


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;
}
}
}

总结


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


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

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


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

企业微信20231205-221500@2x.png


实现代码


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


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着色,不要去修改像素

作者:时光少年
来源:juejin.cn/post/7308925069916225588

0 个评论

要回复文章请先登录注册