今天是2018年第一天,首先真诚地祝福各位同行朋友元旦快乐,在未来开发之路不断进步,为促进公司发展壮大、为提升人们生活品质以实现自我价值。言归正传,继续探讨Android移动端的图片处理,使用NDK实现人脸抠图。天天P图、美图秀秀们都具备P图技能,让女孩秒变网红脸、明星脸,这技能令人爱不释手,简直是女神打印机。那么到底如何实现这变脸大法呢?经过自己研究,总结出三个步骤:人脸检测—>人脸抠图—>人脸替换。
1、人脸检测
利用Android系统自带人脸检测API,只需要创建FaceDetector,传入图片宽度、高度以及检测人脸最大数这三个参数:
faceDetector = new FaceDetector(mFace.getWidth(), mFace.getHeight(), N_MAX);
然后开始人脸检测,传入图片以及人脸数组:
int nFace = faceDetector.findFaces(mFace, face);
2、人脸抠图
根据人脸检测结果,得到人脸的眼坐标与中间坐标,计算出人脸矩形:left、top、right、bottom,最后根据这四个坐标点进行人脸抠图。先看下Java版本的方法:
public int[] extractFaceByJava(Bitmap bitmap, int left, int top, int right, int bottom){
int width = bitmap.getWidth();
int height = bitmap.getHeight();
int mWidth = right-left;
int mHeight = bottom-top;
int[] pixels = new int[width*height];
int[] mPixels = new int[mWidth*mHeight];
//获取bitmap的所有pixel像素点
bitmap.getPixels(pixels, 0, width, 0, 0, width, height);
//获取ROI(区域遍历)
for(int x=left; x<right; x++){
for(int y=top; y<bottom; y++){
mPixels[(y-top)*mWidth + (x-left)] = pixels[y*width + x];
}
}
return mPixels;
}
下面是NDK版本的方法,供Java层调用:
jintArray
Java_com_frank_image_ImageUtil_extractFace(JNIEnv *env, jobject, jobject bitmap, jint left, jint top, jint right, jint bottom){
AndroidBitmapInfo bitmapInfo;
int *pixelColor;
int x, y;
int mWidth = right-left;
int mHeight = bottom-top;
jint *mPixels = new jint[mWidth*mHeight];
if (AndroidBitmap_getInfo(env, bitmap, &bitmapInfo) < 0){
LOGI("AndroidBitmap_getInfo error...");
return NULL;
}
if(AndroidBitmap_lockPixels(env, bitmap, (void **) &pixelColor) < 0){
LOGI("AndroidBitmap_lockPixels error...");
return NULL;
}
//获取ROI(区域遍历)
for(y=top; y<bottom; y++){
for(x=left; x<right; x++){
mPixels[(y-top)*mWidth + (x-left)] = *(pixelColor + y*bitmapInfo.width + x);
}
}
AndroidBitmap_unlockPixels(env, bitmap);
LOGI("extractFace has done...");
jintArray newPixel = env->NewIntArray(mWidth*mHeight);
env->SetIntArrayRegion(newPixel, 0, mWidth*mHeight, mPixels);
return newPixel;
}
另外,在研究openCV时,发现它提供一个封装好的方法,可以很轻松获取ROI区域:
/**
*利用openCV的ROI实现人脸抠图(需要导入openCV库)
*/
public Bitmap extractFaceByOpenCV(Mat src, int left, int top, int right, int bottom){
//创建ROI矩形区域
Rect ROI = new Rect(left, top, right-left, bottom-top);
//根据原图得到ROI区域
Mat roiMat = new Mat(src, ROI);
Bitmap bitmap = Bitmap.createBitmap(src.width(), src.height(), Bitmap.Config.ARGB_8888);
//Mat转成Bitmap
Utils.matToBitmap(roiMat, bitmap);
return bitmap;
}
聊了这么多,中场暂停休息下,看看人脸抠图效果:
3、人脸替换
其实人脸替换需要重复第一步,进行人脸检测,然后把目标人脸替换原本人脸。整个过程如下:
/**
* 人脸替换
* @return Bitmap
*/
public Bitmap doExchangeFace(Bitmap src, Bitmap faceBitmap){
if(faceBitmap == null){
return src;
}
int nFace = faceDetector.findFaces(src, face);
if(nFace > 0){
Face f = face[0];
PointF midPoint = new PointF();
float dis = f.eyesDistance();
f.getMidPoint(midPoint);
int dd = (int)(dis);
int width = dd * 2;
int height = dd * 2 + compensation;
faceBitmap = Bitmap.createScaledBitmap(faceBitmap, width, height, true);
return extractFaceHelper.exchangeFace(src, faceBitmap, (int)(midPoint.x - dd), (int)(midPoint.y - dd),
(int)(midPoint.x + dd), (int)(midPoint.y + dd) + compensation);
}
return src;
}
上面调用的NDK的实现方法:
jobject
Java_com_frank_image_ImageUtil_exchangeFace(JNIEnv *env, jobject, jobject src, jobject face,
jint left, jint top, jint right, jint bottom){
AndroidBitmapInfo srcInfo, faceInfo;
int *srcColor, *faceColor;
int x, y;
if (AndroidBitmap_getInfo(env, src, &srcInfo) < 0
|| AndroidBitmap_getInfo(env, face, &faceInfo) < 0){
LOGI("AndroidBitmap_getInfo error...");
return NULL;
}
if(AndroidBitmap_lockPixels(env, src, (void **) &srcColor) < 0
|| AndroidBitmap_lockPixels(env, face, (void **) &faceColor) < 0){
LOGI("AndroidBitmap_lockPixels error...");
return NULL;
}
for(y=top; y<bottom; y++){
for(x=left; x<right; x++){
*(srcColor + y*srcInfo.width + x) = *(faceColor + (y-top)*faceInfo.width + (x-left));
}
}
AndroidBitmap_unlockPixels(env, src);
AndroidBitmap_unlockPixels(env, face);
LOGI("extractFace has done...");
return src;
}
由此可见,JNI可以获取Bitmap地址,直接操作Bitmap像素数据,节省内存开销,并且效率比Java方法相对高一点。
人脸替换的效果并不是很理想,因为每个人脸大小不一致,需要根据目标人脸进行压缩,而且这里抠图是矩形人脸,没有无缝替换原本人脸。大家别介意,在这里只为分享实现思路。