一、初识Canvas.drawBitmapMesh()

1、方法介绍分析

先来看看 Android API 中对 drawBitmapMesh 方法的介绍:

drawBitmapMesh方法

这个方法的参数还不少, 下面稍微讲讲几个比较重要的参数的意思:

bitmap:将要扭曲的图像

meshWidth:控制在横向上把该图像划成多少格

meshHeight:控制在纵向上把该图像划成多少格

verts:网格交叉点坐标数组,长度为(meshWidth + 1) * (meshHeight + 1) * 2

vertOffset:控制verts数组中从第几个数组元素开始才对bitmap进行扭曲

Android 中的 drawBitmapMesh() 方法与操纵像素点来改变色彩的原理类似。只不过是把图像分成一个个的小块,然后通过改变每一个图像块来改变整个图像。来看看下面这张经典的图像对比:

drawBitmapMesh效果

如上图,我们将图像分割成若干个图像块,在图像上横纵方向各划分成 N-1 格,而这横纵分割线就交织成了N*N个点,而每个点的坐标将以x1,y1,x2,y2,···,xn,yn的形式保存在 verts 数组里。也就是说,verts 数组中每两个元素保存一个交织点的位置,第一个保存横坐标,第二个保存纵坐标。而 drawBitmapMesh() 方法改变图像的方式,就是通过改变这个 verts 数组里的元素的坐标值来重新定位对应的图像块的位置,从而达到图像效果处理的功能。从这里我们就可以看得出来,借用 Canvas.drawBitmapMesh() 方法可以实现各种图像形状的处理效果,只是实现起来比较复杂,关键在于计算、确定新的交叉点的坐标。

Canvas.drawBitmapMesh()

2、方法代码实现

首先,我们将要修整的图片加载进来,然后获取其交叉点的坐标值,并将坐标值保存到 orig[] 数组中。其获取交叉点坐标的原理是通过循环遍历所有的交叉线,并按比例获取其坐标,代码如下:

//将图像分成多少格

private int WIDTH = 200;

private int HEIGHT = 200;

//交点坐标的个数

private int COUNT = (WIDTH + 1) * (HEIGHT + 1);

//用于保存COUNT的坐标

//x0, y0, x1, y1......

private float[] verts = new float[COUNT * 2];

//用于保存原始的坐标

private float[] orig = new float[COUNT * 2];

private void initView() {

int index = 0;

Bitmap mBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.test00);

float bmWidth = mBitmap.getWidth();

float bmHeight = mBitmap.getHeight();

for (int i = 0; i < HEIGHT + 1; i++) {

float fy = bmHeight * i / HEIGHT;

for (int j = 0; j < WIDTH + 1; j++) {

float fx = bmWidth * j / WIDTH;

//X轴坐标 放在偶数位

verts[index * 2] = fx;

orig[index * 2] = verts[index * 2];

//Y轴坐标 放在奇数位

verts[index * 2 + 1] = fy;

orig[index * 2 + 1] = verts[index * 2 + 1];

index += 1;

}

}

}

然后就是将 verts[] 数组里面的坐标值进行一系列的自定义的修改。这里对 verts[] 数组的修改直接体现在图像的显示效果,各种图像特效的处理关键就在于此。比如这篇文章对 verts[] 数组的修改是实现图像局部约束变形效果。

接着,我们将在onDraw()方法里,将修改过的 verts[] 数组重新绘制一遍,代码如下:

@Override

protected void onDraw(Canvas canvas) {

super.onDraw(canvas);

canvas.drawBitmapMesh(mBitmap, WIDTH, HEIGHT, verts, 0, null, 0, null);

}

好,大致讲完 Canvas.drawBitmapMesh() 方法之后,我们接下来进入实践环节,也是本文的重点环节——实现人像瘦脸的功能。

二、实现瘦脸效果

1、算法提及

小弟这里用到的平滑过渡可交互的瘦脸算法是 Andreas Gustafsson 的 Interactive Image Warping 文献里提及的Uwarp's local mapping functions。截个图大家看看:

