The preface

This article is the second part of the code of color makeup, mainly introduces how to carry out local image deformation on Android, and achieve the popular effects of big eyes, thin face and long legs on Douyin.

Before we begin, let’s review the main content of the last article. Drawing half of the code looks like this

public enum Region {

    FOUNDATION("Foundation"),
    BLUSH("Blush"),
    LIP("Lip"),
    BROW("Eyebrows"),

    EYE_LASH("Eyelash"),
    EYE_CONTACT("Lenses"),
    EYE_DOUBLE("Double eyelid"),
    EYE_LINE("Line"),
    EYE_SHADOW("Eye shadow");

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

Use code to draw various effects. Android: Let your “goddess” reverse attack, code stroking makeup (makeup)

The last post and the code of this article are the same address, have been hosted on Github, if you like, welcome to a star, thank you github.com/DingProg/Ma…

Now we start our topic today, the local deformation of the human body (image), if you want to see the effect directly, you can click on the catalog to quickly slide to the effect area.

Big eye

The effect

implementation

Picture partial scaling principle

As we know, it is relatively easy to zoom in and out of pictures, and the corresponding library has been packaged and can be used directly (we do not need to pay attention to the image zoom in and out of interpolation processing). However, the local zoom in and out of the picture is not directly encapsulated. For example, Android bitmap does not directly deal with the local ZOOM in and out of the API.

So let’s first look at what is local scaling of a graph?

Local scaling, we can imagine that the central point is scaled in a small proportion, while the edges are scaled in a small proportion, or the border area is almost unchanged, so that a smooth effect can be achieved. If you change it directly to only the selected circle, the edge becomes a fractured scale.

We use Interactive Image Warping, a doctoral thesis in 1993, to scale local images

Code implementation

Since we want to enlarge the eye, we assign the value ️ of the corresponding point near the center of the circle to the remote point. According to the ideas mentioned in the paper, some modifications are made to achieve the following.

  /** * Eye enlargement algorithm *@paramBitmap Original bitmap *@paramCenterPoint magnified centerPoint *@paramRadius Zoom radius *@paramSizeLevel size [0,4] *@returnZoom in on the image behind the eyes */
    public static Bitmap magnifyEye(Bitmap bitmap, Point centerPoint, int radius, float sizeLevel) {
        TimeAopUtils.start();
        Bitmap dstBitmap = bitmap.copy(Bitmap.Config.RGB_565, true);
        int left = centerPoint.x - radius < 0 ? 0 : centerPoint.x - radius;
        int top = centerPoint.y - radius < 0 ? 0 : centerPoint.y - radius;
        int right = centerPoint.x + radius > bitmap.getWidth() ? bitmap.getWidth() - 1 : centerPoint.x + radius;
        int bottom = centerPoint.y + radius > bitmap.getHeight() ? bitmap.getHeight() - 1 : centerPoint.y + radius;
        int powRadius = radius * radius;

        int offsetX, offsetY, powDistance, powOffsetX, powOffsetY;

        int disX, disY;

        // When the value is negative, the value is reduced
        float strength = (5 + sizeLevel * 2) / 10;

        for (int i = top; i <= bottom; i++) {
            offsetY = i - centerPoint.y;
            for (int j = left; j <= right; j++) {
                offsetX = j - centerPoint.x;
                powOffsetX = offsetX * offsetX;
                powOffsetY = offsetY * offsetY;
                powDistance = powOffsetX + powOffsetY;

                if (powDistance <= powRadius) {
                    double distance = Math.sqrt(powDistance);
                    double sinA = offsetX / distance;
                    double cosA = offsetY / distance;

                    double scaleFactor = distance / radius - 1;
                    scaleFactor = (1 - scaleFactor * scaleFactor * (distance / radius) * strength);

                    distance = distance * scaleFactor;
                    disY = (int) (distance * cosA + centerPoint.y + 0.5);
                    disY = checkY(disY, bitmap);
                    disX = (int) (distance * sinA + centerPoint.x + 0.5);
                    disX = checkX(disX, bitmap);
                    // The center point is not processed
                    if(! (j == centerPoint.x && i == centerPoint.y)) { dstBitmap.setPixel(j, i, bitmap.getPixel(disX, disY)); } } } } TimeAopUtils.end("eye"."magnifyEye");
        return dstBitmap;
    }

