前言
??最近公司項(xiàng)目比較空,花了點(diǎn)時(shí)間寫(xiě)了個(gè)人臉識(shí)別的app,可以查看你的性別、年齡、顏值、情緒等信息,利用的是 Face++ 的人臉識(shí)別API。本項(xiàng)目采用了 MVP 的架構(gòu),使用了 Retrofit、RxJava、Dagger、EventBus 等框架進(jìn)行開(kāi)發(fā)和解耦,利用 MaterialDesign 進(jìn)行UI上的布局設(shè)計(jì)。
??主要的功能就是拍照,然后將照片傳至 Face++ 服務(wù)器,進(jìn)行人臉識(shí)別,獲取返回的信息,對(duì)信息進(jìn)行處理。將人臉在照片上標(biāo)出,并將信息展示出來(lái)。
??話不多說(shuō),先來(lái)看一下 app 的效果(吳彥祖還是帥啊,哈哈)。



??項(xiàng)目我已經(jīng)放在 github 上,clone 下來(lái)即可編譯運(yùn)行。github 地址: reggie1996 - FaceDetect 。下面文章主要介紹的是本項(xiàng)目的開(kāi)發(fā)過(guò)程和碰到的坑。
過(guò)程
??項(xiàng)目的整個(gè)流程很簡(jiǎn)單無(wú)非就是三步,拍照片,傳照片獲取數(shù)據(jù),然后對(duì)數(shù)據(jù)進(jìn)行處理展示。
拍照獲取照片
??拍照需要獲取系統(tǒng)權(quán)限,我封裝了一個(gè)方法,來(lái)判斷App是否有拍照相關(guān)的權(quán)限,如果沒(méi)有就去動(dòng)態(tài)請(qǐng)求權(quán)限,并返回 false,如果有就返回 true。
public static boolean checkAndRequestPermission(Context context, int requestCode) {
if (context.checkSelfPermission( Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED
|| context.checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED
|| context.checkSelfPermission(Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
((Activity) context).requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.CAMERA}, requestCode);
return false;
}else {
return true;
}
}
??獲取到拍照權(quán)限后就可以拍照了,但是拍照得到的照片我們需要通過(guò) FileProvider 獲取。FileProvider 相關(guān)的內(nèi)容就不作介紹了,Android 7.0 之后都得用這個(gè)。
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="com.chaochaowu.facedetect.provider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
??拍照之后從文件中讀取照片,我們可以得到一個(gè) BitMap 對(duì)象。這里就有一個(gè)很大的坑,如果手機(jī)是三星的話,照片從文件里讀出來(lái),最后得到的照片會(huì)被旋轉(zhuǎn) 90°?。?!,這個(gè)賊坑啊,調(diào)了我好久,以為是自己手機(jī)的故障,后來(lái)網(wǎng)上查了一下,也請(qǐng)教了一下前輩,原來(lái)三星的手機(jī)都有這個(gè)問(wèn)題,所以說(shuō)我們要對(duì)文件中取出來(lái)的照片進(jìn)行一下處理。
/**
* 讀取圖片的旋轉(zhuǎn)的角度
*
* @param path 圖片絕對(duì)路徑
* @return 圖片的旋轉(zhuǎn)角度
*/
public static int getBitmapDegree(String path) {
int degree = 0;
try {
// 從指定路徑下讀取圖片,并獲取其EXIF信息
ExifInterface exifInterface = new ExifInterface(path);
// 獲取圖片的旋轉(zhuǎn)信息
int orientation = exifInterface.getAttributeInt(ExifInterface.TAG_ORIENTATION,
ExifInterface.ORIENTATION_NORMAL);
switch (orientation) {
case ExifInterface.ORIENTATION_ROTATE_90:
degree = 90;
break;
case ExifInterface.ORIENTATION_ROTATE_180:
degree = 180;
break;
case ExifInterface.ORIENTATION_ROTATE_270:
degree = 270;
break;
default:
degree = 0;
break;
}
} catch (IOException e) {
e.printStackTrace();
}
return degree;
}
/**
* 將圖片按照某個(gè)角度進(jìn)行旋轉(zhuǎn)
*
* @param bm 需要旋轉(zhuǎn)的圖片
* @param degree 旋轉(zhuǎn)角度
* @return 旋轉(zhuǎn)后的圖片
*/
public static Bitmap rotateBitmapByDegree(Bitmap bm, int degree) {
Bitmap returnBm = null;
// 根據(jù)旋轉(zhuǎn)角度,生成旋轉(zhuǎn)矩陣
Matrix matrix = new Matrix();
matrix.postRotate(degree);
try {
// 將原始圖片按照旋轉(zhuǎn)矩陣進(jìn)行旋轉(zhuǎn),并得到新的圖片
returnBm = Bitmap.createBitmap(bm, 0, 0, bm.getWidth(), bm.getHeight(), matrix, true);
} catch (OutOfMemoryError | Exception e) {
e.printStackTrace();
}
if (returnBm == null) {
returnBm = bm;
}
if (bm != returnBm) {
bm.recycle();
}
return returnBm;
}
??封裝了兩個(gè)方法,依次調(diào)用可以解決三星手機(jī)照片的問(wèn)題。兩個(gè)方法主要的工作就是,得到取出來(lái)的照片被旋轉(zhuǎn)的角度,然后再將角度旋轉(zhuǎn)回去,就可以得到原來(lái)的照片。因?yàn)椴⒉皇撬械氖謾C(jī)在獲取照片時(shí),照片都會(huì)被旋轉(zhuǎn),所以得先判斷一下照片有沒(méi)有被旋轉(zhuǎn),再?zèng)Q定是否需要將它旋轉(zhuǎn)調(diào)整。
??行,這樣最后就獲得到了正確的 BitMap 照片,可以進(jìn)行下一步了。
傳照片獲取數(shù)據(jù)
??傳照片獲取數(shù)據(jù),主要是運(yùn)用了 Retrofit 和 RxJava 的封裝。請(qǐng)求的參數(shù)可以參考 Face++ 的官方文檔。
/**
* retrofit 面部識(shí)別請(qǐng)求的網(wǎng)絡(luò)服務(wù)
* @author chaochaowu
*/
public interface FaceppService {
/**
* @param apikey
* @param apiSecret
* @param imageBase64
* @param returnLandmark
* @param returnAttributes
* @return
*/
@POST("facepp/v3/detect")
@FormUrlEncoded
Observable<FaceppBean> getFaceInfo(@Field("api_key") String apikey,
@Field("api_secret") String apiSecret,
@Field("image_base64") String imageBase64,
@Field("return_landmark") int returnLandmark,
@Field("return_attributes") String returnAttributes);
}
??照片需要進(jìn)行 base64 轉(zhuǎn)碼后上傳至服務(wù)器,封裝了一個(gè)照片base64轉(zhuǎn)碼方法。
public static String base64(Bitmap bitmap){
ByteArrayOutputStream baos = new ByteArrayOutputStream();
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, baos);
byte[] bytes = baos.toByteArray();
return Base64.encodeToString(bytes, Base64.DEFAULT);
}
??處理完成之后就可以進(jìn)行網(wǎng)絡(luò)請(qǐng)求獲取數(shù)據(jù)。
@Override
public void getDetectResultFromServer(final Bitmap photo) {
String s = Utils.base64(photo);
faceppService.getFaceInfo(BuildConfig.API_KEY, BuildConfig.API_SECRET, s, 1, "gender,age,smiling,emotion,beauty")
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Observer<FaceppBean>() {
@Override
public void onSubscribe(Disposable d) {
mView.showProgress();
}
@Override
public void onNext(FaceppBean faceppBean) {
handleDetectResult(photo,faceppBean);
}
@Override
public void onError(Throwable e) {
mView.hideProgress();
}
@Override
public void onComplete() {
mView.hideProgress();
}
});
}
??Face++ 服務(wù)器會(huì)對(duì)我們上傳的照片進(jìn)行處理,分析照片中的人臉信息,并以 json 形式返回,返回的數(shù)據(jù)將被放入我們定義的bean類(lèi)中。
/**
* 面部識(shí)別結(jié)果的bean
* @author chaochaowu
*/
public class FaceppBean {
/**
* image_id : Dd2xUw9S/7yjr0oDHHSL/Q==
* request_id : 1470472868,dacf2ff1-ea45-4842-9c07-6e8418cea78b
* time_used : 752
* faces : [{"landmark":{"mouth_upper_lip_left_contour2":{"y":185,"x":146},"contour_chin":{"y":231,"x":137},"right_eye_pupil":{"y":146,"x":205},"mouth_upper_lip_bottom":{"y":195,"x":159}},"attributes":{"gender":{"value":"Female"},"age":{"value":21},"glass":{"value":"None"},"headpose":{"yaw_angle":-26.625063,"pitch_angle":12.921974,"roll_angle":22.814377},"smile":{"threshold":30.1,"value":2.566890001296997}},"face_rectangle":{"width":140,"top":89,"left":104,"height":141},"face_token":"ed319e807e039ae669a4d1af0922a0c8"}]
*/
private String image_id;
private String request_id;
private int time_used;
private List<FacesBean> faces;
...顯示部分內(nèi)容
??bean 類(lèi)中有人臉識(shí)別得到的 性別、年齡、顏值、情緒等信息,還有每張人臉在照片中的坐標(biāo)位置。接下來(lái)的工作就是對(duì)這些數(shù)據(jù)進(jìn)行處理。
獲取信息后的數(shù)據(jù)處理
??數(shù)據(jù)的處理主要就兩件事,一個(gè)是將數(shù)據(jù)以文字的形式展現(xiàn),這個(gè)很簡(jiǎn)單,就不介紹了,還有一個(gè)就是將人臉在照片中標(biāo)示出來(lái),這個(gè)需要對(duì) BitMap 進(jìn)行處理,利用數(shù)據(jù)中人臉在照片中的坐標(biāo)位置,我們用方框?qū)⑷四槝?biāo)識(shí)出來(lái)。
private Bitmap markFacesInThePhoto(Bitmap bitmap, List<FaceppBean.FacesBean> faces) {
Bitmap tempBitmap = bitmap.copy(Bitmap.Config.ARGB_8888, true);
Canvas canvas = new Canvas(tempBitmap);
Paint paint = new Paint();
paint.setColor(Color.RED);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(10);
for (FaceppBean.FacesBean face : faces) {
FaceppBean.FacesBean.FaceRectangleBean faceRectangle = face.getFace_rectangle();
int top = faceRectangle.getTop();
int left = faceRectangle.getLeft();
int height = faceRectangle.getHeight();
int width = faceRectangle.getWidth();
canvas.drawRect(left, top, left + width, top + height, paint);
}
return tempBitmap;
}
??封裝了一個(gè)方法,運(yùn)用 Canvas 在照片上進(jìn)行繪制,因?yàn)檎掌械娜四樋赡懿恢挂粋€(gè),所以用for循環(huán)遍歷。獲取人臉在照片中的坐標(biāo),利用人臉左上角的坐標(biāo)以及人臉的寬高,在照片中繪制一個(gè)方框?qū)⑷四槝?biāo)出。

