作者:reggie1996
連結:https://www.jianshu.com/p/920b9c525d2f
前言
最近公司專案比較空,花了點時間寫了個人臉識別的app,可以檢視你的性別、年齡、顏值、情緒等資訊,利用的是 Face++ 的人臉識別API。本專案採用了 MVP 的架構,使用了 Retrofit、RxJava、Dagger、EventBus 等框架進行開發和解耦,利用 MaterialDesign 進行UI上的佈局設計。
主要的功能就是拍照,然後將照片傳至 Face++ 伺服器,進行人臉識別,獲取傳回的資訊,對資訊進行處理。將人臉在照片上標出,並將資訊展示出來。
話不多說,先來看一下 app 的效果(吳彥祖還是帥啊,哈哈)。
專案我已經放在 github 上,clone 下來即可編譯執行。github 地址: reggie1996 – FaceDetect 。下麵文章主要介紹的是本專案的開發過程和碰到的坑。
過程
專案的整個流程很簡單無非就是三步,拍照片,傳照片獲取資料,然後對資料進行處理展示。
拍照獲取照片
拍照需要獲取系統許可權,我封裝了一個方法,來判斷App是否有拍照相關的許可權,如果沒有就去動態請求許可權,並傳回 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;
}
}
獲取到拍照許可權後就可以拍照了,但是拍照得到的照片我們需要透過 FileProvider 獲取。FileProvider 相關的內容就不作介紹了,Android 7.0 之後都得用這個。
<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>
拍照之後從檔案中讀取照片,我們可以得到一個 BitMap 物件。這裡就有一個很大的坑,如果手機是三星的話,照片從檔案裡讀出來,最後得到的照片會被旋轉 90°!!!,這個賊坑啊,調了我好久,以為是自己手機的故障,後來網上查了一下,也請教了一下前輩,原來三星的手機都有這個問題,所以說我們要對檔案中取出來的照片進行一下處理。
/**
* 讀取圖片的旋轉的角度
*
* @param path 圖片絕對路徑
* @return 圖片的旋轉角度
*/
public static int getBitmapDegree(String path) {
int degree = 0;
try {
// 從指定路徑下讀取圖片,並獲取其EXIF資訊
ExifInterface exifInterface = new ExifInterface(path);
// 獲取圖片的旋轉資訊
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;
}
/**
* 將圖片按照某個角度進行旋轉
*
* @param bm 需要旋轉的圖片
* @param degree 旋轉角度
* @return 旋轉後的圖片
*/
public static Bitmap rotateBitmapByDegree(Bitmap bm, int degree) {
Bitmap returnBm = null;
// 根據旋轉角度,生成旋轉矩陣
Matrix matrix = new Matrix();
matrix.postRotate(degree);
try {
// 將原始圖片按照旋轉矩陣進行旋轉,並得到新的圖片
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;
}
封裝了兩個方法,依次呼叫可以解決三星手機照片的問題。兩個方法主要的工作就是,得到取出來的照片被旋轉的角度,然後再將角度旋轉回去,就可以得到原來的照片。因為並不是所有的手機在獲取照片時,照片都會被旋轉,所以得先判斷一下照片有沒有被旋轉,再決定是否需要將它旋轉調整。
行,這樣最後就獲得到了正確的 BitMap 照片,可以進行下一步了。
傳照片獲取資料
傳照片獲取資料,主要是運用了 Retrofit 和 RxJava 的封裝。請求的引數可以參考 Face++ 的官方檔案。
/**
* retrofit 面部識別請求的網路服務
* @author chaochaowu
*/
public interface FaceppService {
/**
* @param apikey
* @param apiSecret
* @param imageBase64
* @param returnLandmark
* @param returnAttributes
* @return
*/
@POST("facepp/v3/detect")
@FormUrlEncoded
Observable 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) ;
}
照片需要進行 base64 轉碼後上傳至伺服器,封裝了一個照片base64轉碼方法。
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);
}
處理完成之後就可以進行網路請求獲取資料。
@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() {
@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++ 伺服器會對我們上傳的照片進行處理,分析照片中的人臉資訊,並以 json 形式傳回,傳回的資料將被放入我們定義的bean類中。
/**
* 面部識別結果的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 faces;
...顯示部分內容
bean 類中有人臉識別得到的 性別、年齡、顏值、情緒等資訊,還有每張人臉在照片中的坐標位置。接下來的工作就是對這些資料進行處理。
獲取資訊後的資料處理
資料的處理主要就兩件事,一個是將資料以文字的形式展現,這個很簡單,就不介紹了,還有一個就是將人臉在照片中標示出來,這個需要對 BitMap 進行處理,利用資料中人臉在照片中的坐標位置,我們用方框將人臉標識出來。
private Bitmap markFacesInThePhoto(Bitmap bitmap, List 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;
}
封裝了一個方法,運用 Canvas 在照片上進行繪製,因為照片中的人臉可能不止一個,所以用for迴圈遍歷。獲取人臉在照片中的坐標,利用人臉左上角的坐標以及人臉的寬高,在照片中繪製一個方框將人臉標出。
剩餘資訊我這邊採用 RecyclerView 來展示。左右滑動可以檢視每張人臉的資訊。RecyclerView 的 item 上展示的是簡要資訊,可以點選 item 進入詳情頁面檢視面部識別的詳細資訊。RecyclerView 以及詳情介面的實現就不作介紹了,很基本的操作。我這邊也就只使用了 SharedElement 讓介面切換看起來舒服一點。具體的實現可以看 github 上的程式碼。
其他就沒什麼操作了,還可以看一下我的專案架構。由於用了各種框架進行解耦,所以程式碼檔案數量變多了,但是單個檔案中的程式碼會變少一點,清晰易讀一點,這也是解耦的目的,也方便之後的維護。
具體實現的細節可以看 github 上面的程式碼~
最後
寫完這個APP後,我一直在思考一個問題,APP給吳彥祖的顏值打分80多,那100分的顏值會是怎樣?
感興趣的朋友可以把程式碼下載下來玩一下,測一下自己或者是朋友的顏值,嘿嘿。github 地址:https://github.com/reggie1996/FaceDetect
最後祝大家生活愉快~
●編號358,輸入編號直達本文
●輸入m獲取到文章目錄
Java程式設計
更多推薦《18個技術類公眾微信》
涵蓋:程式人生、演演算法與資料結構、駭客技術與網路安全、大資料技術、前端開發、Java、Python、Web開發、安卓開發、iOS開發、C/C++、.NET、Linux、資料庫、運維等。