Skip to content
让你的“女神”逆袭,代码撸彩妆(画妆)
Java
Branch: master
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
app Add MagnifyEye Aug 22, 2019
gradle/wrapper add code to github Aug 8, 2019
.gitignore add code to github Aug 8, 2019
README.md update readMe Aug 9, 2019
build.gradle add code to github Aug 8, 2019
gradle.properties add code to github Aug 8, 2019
gradlew add code to github Aug 8, 2019
gradlew.bat add code to github Aug 8, 2019
settings.gradle add code to github Aug 8, 2019

README.md

导读: 本文使用代码撸一个你心目中的“女神”,代码上彩妆。 技术主要内容是Canvas的应用.

背景

最近刷抖音,看到一些大汉变“女神”,这化妆可以称之为逆袭啊,大汉变萝莉.

作为技术,大部分是男生,并且经常有男生被女票怼我的口红有多少色号,那是一样的红色吗?

为了广大男同胞能好好的“活在”女票跟前,今天来讲述一下【化妆】,用代码撸一个好看的女票.

效果

先上效果在说吧,学习抖音的化妆教程方式,就画一半,方便形成对比,效果如下:

如果正在看篇文章的人是个妹子,你应该很清楚画了些什么吧?为了照顾广大爷们,先讲一下画了些什么吧.

直接看代码吧

public enum Region {

    FOUNDATION("粉底"),
    BLUSH("腮红"),
    LIP("唇彩"),
    BROW("眉毛"),

    EYE_LASH("睫毛"),
    EYE_CONTACT("美瞳"),
    EYE_DOUBLE("双眼皮"),
    EYE_LINE("眼线"),
    EYE_SHADOW("眼影");

    private String name;
    Region(String name) {
        this.name = name;
    }
}

女程序员们,你们看出这么多来了吗?其实我也是挺佩服我自己的,一个男生知道那么多,吓坏了我很多小伙伴,宅男的世界你们不懂。

磨皮

砍柴不误摸到工,我们知道,一般的痘痘用粉底是盖不住的,那么先来一次磨皮吧,把"底板"搞干净了,我们使用一个高通滤波器(去掉低频信号,来达到保留细节的效果) + Curve Adjustment某些频率应用调整 然后在融合来达到磨皮的目的 流程大概是这样的

(图片来源下面所述库里)

效果如下 080819243542_01.png

本文就没有在撸一个这样的库,直接使用了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;
    }

宽高比旋转角度

有了角度,那么我们在计算宽高比.

 /**
     * @param targetP1 缩放目标线段点p1
     * @param targetP2 缩放目标线段点p2
     * @param P1       待缩放线段点p1
     * @param P2       待缩放线段点p2
     * @return 水平高度比值
     */
    public double computeScaleX(Point targetP1, Point targetP2, Point P1, Point P2) {
        int targetLengthSquare = (targetP1.x - targetP2.x) * (targetP1.x - targetP2.x) + (targetP1.y - targetP2.y) * (targetP1.y - targetP2.y);
        int sourceLengthSquare = (P1.x - P2.x) * (P1.x - P2.x) + (P1.y - P2.y) * (P1.y - P2.y);
        double scale = targetLengthSquare * 1.0 / sourceLengthSquare;
        return Math.sqrt(scale);
    }

    /**
     * @param targetP1 缩放目标三角形顶点
     * @param targetP2 缩放目标三角形顶点
     * @param targetP3 缩放目标三角形顶点
     * @param P1       待缩放三角形顶点
     * @param P2       待缩放三角形顶点
     * @param P3       待缩放三角形顶点
     * @return 垂直高度比值
     */
    public double computeScaleY(Point targetP1, Point targetP2, Point targetP3, Point P1, Point P2, Point P3) {
        double targetHeight = getTriangleHeight(targetP1, targetP2, targetP3);
        double sourceHeight = getTriangleHeight(P1, P2, P3);
        return targetHeight / sourceHeight;
    }

平移

因为我们的图形是巨型,不可能从开始位置往上画,那就需要把画的位置通过平移,来达到第一个点的位置和对应位置的点,对应上.

eyeAngleAndScaleCalc.topP1.x - (int) (bean.topP1.x * eyeAngleAndScaleCalc.topScaleX),
                eyeAngleAndScaleCalc.topP1.y - (int) (bean.topP1.y * eyeAngleAndScaleCalc.topScaleY)