好了,接下来大家还是看看我的理解吧。

2、算法分析

看上图,这个坐标系对应着我们 Android 屏幕上的绘图坐标,点 C 就是我们手指触摸按下的坐标点,半径为 rmax 的圆形范围就是我们要平滑变形的区域,当我们在 C 位置按下屏幕并拖动到点 M 位置时,半径为 rmax 的变形区域内的每一个像素点将按照上述提及的算法公式进行位移,效果就是点 U 移动到点 X 的位置。所以,关键就是找到上面这个变换的逆变换——给出点 X 时,可以求出它变换前的坐标 U,然后用变化前图像在 U 点附近的像素进行插值,求出U的像素值。如此对圆形选区内的每一个像素进行求值,便可得出变换后的图像。在这里,就是求出点 U 的在 verts 数组对应的坐标值,并将此坐标值赋给 X 点在 verts 数组对应的元素,然后重新绘制,就可以得到我们想要的变形后的图像。

说白了就是需要我们实现以下特点:

只有圆形选区内的图像才进行变形(这里需要自己用代码控制一下)

拖动距离 MC 越大变形效果越明显(这里需要自己用代码控制一下,下面我会给大家讲讲)

越靠近圆心,变形越大,越靠近边缘的变形越小,边界处无变形(算法公式已经实现)

变形是平滑的(算法公式已经实现)

那有同学会注意到,文献中讲到的公式是向量的计算,这算法公式并不能直接用啊!且看我们中学的数学知识:

坐标系解向量加减法:

在直角坐标系里面,定义原点为向量的起点.两个向量和与差的坐标分别等于这两个向量相应坐标的和与差若向量的表示为(x,y)形式,

A(X1,Y1) ; B(X2,Y2),则:

A+B=(X1+X2,Y1+Y2),A-B=(X1-X2,Y1-Y2)

这样,我们可以从横纵坐标入手。话不多说,来实现吧。

3、算法的代码实现

首先通过 onTouchEvent() 方法获取到触摸按下时的点 C 的坐标,以及拖动结束时的点 M 的坐标:

@Override

public boolean onTouchEvent(MotionEvent event) {

switch (event.getAction()) {

case MotionEvent.ACTION_DOWN:

startX = event.getX();

startY = event.getY();

break;

case MotionEvent.ACTION_UP:

//调用warp方法根据触摸屏事件的坐标点来扭曲verts数组

warp(startX, startY, event.getX(), event.getY());

break;

}

return true;

}

定义一下我们局部变形的作用半径 rmax:

//作用范围半径

private int r = 100;

接着就是最关键的代码,这里是将圆形范围内的每一个交叉点的横纵坐标分别求出其逆变换的坐标,并将求得的值重新赋给这个交叉点,下面将算法转换成java代码:

private void warp(float startX, float startY, float endX, float endY) {

//计算拖动距离

float ddPull = (endX - startX) * (endX - startX) + (endY - startY) * (endY - startY);

float dPull = (float) Math.sqrt(ddPull);

//文献中提到的算法,并不能很好的实现拖动距离 MC 越大变形效果越明显的功能,下面这行代码则是我对该算法的优化

dPull = screenWidth - dPull >= 0.0001f ? screenWidth - dPull : 0.0001f;

for (int i = 0; i < COUNT * 2; i += 2) {

//计算每个坐标点与触摸点之间的距离

float dx = verts[i] - startX;

float dy = verts[i + 1] - startY;

float dd = dx * dx + dy * dy;

float d = (float) Math.sqrt(dd);

//文献中提到的算法同样不能实现只有圆形选区内的图像才进行变形的功能,这里需要做一个距离的判断

if (d < r) {

//变形系数,扭曲度

double e = (r * r - dd) * (r * r - dd) / ((r * r - dd + dPull * dPull) * (r * r - dd + dPull * dPull));

double pullX = e * (endX - startX);

double pullY = e * (endY - startY);

verts[i] = (float) (verts[i] + pullX);

verts[i + 1] = (float) (verts[i + 1] + pullY);

}

}

invalidate();

}

