本篇文章是代碼擼彩妝的第二篇, 主要介紹在Android上怎麼進行圖片的局部變形,並實現抖音上比較火的大眼,瘦臉,大長腿特效.java
在開始以前咱們先來回顧上一篇的主要內容.
使用代碼畫一半的效果以下 git
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;
}
}
複製代碼
使用代碼畫出各類效果. 上一篇的文章地址 Android:讓你的「女神」逆襲,代碼擼彩妝(畫妝)程序員
上一篇和本篇的代碼所在地址一致,都已經託管到github,若是你喜歡,歡迎給一個star,謝謝 github.com/DingProg/Ma…github
如今開始咱們今天的主題,人體(圖像)的局部變形,若是要直接看效果的話,能夠點擊目錄快速滑到效果區域.算法
咱們知道,圖片的放大縮小,是比較容易的事,相應的庫已經封裝好了,能夠直接使用(咱們並不須要關注圖形放大縮小的插值處理等). 可是圖片的局部放大縮小,並無直接封裝好,好比Android裏面的bitmap,並無直接局部處理放大縮小的API.canvas
那咱們先來看一下什麼是圖形的局部縮放?數組
局部的縮放,咱們能夠想象成中心點被縮放的比例比較小,而邊緣的地方被縮放的比例很小,或者邊界區域幾乎沒有變化,這樣就能夠達到一種平滑的效果。若是直接只對選中的圓形區域,變化的話,那邊緣就變成了斷裂式的縮放.網絡
借用1993年的一篇博士論文 Interactive Image Warping 對局部圖片進行縮放ide
其中a爲縮放因子,當a=0時,不縮放既然要讓眼睛放大,那麼咱們就把對應的近圓心的點的值️賦給遠心點。 按照論文裏所提到的思路,進行部分修改,實現以下.post
/** * 眼睛放大算法 * @param bitmap 原來的bitmap * @param centerPoint 放大中心點 * @param radius 放大半徑 * @param sizeLevel 放大力度 [0,4] * @return 放大眼睛後的圖片 */
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;
//當爲負數時,爲縮小
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);
//中心點不作處理
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;
}
複製代碼
其中裏面計算縮放先後後的點,使用的是以下圖所示的計算規則計算.
有了這個方法,咱們藉助人臉識別的結果,把眼睛中心部分傳入進去就能夠實現自動大眼的效果了.
Bitmap magnifyEye = MagnifyEyeUtils.magnifyEye(bitmap,
Objects.requireNonNull(FacePoint.getLeftEyeCenter(faceJson)),
FacePoint.getLeftEyeRadius(faceJson) * 3, 3);
複製代碼
大眼效果,使用了bitmap直接去操做像素點,效率有點低,因此在實現瘦臉和打長腿時,採用了另外的實現方式實現.
Cavans的drawBitmapMesh方法
// 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);
}
複製代碼
這個方法,大概說的是,將圖片使用網格的方式先進行分割,而後操做這些網格,就可讓圖片達到扭曲的效果.
Gif中拖動就能夠進行自動瘦臉功能,這是一個自定義的View,在View上經過手勢操做,去改變那個網格,而後在調用重繪.
第一步,初始化圖片,把圖片放在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.0f;
// 圖片的寬度大於控件的寬度,圖片的高度小於空間的高度,咱們將其縮小
if (dw > width && dh < height) {
scale = width * 1.0f / dw;
}
// 圖片的寬度小於控件的寬度,圖片的高度大於空間的高度,咱們將其縮小
if (dh > height && dw < width) {
scale = height * 1.0f / dh;
}
// 縮小值
if (dw > width && dh > height) {
scale = Math.min(width * 1.0f / dw, height * 1.0f / dh);
}
// 放大值
if (dw < width && dh < height) {
scale = Math.min(width * 1.0f / dw, height * 1.0f / dh);
}
//縮小
if (dw == width && dh > height) {
scale = height * 1.0f / dh;
}
dx = width / 2 - (int) (dw * scale + 0.5f) / 2;
dy = height / 2 - (int) (dh * scale + 0.5f) / 2;
mScale = scale;
restoreVerts();
}
複製代碼
接着初始化網格
//將圖像分紅多少格
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 restoreVerts() {
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;
//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;
}
}
showCircle = false;
showDirection = false;
}
複製代碼
那最後一步把這個圖片畫上去
@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);
}
}
複製代碼
那麼接下來,就來操做網格,而後產生一些變形的效果了. 添加事件監聽
@Override
public boolean onTouchEvent(MotionEvent event) {
if (!isEnableOperate) return true;
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數組
if(mBitmap != null && verts!= null && !mBitmap.isRecycled()) {
warp(startX, startY, event.getX(), event.getY());
}
if (onStepChangeListener != null) {
onStepChangeListener.onStepChange(false);
}
break;
}
return true;
}
複製代碼
這裏重點,看咱們的wrap方法,來操做網格的變形.先簡述一下思路,咱們剛纔看到眼睛的放大,就是中心部分,操做幅度大,離的遠的地方基本不操做.
來看一下代碼
private void warp(float startX, float startY, float endX, float endY) {
startX = toX(startX);
startY = toY(startY);
endX = toX(endX);
endY = toY(endY);
//計算拖動距離
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.8f * r;
} else {
dPull = 2.5f * 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++) {
//邊界區域不處理
if(i < offset || i > HEIGHT - offset || j < offset || j > WIDTH - offset){
index = index + 1;
continue;
}
//計算每一個座標點與觸摸點之間的距離
float dx = verts[index * 2] - startX;
float dy = verts[index * 2 + 1] - startY;
float dd = dx * dx + dy * dy;
if (dd < powR) {
//變形係數,扭曲度
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();
}
複製代碼
只要在操做半徑內,對X和Y進行不一樣的變形便可.
其實有了上面的拖動,要實現自動瘦臉就容易得多,咱們對幾個關鍵點進行模擬拖動便可。
實現代碼以下
/** * 瘦臉算法 * * @param bitmap 原來的bitmap * @return 以後的圖片 */
public static Bitmap smallFaceMesh(Bitmap bitmap, List<Point> leftFacePoint,List<Point> rightFacePoint,Point centerPoint, int level) {
//交點座標的個數
int COUNT = (WIDTH + 1) * (HEIGHT + 1);
//用於保存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;
//X軸座標 放在偶數位
verts[index * 2] = fx;
//Y軸座標 放在奇數位
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;
}
複製代碼
看代碼有些累吧,下面來看一個明星 美女,有人知道這是誰嗎?問了兩三個程序員朋友,要麼不知道,要麼說這是楊冪嗎?哎,感嘆程序員認識的明星就那麼多嗎?
上面的瘦臉操做須要對x和y兩個地方進行操做,那大長腿就繪變得容易一些,僅僅操做Y方向便可.
第一張圖,上面的覆蓋層爲一個自定義View,下層直接使用了瘦臉功能的那個View,把圖片放在中心,只是不容許手勢操做圖片.
smallFaceView.setEnableOperate(false);
複製代碼
上層View核心代碼
//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);
}
}
}
複製代碼
手勢交互部分
//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) {
// 最小 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){
return offset;
}
}
if (selectPos == 2) {
if(bottomLine + offset > minLine && bottomLine + offset < maxLine){
return offset;
}
}
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;
}
複製代碼
那麼怎麼把腿部拉長呢?直接看一下算法部分
private static void warpLeg(int COUNT, float verts[], float centerY,int totalHeight,float region,float strength) {
float r = region / 2; //縮放區域力度
for (int i = 0; i < COUNT * 2; i += 2) {
//計算每一個座標點與觸摸點之間的距離
float dy = verts[i + 1] - centerY;
double e = (totalHeight - Math.abs(dy)) / totalHeight;
if(Math.abs(dy) < r){
//拉長比率
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;
複製代碼
依然使用的是drawBitmapMesh,算法部分,只對Y進行了操做,X部分不操做,而且距離越遠,操做幅度越小. 儘可能只拉長腿部,其餘部分保持原有不動.
本篇主要是介紹了,在Android上,使用原生API,怎麼去實現一些酷炫的效果. 文中的全部代碼都託管在github上,若是有須要,歡迎star, Github Makeup ,很是感謝,後續更新都會在此庫中進行.
本文大眼算法,廋臉算法僅來源網絡,若有侵權,請聯繫做者馬上刪除.大長腿算法,做者本身實踐得出,可自行取用.
Android:讓你的「女神」逆襲,代碼擼彩妝(畫妝)
Flutter PIP(畫中畫)效果的實現
Android 繪製原理淺析【乾貨】