    private static int checkY(int disY, Bitmap bitmap) {
        if (disY < 0) {
            disY = 0;
        } else if (disY >= bitmap.getHeight()) {
            disY = bitmap.getHeight() - 1;
        }
        return disY;
    }

    private static int checkX(int disX, Bitmap bitmap) {
        if (disX < 0) {
            disX = 0;
        } else if (disX >= bitmap.getWidth()) {
            disX = bitmap.getWidth() - 1;
        }
        return disX;
    }
Copy the code

The points before and after scaling are calculated using the calculation rules as shown in the figure below.

With this method, we can use the results of face recognition, and pass in the center of the eye to achieve automatic eye widening effect.

    Bitmap magnifyEye = MagnifyEyeUtils.magnifyEye(bitmap,
    Objects.requireNonNull(FacePoint.getLeftEyeCenter(faceJson)),
    FacePoint.getLeftEyeRadius(faceJson) * 3.3);
Copy the code

Slightly less than

  • The part shown in the code is not interpolated (the code directly uses values instead of two points, three points, for interpolation calculation). If the amplification is very large, the effect may be blurred
  • Android bitmaps get pixels directly, which is inefficient. The correct way is to get all the corresponding pixels at once, then operate on the array (consider the content, directly use each to read/set), after the operation, set back.

Thin face

The effect

Manual mode

Automatic mode

implementation

The effect of big eyes uses bitmap to operate pixels directly, which is a little inefficient. Therefore, another implementation method is adopted to achieve face thinning and long legs.

Cavans drawBitmapMesh method

// Canvas
  /** * Draw the bitmap through the mesh, where mesh vertices are evenly distributed across the * bitmap. There are meshWidth+1 vertices across, and meshHeight+1 vertices down. The verts * array is accessed in row-major order, so that the first meshWidth+1 vertices are distributed * across the top of the bitmap from left to right. A more general  version of this method is * drawVertices(). * * Prior to API level {@value Build.VERSION_CODES#P} vertOffset and colorOffset were ignored,
     * effectively treating them as zeros. In API level {@value Build.VERSION_CODES#P} and above
     * these parameters will be respected.
     *
     * @param bitmap The bitmap to draw using the mesh
     * @param meshWidth The number of columns in the mesh. Nothing is drawn if this is 0
     * @param meshHeight The number of rows in the mesh. Nothing is drawn if this is 0
     * @param verts Array of x,y pairs, specifying where the mesh should be drawn. There must be at
     *            least (meshWidth+1) * (meshHeight+1) * 2 + vertOffset values in the array
     * @param vertOffset Number of verts elements to skip before drawing
     * @param colors May be null. Specifies a color at each vertex, which is interpolated across the
     *            cell, and whose values are multiplied by the corresponding bitmap colors. If not
     *            null, there must be at least (meshWidth+1) * (meshHeight+1) + colorOffset values
     *            in the array.
     * @param colorOffset Number of color elements to skip before drawing
     * @param paint May be null. The paint used to draw the bitmap
     */
    public void drawBitmapMesh(@NonNull Bitmap bitmap, int meshWidth, int meshHeight,
            @NonNull float[] verts, int vertOffset, @Nullable int[] colors, int colorOffset,
            @Nullable Paint paint) {
        super.drawBitmapMesh(bitmap, meshWidth, meshHeight, verts, vertOffset, colors, colorOffset,
                paint);
    }
Copy the code

The method, roughly speaking, is to use the grid method of image segmentation, and then manipulate the grid, the image can achieve a distorted effect.

Code implementation

Drag in Gif to perform automatic face trimming, this is a custom View, on the View by gesture manipulation, to change the mesh, and then call redraw.

The first step is to initialize the image and place it in the center of the View

