本文就没有在撸一个这样的库,直接使用了github开源的磨皮库.使用 HighPassSkinSmoothing
但是我这里为了形成对比,所以只取了左边的脸
Bitmap leftAndRightBitmap = Bitmap.createBitmap(bitmap.getWidth(),bitmap.getHeight(), Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(leftAndRightBitmap);
//+3,为了弥补 int值相除精读损失,让左边多一些
Rect left = new Rect(0,0,bitmap.getWidth()/2 + 3,bitmap.getHeight());
Rect right = new Rect(bitmap.getWidth() - bitmap.getWidth()/2 ,0,bitmap.getWidth(),bitmap.getHeight());
canvas.drawBitmap(result,left,left,null);
canvas.drawBitmap(bitmap,right,right,null);
人脸关键点检测
往人脸上化妆,拿整张照片的磨皮肯定不行啊,我们需要精准的人脸,那就需要人脸识别技术,开源的库也有一些,但是精度有待加强,所以本文选用了商用的人脸关键点检测技术,大概看了一下,有这么几家人脸识别技术做的还可以
- 商汤
- Face++
- 百度
- 虹软
他们的技术,人脸精度,使用价格,在此不做评论. 本文选用了Face++的稠密关键点检测. 为了方便去见,没有下载其SDK,使用了网页版本的关键点检测,可以上传本地照片,然后把数据拿下来.
右侧有关键点的json,可以直接复制下来,供后续使用.
{
“time_used”: 140,
“request_id”: “1565152700,b5efc234-055c-4109-8899-e7bd0b9d1d63”,
“face”: {
“landmark”: {
“left_eye”: {
“left_eye_43”: {
“y”: 170,
“x”: 140
},
“left_eye_42”: {
“y”: 170,
“x”: 141
},
“left_eye_41”: {
“y”: 170,
“x”: 142
},
“left_eye_40”: {
“y”: 170,
“x”: 143
},
“left_eye_47”: {
“y”: 170,
“x”: 136
},
“left_eye_46”: {
“y”: 170,
“x”: 137
}
}
}
}
}
如果商用建议购买其SDK。 有了这些点,我们就可以接下来“画”妆了。
粉底
有了磨皮,但是不够白啊,上述的库里其实包含了美白,它是对整个图片进行处理,叠加白色滤波,但效果很差,肯定不是我们想要的。但是有了人脸检测的点,那我们就好办了,涂一层粉底吧.(女生还要先涂水啊,乳啊什么,照片上不了水了…)
看Face++的文档我们可以知道json里面的关键点为face_contour_left_和face_hairline_为脸的区域.
直接拿出左边脸的区域.
public static Path landmark(String faceJson){
JSONObject jsonObject = null;
try {
jsonObject = new JSONObject(faceJson);
JSONObject eye = jsonObject.getJSONObject(“face”).getJSONObject(“landmark”).getJSONObject(“face”);
Path path = new Path();
Point start = getPointByJson(eye.getJSONObject(“face_contour_left_0”));
path.moveTo(start.x,start.y);
for(int i= 1;i< 64;i++){
Point point = getPointByJson(eye.getJSONObject(“face_contour_left_”+i));
path.lineTo(point.x,point.y);
}
for(int i= 144;i>= 72;i–){
Point point = getPointByJson(eye.getJSONObject(“face_hairline_”+i));
path.lineTo(point.x,point.y);
}
path.close();
return path;
} catch (JSONException e) {
e.printStackTrace();
}
return null;
}
有了左边区域,只需要一个画笔就可以画上去(原图就可以是画板 new Canvas(originBitmap)),那我们正常直接涂一层白色,肯定不行,会吓坏小伙伴的,那白色加透明可以吗?那我们试试吧
Canvas canvas = new Canvas(originBitmap);
Paint paint = new Paint();
paint.setColor(Color.WHITE);
paint.setAlpha(50);
paint.setStyle(Paint.Style.FILL);
canvas.drawPath(facePath,paint);
效果
感觉挺假的,我们知道,画笔是可以设置成高斯模糊的,那就来试试吧.
private static Bitmap createMask(final Path path, int color, @Nullable PointF position, int alpha, int blur_radius) {
if (path == null || path.isEmpty())
return null;
RectF bounds = new RectF();
path.computeBounds(bounds, true);
int width = (int) bounds.width();
int height = (int) bounds.height();
Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); // mutable
Canvas canvas = new Canvas(bitmap);
Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setMaskFilter(new BlurMaskFilter(blur_radius, BlurMaskFilter.Blur.NORMAL));
paint.setColor(color);
paint.setAlpha(alpha);
paint.setStyle(Paint.Style.FILL);
path.offset(-bounds.left, -bounds.top);
canvas.drawPath(path, paint);
if (position != null) {
position.x = bounds.left;
position.y = bounds.top;
}
return bitmap;
}
事实证明这样是可以的,但是效果还是不咋行,那我们在用原图来做一次渐变,刚好可以达到效果
private static Bitmap getGradientBitmapByXferomd(Bitmap originBitmap, float radius){
if(radius < 10) radius = 10;
Bitmap canvasBitmap = Bitmap.createBitmap(originBitmap.getWidth(),originBitmap.getHeight(), Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(canvasBitmap);
Paint paint = new Paint();
BitmapShader bitmapShader = new BitmapShader(originBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
RadialGradient radialGradient = new RadialGradient(originBitmap.getWidth() / 2, originBitmap.getHeight() / 2,
radius, Color.BLACK, Color.TRANSPARENT, Shader.TileMode.CLAMP);
paint.setShader(new ComposeShader(bitmapShader,radialGradient,new PorterDuffXfermode(PorterDuff.Mode.DST_IN)));
canvas.drawRect(new Rect(0,0,canvasBitmap.getWidth(),canvasBitmap.getHeight()), paint);
return canvasBitmap;
}
口红
关于口红也只是仅仅画上一层颜色,有了画笔,就可以和粉底一样的实现方式.
先看一下怎么连接的区域吧,为了方便,我直接采用了把外面的区域连接起来,然后在去做一次diff就可以了,代码如下
public static Path getMouthPath(String faceJson){
try {
JSONObject jsonObject = new JSONObject(faceJson);
JSONObject mouthJson = jsonObject.getJSONObject(“face”).getJSONObject(“landmark”).getJSONObject(“mouth”);
Path outPath = new Path();
Path inPath = new Path();
Point start = getPointByJson(mouthJson.getJSONObject(“upper_lip_0”));
outPath.moveTo(start.x,start.y);
for(int i = 1;i < 18;i++){
Point pointByJson = getPointByJson(mouthJson.getJSONObject(“upper_lip_” + i));
outPath.lineTo(pointByJson.x,pointByJson.y);
}
for(int i = 16;i > 0;i–){
Point pointByJson = getPointByJson(mouthJson.getJSONObject(“lower_lip_” + i));
outPath.lineTo(pointByJson.x,pointByJson.y);
}
outPath.close();
Point inStart = getPointByJson(mouthJson.getJSONObject(“upper_lip_32”));
inPath.moveTo(inStart.x,inStart.y);
for(int i = 46;i < 64;i++){
Point pointByJson = getPointByJson(mouthJson.getJSONObject(“upper_lip_” + i));
inPath.lineTo(pointByJson.x,pointByJson.y);
}
for(int i = 63;i >= 46;i–){
Point pointByJson = getPointByJson(mouthJson.getJSONObject(“lower_lip_” + i));
inPath.lineTo(pointByJson.x,pointByJson.y);
}
//取不同的地方
outPath.op(inPath, Path.Op.DIFFERENCE);
return outPath;
} catch (JSONException e) {
e.printStackTrace();
}
return null;
}
Path.op()方法需要在API 19及以上才可以使用,如果使用了低版本的api,可以直接使用canvas.clipPath().
腮红
只有粉底,那看上去,还是有点假,那是不是需要用画笔画上一个腮红呢?但是形状什么,不好搞定,所以选择了直接使用腮红素材,直接贴上去.
实现也相对容易一些.
public static void drawBlush(Canvas canvas, Bitmap faceBlush, Path path, int alpha) {
Paint paint = new Paint();
paint.setAlpha(alpha);
RectF rectF = new RectF();
path.computeBounds(rectF,true);
canvas.drawBitmap(faceBlush,null,rectF,paint);
}
眉毛
眉毛这个其实困扰了我很长时间,因为要把底部的眉毛给扣了,在装新的眉毛在上面,不然可能完全盖不住,眉形变化,识别准确率,会导致效果的直接变化.尝试了很多方法其中OpenCV里有一个著名的inpaint方法的图片修复方法,看别人写的去书印demo,也都还行,但是放到这里去眉毛,效果很差,是因为我使用不对,还是什么问题,有大神可以指点,提取周边的皮肤颜色去掉原来的眉毛.
最终还是放弃了去掉原来的眉毛,直接覆盖眉毛.
public static Path getLeftEyeBrow(String faceJson){
try {
JSONObject jsonObject = new JSONObject(faceJson);
JSONObject eye = jsonObject.getJSONObject(“face”).getJSONObject(“landmark”).getJSONObject(“left_eyebrow”);
Path path = new Path();
Point start = getPointByJson(eye.getJSONObject(“left_eyebrow_0”));
path.moveTo(start.x,start.y);
for(int i= 1;i< 64;i++){
Point point = getPointByJson(eye.getJSONObject(“left_eyebrow_”+i));
path.lineTo(point.x,point.y);
}
path.close();
return path;
}catch (Exception e){
e.printStackTrace();
}
return null;
}
public static void draw(Canvas canvas, Bitmap eyeBrowRes, Path path, int alpha){
Paint paint = new Paint();
paint.setAlpha(alpha);
RectF rectF = new RectF();
path.computeBounds(rectF,true);
canvas.drawBitmap(eyeBrowRes,new Rect(0,0,eyeBrowRes.getWidth(),eyeBrowRes.getHeight() - 30),rectF,paint);
}
最终效果
但是文中的开始给的效果那张照片,因为识别偏差,导致效果不太好.
眼睛(睫毛,眼影,双眼皮,眼线,美瞳)
眼睛部分是最复杂的部分了,因为可以画的实在是太多了.
这就将两个地方的实现,其他具体实现可以参考实际代码,先看一下这些不是主要的素材吧
美瞳
要向眼睛里画美瞳,那么我们首先要有这个区域,区域人脸关键点已经给了,那么,我们知道,人的眼睛一般是椭圆性的,不可能直接是圆形的,所以画的时候,需要和眼睛的区域做一个交集来得到结果.
public static void drawContact(Canvas canvas, Bitmap contactBitmap, Path eyePath, Point centerPoint, int eyeRadius, int alpha) {
Path contactPath = new Path();
contactPath.addCircle(centerPoint.x,centerPoint.y,eyeRadius, Path.Direction.CCW);
//重点地方,做交集得到结果
contactPath.op(eyePath, Path.Op.INTERSECT);
RectF bounds = new RectF();
contactPath.computeBounds(bounds,true);
bounds.offset(1,0);
Paint paint = new Paint();
paint.setAlpha(alpha);
canvas.drawBitmap(contactBitmap,new Rect(0,30,contactBitmap.getWidth(),contactBitmap.getHeight() - 60),bounds,paint);
}
睫毛
我们知道,睫毛有上睫毛和下睫毛,那么怎么把这个眉毛画上去呢? 其实我们知道,一般把图片绘制到目标区域需要经过,平移,旋转,缩放来进行.
睫毛我们选取了素材上的三个点,和眼睛上的三个点来做上述的三个操作.
有了这三个点,我们就可以计算宽高比,角度,使用三角函数可以很容易计算得到.
旋转角度
使用人眼睛上对应的三个点来计算旋转角度,(如果人的头像是正的,可以不用计算,但是人可能偏头,什么,需要计算旋转角度,来warp)
/**
- @param p1 三角形顶点
- @param p2 三角形顶点
- @param p3 三角形顶点
- @return 三角形顶点p3 到 p1,p3垂直高度
*/
public double getTriangleHeight(Point p1, Point p2, Point p3) {
int a = p1.x;
int b = p1.y;
int c = p2.x;
int d = p2.y;
int e = p3.x;
int f = p3.y;
//计算三角形面积
double S = (a * d + b * e + c * f - a * f - b * c - d * e) / 2;
int lengthSquare = (p1.x - p2.x) * (p1.x - p2.x) + (p1.y - p2.y) * (p1.y - p2.y);
return Math.abs(2 * S / Math.sqrt(lengthSquare));
}
//获取坐标轴内两个点间的距离
public double getLength(Point p1, Point p2) {
double diff_x = Math.abs(p1.x - p2.x);
double diff_y = Math.abs(p1.y - p2.y);
//两个点在 横纵坐标的差值与两点间的直线 构成直角三角形。length_pow等于该距离的平方
double length_pow = Math.pow(diff_x, 2) + Math.pow(diff_y, 2);
double sqrt = Math.sqrt(length_pow);
return sqrt == 0?0.001f:(float) sqrt;
}
static double pi180 = 180 / Math.PI;
public double getAngle(Point p1, Point p2, Point p3) {
double _cos1 = getCos(p1, p2, p3);//第一个点为顶点的角的角度的余弦值
return 90 - Math.acos(_cos1) * pi180;
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)
最后
我的面试经验分享可能不会去罗列太多的具体题目,因为我依然认为面试经验中最宝贵的不是那一个个具体的题目或者具体的答案,而是结束面试时,那一刻你的感受以及多天之后你的回味~
很多人在刚接触这个行业的时候或者是在遇到瓶颈期的时候,总会遇到一些问题,比如学了一段时间感觉没有方向感,不知道该从那里入手去学习,对此我整理了一些资料,需要的可以免费分享给大家
在这里小编分享一份自己收录整理上述技术体系图相关的几十套腾讯、头条、阿里、美团等公司的面试题,把技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节,由于篇幅有限,这里以图片的形式给大家展示一部分。
【Android核心高级技术PDF文档,BAT大厂面试真题解析】
【算法合集】
【延伸Android必备知识点】
【Android部分高级架构视频学习资源】
**Android精讲视频领取学习后更加是如虎添翼!**进军BATJ大厂等(备战)!现在都说互联网寒冬,其实无非就是你上错了车,且穿的少(技能),要是你上对车,自身技术能力够强,公司换掉的代价大,怎么可能会被裁掉,都是淘汰末端的业务Curd而已!现如今市场上初级程序员泛滥,这套教程针对Android开发工程师1-6年的人员、正处于瓶颈期,想要年后突破自己涨薪的,进阶Android中高级、架构师对你更是如鱼得水,赶快领取吧!
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!
6640)]
【延伸Android必备知识点】
[外链图片转存中…(img-fQ9TItyO-1712450116640)]
【Android部分高级架构视频学习资源】
**Android精讲视频领取学习后更加是如虎添翼!**进军BATJ大厂等(备战)!现在都说互联网寒冬,其实无非就是你上错了车,且穿的少(技能),要是你上对车,自身技术能力够强,公司换掉的代价大,怎么可能会被裁掉,都是淘汰末端的业务Curd而已!现如今市场上初级程序员泛滥,这套教程针对Android开发工程师1-6年的人员、正处于瓶颈期,想要年后突破自己涨薪的,进阶Android中高级、架构师对你更是如鱼得水,赶快领取吧!
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!