有了这些步骤,那既可以直接合成绘制了,代码如下

 public static void drawLash(Context context, Canvas canvas, EyeAngleAndScaleCalc.Bean bean, List<Point> pointList, int alpha, boolean needMirror) {
        EyeAngleAndScaleCalc eyeAngleAndScaleCalc = new EyeAngleAndScaleCalc(pointList,bean);

        Paint paint = new Paint();
        paint.setAlpha(alpha);

        Bitmap resTopBitmap = BitmapUtils.getBitmapByAssetsName(context,bean.resTop);
        Bitmap scaledBitmapTop = Bitmap.createScaledBitmap(resTopBitmap, (int) (resTopBitmap.getWidth() * eyeAngleAndScaleCalc.topScaleX + 0.5),
                (int) (resTopBitmap.getHeight() * eyeAngleAndScaleCalc.topScaleY + 0.5), true);
        resTopBitmap.recycle();


        Bitmap resBottomBitmap = null;
        Bitmap scaledBitmapBottom = null;
        if (!TextUtils.isEmpty(bean.resBottom)) {
            resBottomBitmap = BitmapUtils.getBitmapByAssetsName(context,bean.resBottom);
            scaledBitmapBottom = Bitmap.createScaledBitmap(resBottomBitmap, (int) (resBottomBitmap.getWidth() * eyeAngleAndScaleCalc.bottomScaleX + 0.5),
                    (int) (resBottomBitmap.getHeight() * eyeAngleAndScaleCalc.bottomScaleY + 0.5), true);
            resBottomBitmap.recycle();
        }

        if (needMirror) {
            Matrix matrix = new Matrix();
            matrix.postScale(-1, 1);   //镜像水平翻转
            scaledBitmapTop = Bitmap.createBitmap(scaledBitmapTop, 0, 0, scaledBitmapTop.getWidth(), scaledBitmapTop.getHeight(), matrix, true);
            if (resBottomBitmap != null) {
                scaledBitmapBottom = Bitmap.createBitmap(scaledBitmapBottom, 0, 0, scaledBitmapBottom.getWidth(), scaledBitmapBottom.getHeight(), matrix, true);
            }
        }

        canvas.save();
        //canvas.rotate(eyeAngleAndScaleCalc.getTopEyeAngle(), eyeAngleAndScaleCalc.topP1.x, eyeAngleAndScaleCalc.topP1.y);
        canvas.drawBitmap(scaledBitmapTop,
                eyeAngleAndScaleCalc.topP1.x - (int) (bean.topP1.x * eyeAngleAndScaleCalc.topScaleX),
                eyeAngleAndScaleCalc.topP1.y - (int) (bean.topP1.y * eyeAngleAndScaleCalc.topScaleY), paint);
        canvas.restore();

        if (scaledBitmapBottom != null) {
            canvas.save();
            canvas.rotate(eyeAngleAndScaleCalc.getBottomEyeAngle(), eyeAngleAndScaleCalc.bottomP1.x, eyeAngleAndScaleCalc.bottomP1.y);
            canvas.drawBitmap(scaledBitmapBottom, eyeAngleAndScaleCalc.bottomP1.x,
                    eyeAngleAndScaleCalc.bottomP1.y - (int) (bean.bottomP1.y * eyeAngleAndScaleCalc.bottomScaleY), paint);
            canvas.restore();
            scaledBitmapBottom.recycle();
        }
        scaledBitmapTop.recycle();
    }

眼睛部分,略微复杂一些,具体代码可以查看 Github Makeup ,如果你觉得还可以,可以给一个star吗?谢谢

其他

我们知道,上述内容只是对脸上进行了一些化妆,那要成为真正的“美女”,可能还要打上问号? 那什么样的化妆才是真正的美女呢,一般是底子好的人。在加上化妆就更漂亮了,那一张照片,要变的底子好,一般有那些方式呢? 这里提供一些思路(包含美体)

public enum BeautyType {

    INPAINT(1,"祛斑"),
    SMALLFACE(2,"瘦脸"),
    LONGLEG(3,"大长腿增高"),
    EYE(4,"眼睛放大"),
    BREST(5,"丰胸"),
    WHITE(7,"美白"),
    MAKEUP(8,"美妆"),
    SMALLBODY(9,"瘦脸瘦身");

    private int type;
    private String name;

    BeautyType(int type, String name) {
        this.type = type;
        this.name = name;
    }

    public int getType() {
        return type;
    }

    public String getName() {
        return name;
    }
}

如果只针对脸部,那么就只需要,磨皮,美白,祛斑,大眼,瘦脸等功能了.

文末

今天的文章分享到这就结束了,这些算法,目前知网论文库里都有,可以查看后轻松实现. 如果后续还想在搞就推出下篇博客,打造完美身材.

上述内容资源,使用完后请在24小时内删除,如果有侵权请联系作者立刻删除.

还有一篇比较好玩的博客推荐给大家,Flutter版本的 Flutter PIP(画中画)效果的实现

推荐阅读

Flutter高性能原理 Android 绘制原理浅析【干货】

You can’t perform that action at this time.