   private void zoomBitmap(Bitmap bitmap, int width, int height) {
        if(bitmap == null) return;
        int dw = bitmap.getWidth();
        int dh = bitmap.getHeight();

        float scale = 1.0 f;

        // The width of the image is larger than the width of the control. The height of the image is smaller than the height of the space
        if (dw > width && dh < height) {
            scale = width * 1.0 f / dw;
        }

        // The width of the image is smaller than the width of the control. The height of the image is larger than the height of the space
        if (dh > height && dw < width) {
            scale = height * 1.0 f / dh;
        }

        / / narrow values
        if (dw > width && dh > height) {
            scale = Math.min(width * 1.0 f / dw, height * 1.0 f / dh);
        }

        / / value
        if (dw < width && dh < height) {
            scale = Math.min(width * 1.0 f / dw, height * 1.0 f / dh);
        }

        / / to narrow
        if (dw == width && dh > height) {
            scale = height * 1.0 f / dh;
        }
        dx = width / 2 - (int) (dw * scale + 0.5 f) / 2;
        dy = height / 2 - (int) (dh * scale + 0.5 f) / 2;

        mScale = scale;
        restoreVerts();
    }
Copy the code

Next, initialize the grid

    // How many cells to divide the image into
    private int WIDTH = 200;
    private int HEIGHT = 200;

    // The number of coordinates of intersections
    private int COUNT = (WIDTH + 1) * (HEIGHT + 1);

    // Hold the coordinates of COUNT
    //x0, y0, x1, y1......
    private float[] verts = new float[COUNT * 2];

    // Save the original coordinates
    private float[] orig = new float[COUNT * 2];
   private void restoreVerts(a) {
        int index = 0;
        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;
                // Put the X coordinate in even digits
                verts[index * 2] = fx;
                orig[index * 2] = verts[index * 2];
                // Put the Y coordinates in odd digits
                verts[index * 2 + 1] = fy;
                orig[index * 2 + 1] = verts[index * 2 + 1];
                index += 1;
            }
        }
        showCircle = false;
        showDirection = false;
    }
Copy the code

So the last step is to draw this picture

   @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if(mBitmap == null) return;
        canvas.save();
        canvas.translate(dx, dy);
        canvas.scale(mScale, mScale);
        if (isShowOrigin) {
            canvas.drawBitmapMesh(mBitmap, WIDTH, HEIGHT, orig, 0.null.0.null);
        } else {
            canvas.drawBitmapMesh(mBitmap, WIDTH, HEIGHT, verts, 0.null.0.null);
        }

        canvas.restore();
        if (showCircle && isEnableOperate) {
            canvas.drawCircle(startX, startY, radius, circlePaint);
            canvas.drawCircle(startX, startY, 5, directionPaint);
        }
        if(showDirection && isEnableOperate) { canvas.drawLine(startX, startY, moveX, moveY, directionPaint); }}Copy the code

So next, it’s time to manipulate the mesh and create some distortion effects. Adding Event Listeners

  @Override
    public boolean onTouchEvent(MotionEvent event) {
        if(! isEnableOperate)return true;
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                // Draw the deformation area
                startX = event.getX();
                startY = event.getY();
                showCircle = true;
                invalidate();
                break;
            case MotionEvent.ACTION_MOVE:
                // Draw the deformation direction
                moveX = event.getX();
                moveY = event.getY();
                showDirection = true;
                invalidate();
                break;
            case MotionEvent.ACTION_UP:
                showCircle = false;
                showDirection = false;

                // Call the warp method to distort the verts array based on the coordinate points of the touch screen events
                if(mBitmap ! =null&& verts! =null && !mBitmap.isRecycled()) {
                    warp(startX, startY, event.getX(), event.getY());
                }

                if(onStepChangeListener ! =null) {
                    onStepChangeListener.onStepChange(false);
                }
                break;
        }
        return true;
    }

Copy the code

The key here is to look at our wrap method to manipulate the deformation of the mesh. Just to brief the idea, we just saw the enlargement of the eye, which is the central part, the operation range is large, and the remote place is basically not operated.