好了,代码写完了。

说了半天,无图无真相啊。还是看看我的 Demo 的实现效果吧,看看下面的对比图,胖哥的腮帮是不是瘦了,当然,本来P图就是个技术活,我这里只是随手推了推胖哥的脸,难免显得不专业,感兴趣的同学可以到文末下载我的 Demo 玩一玩:

Demo效果

写到这里,大家已经可以动手做一个修图APP出来了,结合我上一篇文章提到的滤镜效果,相信大家可以的。

4、补充

我的 Demo 里面加了作用范围圆形的显示和瘦脸拖动方向的显示,以及一键复原的按钮,方便同学们更加直观的理解和使用。

4.1.添加作用范围圆形的显示和瘦脸拖动方向的显示

在 onDraw() 方法里加上绘制圆形和直线的代码,如下:

//是否显示变形圆圈

private boolean showCircle;

//是否显示变形方向

private boolean showDirection;

@Override

protected void onDraw(Canvas canvas) {

super.onDraw(canvas);

canvas.drawBitmapMesh(mBitmap, WIDTH, HEIGHT, verts, 0, null, 0, null);

if (showCircle) {

canvas.drawCircle(startX, startY, r, circlePaint);

}

if (showDirection) {

canvas.drawLine(startX, startY, moveX, moveY, directionPaint);

}

}

接着重新写写 onTouchEvent() 方法里的代码,在 MotionEvent.ACTION_DOWN 中绘制变形区域,在 MotionEvent.ACTION_MOVE 中绘制变形方向直线,在 MotionEvent.ACTION_UP 中 去掉变形区域和变形方向直线,代码如下:

@Override

public boolean onTouchEvent(MotionEvent event) {

switch (event.getAction()) {

case MotionEvent.ACTION_DOWN:

//绘制变形区域

startX = event.getX();

startY = event.getY();

showCircle = true;

invalidate();

break;

case MotionEvent.ACTION_MOVE:

//绘制变形方向

moveX = event.getX();

moveY = event.getY();

showDirection = true;

invalidate();

break;

case MotionEvent.ACTION_UP:

showCircle = false;

showDirection = false;

//调用warp方法根据触摸屏事件的坐标点来扭曲verts数组

warp(startX, startY, event.getX(), event.getY());

break;

}

return true;

}

4.2.添加一键复原的按钮

还记得上面提到最初获取分割图片的交叉点的坐标,我们将原始坐标保存在了 orig[] 数组中。这里,当我们点击复原按钮,我们就将 orig[] 数组的值赋给 verts[] 数组,然后重新绘制即可,很简单,添加一个接口监听即可,然后在 MainActivity 中调用一下,代码如下:

/**

* 一键恢复

*/

public void resetView() {

for (int i = 0; i < verts.length; i++) {

verts[i] = orig[i];

}

onStepChangeListener.onStepChange(true);

invalidate();

}

public void setOnStepChangeListener(IOnStepChangeListener onStepChangeListener) {

this.onStepChangeListener = onStepChangeListener;

}

public interface IOnStepChangeListener {

void onStepChange(boolean isEmpty);

}

最后按照惯例,上一个Demo的动态图给大家看看吧,我这里就直接将拖动距离加大,好让大家直观地看到效果:

MyDrawBitmapMeshDemo

后续

Demo中还有很多可以完善的细节,这里只做原理分析,感兴趣的同学可以继续完善,比如,变形区域的动态设置,记录每一次变形的数组值用于撤销上一步操作,等等。同样的,这里不仅仅可以瘦脸,还可以瘦各种地方。如果需要做拉伸处理,只需要将 verts[] 数组里的元素做相应的处理即可。

Demo 下载地址

