首頁>技術>

前言

最近公司專案比較空,花了點時間寫了個人臉識別的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<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);}

照片需要進行 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<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++ 伺服器會對我們上傳的照片進行處理,分析照片中的人臉資訊,並以 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<FacesBean> faces;    ...顯示部分內容

bean 類中有人臉識別得到的 性別、年齡、顏值、情緒等資訊,還有每張人臉在照片中的座標位置。接下來的工作就是對這些資料進行處理。

獲取資訊後的資料處理資料的處理主要就兩件事,一個是將資料以文字的形式展現,這個很簡單,就不介紹了,還有一個就是將人臉在照片中標示出來,這個需要對 BitMap 進行處理,利用資料中人臉在照片中的座標位置,我們用方框將人臉標識出來。

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;    }

封裝了一個方法,運用 Canvas 在照片上進行繪製,因為照片中的人臉可能不止一個,所以用for迴圈遍歷。獲取人臉在照片中的座標,利用人臉左上角的座標以及人臉的寬高,在照片中繪製一個方框將人臉標出。

剩餘資訊我這邊採用 RecyclerView 來展示。左右滑動可以檢視每張人臉的資訊。RecyclerView 的 item 上展示的是簡要資訊,可以點選 item 進入詳情頁面檢視面部識別的詳細資訊。RecyclerView 以及詳情介面的實現就不作介紹了,很基本的操作。我這邊也就只使用了 SharedElement 讓介面切換看起來舒服一點。具體的實現可以看 github 上的程式碼。

其他就沒什麼操作了,還可以看一下我的專案架構。由於用了各種框架進行解耦,所以程式碼檔案數量變多了,但是單個檔案中的程式碼會變少一點,清晰易讀一點,這也是解耦的目的,也方便之後的維護。

注:具體實現的細節可以看 github 上面的程式碼~

最後

寫完這個APP後,我一直在思考一個問題,APP給吳彥祖的顏值打分80多,那100分的顏值會是怎樣?

最後祝大家生活愉快~

  • BSA-TRITC(10mg/ml) TRITC-BSA 牛血清白蛋白改性標記羅丹明
  • .NET Core技術研究-中介軟體的由來和使用