Let’s look at the code

    private void warp(float startX, float startY, float endX, float endY) {
        startX = toX(startX);
        startY = toY(startY);
        endX = toX(endX);
        endY = toY(endY);

        // Calculate the drag distance
        float ddPull = (endX - startX) * (endX - startX) + (endY - startY) * (endY - startY);
        float dPull = (float) Math.sqrt(ddPull);
        //dPull = screenWidth - dPull >= 0.0001f? Screenwidth-dpull: 0.0001f;
        if (dPull < 2 * r) {
            if (isSmllBody) {
                dPull = 1.8 f * r;
            } else {
                dPull = 2.5 f* r; }}int powR = r * r;
        int index = 0;
        int offset = 1;
        for (int i = 0; i < HEIGHT + 1; i++) {
            for (int j = 0; j < WIDTH + 1; j++) {
                // Boundary areas are not processed
                if(i < offset || i > HEIGHT - offset || j < offset || j > WIDTH - offset){
                    index = index + 1;
                    continue;
                }
                // Calculate the distance between each coordinate point and the touch point
                float dx = verts[index * 2] - startX;
                float dy = verts[index * 2 + 1] - startY;
                float dd = dx * dx + dy * dy;

                if (dd < powR) {
                    // Distortion coefficient
                    double e = (powR - dd) * (powR - dd) / ((powR - dd + dPull * dPull) * (powR - dd + dPull * dPull));
                    double pullX = e * (endX - startX);
                    double pullY = e * (endY - startY);
                    verts[index * 2] = (float) (verts[index * 2] + pullX);
                    verts[index * 2 + 1] = (float) (verts[index * 2 + 1] + pullY);

                   // check
                    if(verts[index * 2] < 0){
                        verts[index * 2] = 0;
                    }
                    if(verts[index * 2] > mBitmap.getWidth()){
                        verts[index * 2] =  mBitmap.getWidth();
                    }

                    if(verts[index * 2 + 1] < 0){
                        verts[index * 2 +1] = 0;
                    }
                    if(verts[index * 2 + 1] > mBitmap.getHeight()){
                        verts[index * 2 + 1] = mBitmap.getHeight();
                    }
                }
                index = index + 1;
            }
        }
        invalidate();
    }
Copy the code

You just have to do different transformations of X and Y within the operating radius.

Automatic face trimming implementation

In fact, with the above drag, it is much easier to achieve automatic face trimming, let’s simulate dragging a few key points.

The implementation code is as follows


    /** ** Algorithm **@paramBitmap Original bitmap *@returnThe following images */
    public static Bitmap smallFaceMesh(Bitmap bitmap, List<Point> leftFacePoint,List<Point> rightFacePoint,Point centerPoint, int level) {
        // The number of coordinates of intersections
        int COUNT = (WIDTH + 1) * (HEIGHT + 1);
        // Hold the coordinates of COUNT
        float[] verts = new float[COUNT * 2];
        float bmWidth = bitmap.getWidth();
        float bmHeight = bitmap.getHeight();

        int index = 0;
        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;
                // Put the X coordinate in even digits
                verts[index * 2] = fx;
                // Put the Y coordinates in odd digits
                verts[index * 2 + 1] = fy;
                index += 1; }}int r = 180 + 15 * level;
        warp(COUNT,verts,leftFacePoint.get(16).x,leftFacePoint.get(16).y,centerPoint.x,centerPoint.y,r);
        warp(COUNT,verts,leftFacePoint.get(46).x,leftFacePoint.get(46).y,centerPoint.x,centerPoint.y,r);

        warp(COUNT,verts,rightFacePoint.get(16).x,rightFacePoint.get(16).y,centerPoint.x,centerPoint.y,r);
        warp(COUNT,verts,rightFacePoint.get(46).x,rightFacePoint.get(46).y,centerPoint.x,centerPoint.y,r);

        Bitmap resultBitmap = Bitmap.createBitmap(bitmap.getWidth(),bitmap.getHeight(), Bitmap.Config.ARGB_8888);
        Canvas canvas = new Canvas(resultBitmap);
        Paint paint = new Paint();
        canvas.drawBitmapMesh(bitmap,WIDTH, HEIGHT,verts,0.null.0.null);
        return resultBitmap;
    }
Copy the code

Big long legs

If you’re tired of looking at the code, let’s look at a celebrity beauty, does anyone know who this is? Asked two or three programmer friends, or do not know, or say this is Yang Mi? Alas, programmers know so many stars?

The effect

implementation

The operation of the thin face above needs to be done in both X and Y, so the drawing of the long legs is easier, just in the Y direction.

In the first image, the overlay layer on the top is a custom View, and the lower layer directly uses the View with the thin face function. It puts the image in the center, but does not allow gestures to manipulate the image.

  smallFaceView.setEnableOperate(false);
Copy the code

Upper View core code

