前言
在这篇文章中,我们使用了图像分块(或者是分片)的算法,这样做的目的是降低像素扫描的时间复杂度,并且也利于均色采样和提高绘制效率。其实图像分块是很常见的图像动效处理手段,一般主要用于场景变换,这项技术也和图像光栅化类似。
我们之前写过《Android 实现LED 展示效果》文章中,也适用了类似的方法。
比如,下面的图中,我们能清楚的看到,网格的颜色不存在渐变,每个网格的展示的颜色是固定的,这就是光栅化后着色的场景,但是区别与open gl更加细腻的三角形,我们这里按正方形处理。
(黑色三角形图案的放大效果)
这张图给我的们的启示是,我们对图片区域进行网格分割,然后在网格中绘制一样的颜色,就能达到马赛克效果。
不过,本篇会介绍两种方法,一种不放大的方法,一种先缩小再放大的方法,都能实现马赛克效果。
分片原理简述
马赛克是一种图像编辑技术,广泛应用于隐私保护和涂鸦渲染,很多手机系统自带了这种效果,那如何才能实现这种技术呢?
实际上,马赛克的绘制可以图片分配实现,我们之前就利用分片方式实现过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效果只在两部分存在区别
- 马赛克网格之间不存在间距
- 马赛克采样次数比LED要少
马赛克没有LED间距很好理解,至于次数少的好处第一肯定是效率高,其次是采样太多容易接近原色,而LED是要有一定程度接近原色。
技术实现
本篇我们邀请一位可爱的猫猫,老师们太耀眼的图片就算了,不利于大家阅读。
我们接下来的任务是把给猫脸打上马赛克,了解完这项技术实现后,其实你不仅可以给猫脸打马赛克,自行涂鸦,指哪儿打哪儿。
基本信息
主要要加载的图片,像素块大小
private Bitmap mBitmap; //猫图
private float blockWidth = 30; //30x30的像素块
private RectF blockRect = new RectF(); //猫头区域
private RectF gridRect = new RectF(); //网格区域
Canvas 包裹Bitmap
这样好处就是不需要单独管理Bitmap了
static class BitmapCanvas extends Canvas {
Bitmap bitmap;
public BitmapCanvas(Bitmap bitmap) {
super(bitmap);
this.bitmap = bitmap;
}
public Bitmap getBitmap() {
return bitmap;
}
}
另外也方便绘制,不然每次都需要通过进行绘制时创建Canva
Canvas c = new Canvas(mBitmap);
c.drawXXX(...)
定位猫头位置
由于时间关系,我没有做TOUCH事件处理,就写了这个猫头区域
/ 这个区域到时候可以自己调制,想让什么部位有马赛克那就会有马赛克
blockRect.set(bitmapCanvas.bitmap.getWidth() - 400, 0, bitmapCanvas.bitmap.getWidth() - 100, 300);
网格分割
主要计算出有多少行和多少列,blockRect前面提到过,负责记录要打码的位置
//根据平分猫头矩形区域
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);
......
}
采样和着色
所谓采样就是取网格内部的颜色,这里其实我们没必要纠结那个颜色更准确,直接取每个网格中心位置色值就行,取完颜色之后,利用drawRect将颜色填充到网格中。
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上
实际上,我们这个过程中是先绘制到Bitmap上,再绘制到网格上的,这样有利有弊,如果仅仅为了展示,直接绘制到canvas上也行,如果是为了编辑图片,那绘制到Bitmap上。
另外,Bitmap是自带Buffer的可视化缓冲,好处是可以复用,不过这里我们没有用到。
canvas.drawBitmap(bitmapCanvas.bitmap, null, mainRect, mCommonPaint);
效果预览
注:这张图上有红框残影,应该是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;
}
}
}
全图马赛克
实际上还有另一种方法,我们绘制图片时关闭双线性过滤,同时,接着,将图片缩小20倍左右之后,在放大到原来的大小,这个时候,整张图片都会被打上马赛克。
关闭线性过滤
关闭线性过滤的目的是避免绘制时对图片锯齿或者色块进行优化。
//关闭双线性过滤
int flags = mCommonPaint.getFlags();
mCommonPaint.setFlags(flags &~ Paint.FILTER_BITMAP_FLAG);
mCommonPaint.setFilterBitmap(false);
先缩小再放大
下图是先缩小20倍然后画到原来大小的效果
实现代码
本来不打算放代码的,想想还是放上吧
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;
}
}
}
全图马赛克优化
以上全图马赛克的实现,实际上有些复杂,我们还可以优化
其实我们可以利用Matrix + createBitmap实现类似的效果
Matrix matrix = new Matrix();
float scale = 21.0f / bmp.getWidth();
matrix.postScale(scale, scale);
mMosaicBmp = Bitmap.createBitmap(bm, 0, 0, bmp.getWidth(), bmp.getHeight(),
matrix, false);
总结
本篇的方法是非常适合生产环境的,在这个过程中,无需关注图片大小,只对要打码的区域进行分片即可,也方便通过触摸实践进行联动打码。
同时,如果是全图马赛克,也可以使用第二种方案。
总结下本文分享技术特点:
- 网格化
- 采样
- canvas着色,不要去修改像素