??剩余信息我這邊采用 RecyclerView 來(lái)展示。左右滑動(dòng)可以查看每張人臉的信息。RecyclerView 的 item 上展示的是簡(jiǎn)要信息,可以點(diǎn)擊 item 進(jìn)入詳情頁(yè)面查看面部識(shí)別的詳細(xì)信息。RecyclerView 以及詳情界面的實(shí)現(xiàn)就不作介紹了,很基本的操作。我這邊也就只使用了 SharedElement 讓界面切換看起來(lái)舒服一點(diǎn)。具體的實(shí)現(xiàn)可以看 github 上的代碼。

??其他就沒(méi)什么操作了,還可以看一下我的項(xiàng)目架構(gòu)。由于用了各種框架進(jìn)行解耦,所以代碼文件數(shù)量變多了,但是單個(gè)文件中的代碼會(huì)變少一點(diǎn),清晰易讀一點(diǎn),這也是解耦的目的,也方便之后的維護(hù)。

??具體實(shí)現(xiàn)的細(xì)節(jié)可以看 github 上面的代碼~
最后
??寫(xiě)完這個(gè)APP后,我一直在思考一個(gè)問(wèn)題,APP給吳彥祖的顏值打分80多,那100分的顏值會(huì)是怎樣?
??感興趣的朋友可以把代碼下載下來(lái)玩一下,測(cè)一下自己或者是朋友的顏值,嘿嘿。github 地址: reggie1996 - FaceDetect
??最后祝大家生活愉快~