AdjustLegView (AdjustLegView

  @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //line
        canvas.drawRect(0, topLine, getWidth(), topLine + LINEHIGHT, paint);
        //line
        canvas.drawRect(0, bottomLine, getWidth(), bottomLine + LINEHIGHT, paint);

        if(selectPos ! = -1) {
            swap();
            rect.set(0, topLine + LINEHIGHT, getWidth(), bottomLine);
            canvas.drawRect(rect, bgPaint);
            if(tipStr ! =null) {@SuppressLint("DrawAllocation") Rect textRect = new Rect();
                textPaint.getTextBounds(tipStr,0,tipStr.length()-1,textRect);
                canvas.drawText(tipStr,rect.left + (rect.width()/ 2 -textRect.width()/2),
                        rect.top + (rect.height()/ 2 -textRect.height()/2),textPaint); }}}Copy the code

Gesture interaction

//AdjustLegView  
 @Override
    public boolean onTouchEvent(MotionEvent event) {
        float y = event.getY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                selectPos = checkSelect(y);
                lastY = y;
                if(selectPos ! = -1&& listener ! =null){
                    listener.down();
                }
                break;
            case MotionEvent.ACTION_MOVE:
                if (selectPos == 1) {
                    // The minimum offset is 20
                    topLine += checkLimit(y - lastY);
                    invalidate();
                }
                if (selectPos == 2) {
                    bottomLine += checkLimit(y - lastY);
                    invalidate();
                }
                lastY = y;
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                selectPos = -1;
                invalidate();
                if( listener ! =null){
                    listener.up(rect);
                }
                break;
        }
        return true;
    }

    private float checkLimit(float offset) {
        if (selectPos == 1) {
            if(topLine + offset > minLine && topLine + offset < maxLine){
                returnoffset; }}if (selectPos == 2) {
            if(bottomLine + offset > minLine && bottomLine + offset < maxLine){
                returnoffset; }}return 0;
    }

    private int checkSelect(float y) {
        selectPos = -1;
        RectF rect = new RectF(0, y - OFFSETY, 0, y + OFFSETY);
        float min = -1;
        if (topLine >= rect.top && topLine <= rect.bottom) {
            selectPos = 1;
            min = rect.bottom - topLine;
        }

        if (bottomLine >= rect.top && bottomLine <= rect.bottom) {
            if (min > bottomLine - rect.top || min == -1) {
                selectPos = 2; }}return selectPos;
    }
Copy the code

Big long legs

So how do you lengthen your legs? Let’s go straight to the algorithms

    private static void warpLeg(int COUNT, float verts[], float centerY,int totalHeight,float region,float strength) {
        float  r = region / 2; // Scale area strength

        for (int i = 0; i < COUNT * 2; i += 2) {
            // Calculate the distance between each coordinate point and the touch point
            float dy = verts[i + 1] - centerY;
            double e = (totalHeight - Math.abs(dy)) / totalHeight;
            if(Math.abs(dy) < r){
                // Elongate the ratio
                double pullY = e * dy * strength;
                verts[i + 1] = (float) (verts[i + 1] + pullY);
            }else if(Math.abs(dy) < 2 * r || dy > 0) {double pullY = e * e * dy * strength;
                verts[i + 1] = (float) (verts[i + 1] + pullY);
            }else if(Math.abs(dy) < 3 * r){
                double pullY = e * e * dy * strength /2;
                verts[i + 1] = (float) (verts[i + 1] + pullY);
            }else {
                double pullY = e * e * dy * strength /4;
                verts[i + 1] = (float) (verts[i + 1] + pullY);
            }
        }
    }
    
    Canvas canvas = new Canvas(resultBitmap);
    canvas.drawBitmapMesh(bitmap, WIDTH, HEIGHT, verts, 0.null.0.null);
        return resultBitmap;
Copy the code

The drawBitmapMesh is still used. In the algorithm part, only Y is operated, but X is not operated. The farther the distance, the smaller the operation amplitude. Try to lengthen only the legs, leaving the rest of the body still.

conclusion

This article will show you how to achieve some cool effects on Android using native apis. All code in this article is hosted on Github, if you need it, welcome star, Github Makeup, thank you very much, future updates will be in this library.

This article big eye algorithm, thin face algorithm only source network, if there is infringement, please contact the author immediately delete. The algorithm of long legs is obtained by the author and can be used by himself.

Recommended reading

Android: Make your “goddess” retrofit, code to create a Flutter PIP effect.