php实现图片瘦脸,Android:修图技术之瘦脸效果的实现(drawBitmapMesh)相关推荐

  1. 修图技术之瘦脸效果的实现

    一.初识Canvas.drawBitmapMesh() 1.方法介绍分析 先来看看 Android API 中对 drawBitmapMesh 方法的介绍: 这个方法的参数还不少, 下面稍微讲讲几个比 ...

  2. android 开源图片合成,Android 图像合成技术Xformodes图片剪裁

    先来看16种图片合成模式,如下所示 使用方式 private Bitmap onCompositeImages(){ Bitmap bmp = null; bmp = Bitmap.createBit ...

  3. Android开发技术周报176学习记录

    Android开发技术周报176学习记录 教程 当 OkHttp 遇上 Http 2.0 http://fucknmb.com/2018/04/16/%E5%BD%93OkHttp%E9%81%87% ...

  4. Android开发技术周报 Issue#17

    Android开发技术周报 Issue#17 声明:所有内容收集整理自网络.如有侵权,请联系删除.微信公众号上请点击"阅读原文"阅读完整版本. 业界新闻 1. Google 正秘密 ...

  5. Android开发技术框架和编码规范

    Android开发技术框架和编码规范   2017年11月23日       目录 第一章 绪论.................................................... ...

  6. Android开发技术前线 (android-tech-frontier) --优质技术文章的聚合项目

    Android开发技术前线 ( android-tech-frontier ) Android开发技术前线一个定期翻译.发布国内外Android优质的技术.开源库.软件架构设计.测试等文章的开源项目, ...

  7. Android官方技术文档翻译——新构建系统概述

    本文译自Android官方技术文档<New Build System>,原文地址:http://tools.android.com/tech-docs/new-build-system. ...

  8. Android开发技术周报 Issue#27

    教程 Android开发技术前线第五期 (@MrSimp1e) 深入Android图形管道.Romain Guy的性能优化案例.图片加载框架Glide.模仿iOS的模糊视图,都是些不错的文章. And ...

  9. android 主题xml,自定义Android主题风格theme.xml方法 Android开发技术

    自定义Android主题风格theme.xml方法 Android开发技术 2013 年 5 月 23 日 在Android中可以通过自定义主题风格方式来实现个性化以及复用,首先我们创建theme.x ...

最新文章

  1. 文凭-决定的人生成败?下
  2. 正确使用stl vecotr erase函数
  3. 关于base target=_self 等
  4. 利用FreeNas创建AFP共享
  5. sap新总账中 CodingBlock客户化自定义新字段方法
  6. 关于H3C路由配置VLAN的问题
  7. 【Makefile由浅入深完全学习记录7】Makefile中变量的高级主题下
  8. python 批量打印文档_使用python将Excel数据填充Word模板并生成Word
  9. 怎样才能在前端职场中拥有更强的竞争力?
  10. Julia 语言可重用性高竟源于缺陷和不完美?
  11. mybatis #与$区别
  12. jupyter 教程
  13. java代码jar包混淆,proguard对java代码进行混淆
  14. 彻底搞懂CSS层叠上下文、层叠等级、层叠顺序、z-index
  15. MarkDown编辑器设置图片大小
  16. CSP2019滚粗记
  17. python几何拼贴画_什么是拼贴艺术、集合艺术、拼贴画?
  18. 来自腾讯的高性能服务器架构思路
  19. 微信小程序图片裁剪插件image-cropper
  20. 科学计算法(机器学习)----决策树定义以相关概念

热门文章

  1. 前端:弹幕标签用法详细介绍(跑马灯)
  2. linux命令统计word字数,linux命令大全之wc命令详解(统计文件字节数)
  3. Redis常用命令,清空Redis缓存数据库
  4. 基于Springboot外卖系统08:员工账号状态管理功能+对象转换器+扩展Spring mvc的消息转换器
  5. 光流定位原理是什么??【转】
  6. 无人机上的光流定位通常适用于_优象光流模块助力无人机之使用效果分享
  7. MATLAB浮点数运算精度问题
  8. 流式处理 术语解释 Exactly-once与Effectively-once
  9. IDS简介与性能指标
  10. SpringMVC自定义响应的HTTP状态码