Android人臉識(shí)別app——基于Face++,MVP+Retofit+RxJava+Dagger高度解耦

前言

??最近公司項(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 的效果(吳彥祖還是帥啊,哈哈)。

面部識(shí)別主界面

面部識(shí)別詳情界面

多人臉識(shí)別

??項(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
??最后祝大家生活愉快~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 1、通過(guò)CocoaPods安裝項(xiàng)目名稱(chēng)項(xiàng)目信息 AFNetworking網(wǎng)絡(luò)請(qǐng)求組件 FMDB本地?cái)?shù)據(jù)庫(kù)組件 SD...
    陽(yáng)明AI閱讀 16,211評(píng)論 3 119
  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 179,179評(píng)論 25 708
  • 本篇文章十分的長(zhǎng),大概有2萬(wàn)7千字左右。 一、發(fā)展史 1、人臉識(shí)別的理解: 人臉識(shí)別(Face Recogniti...
    放飛人夜閱讀 20,484評(píng)論 8 122
  • 最近有小伙伴在群里問(wèn)我,“上班族的朋友們是怎么能抽出時(shí)間來(lái)畫(huà)畫(huà)的? ” 對(duì)于這個(gè)問(wèn)題,我自認(rèn)為最有發(fā)言權(quán)。因?yàn)椋?..
    寧博Villa閱讀 329評(píng)論 3 3
  • 感冒了,發(fā)燒了,流清鼻涕,渾身難受,眼睛腫脹,渾身酸軟無(wú)力,躺下不舒服,站起來(lái)也不舒服,一杯子水,一杯子水的喝,好...
    7754e1920aca閱讀 182評(píng)論 0 0

友情鏈接更多精彩內(nèi)容