Android:圖片的縮放趋厉,平移和人臉識(shí)別

圖片的縮放,平移這些需求還是挺常見的君账,我通過自定義ImageView實(shí)現(xiàn)縮放和平移沈善,結(jié)合系統(tǒng)提供API實(shí)現(xiàn)人臉的識(shí)別

  • 代碼稍微有點(diǎn)多乡数,首先上演示效果:
圖片的縮放闻牡,平移,人臉識(shí)別效果.gif
  • 縮放和平移其實(shí)也就是調(diào)用ImageView的setImageMatrix方法便可完成罩润,通過計(jì)算移動(dòng)的距離(tx,ty)設(shè)置Matrix.postTranslate(tx割以,ty)金度,兩個(gè)手指新距離和按下距離的比值scale以按下時(shí)兩指的中點(diǎn)設(shè)置Matrix.postScale(scale严沥,x猜极,y)消玄,再將Matrix設(shè)置為ImageView即可
  • 人臉識(shí)別通過系統(tǒng)提供的FaceDetector便可實(shí)現(xiàn)丢胚,雖然不是很準(zhǔn)確,但是目前也只有能達(dá)到這種結(jié)果受扳,這個(gè)類使用的bitmap有兩個(gè)要求,第一個(gè)是必須使用Bitmap.Config.RGB_565格式加載勘高,第二個(gè)是bitmap的寬必須為偶數(shù)

下面來看具體代碼

首先是人臉識(shí)別,識(shí)別的結(jié)果為一個(gè)Face數(shù)組相满,當(dāng)然所有信息例如眼睛的位置,距離等均包含在Face類中立美,具體可以查看源碼
public class MainActivity extends Activity {

    private MyImageView mMyImageView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initView();
        initPhoto();
    }

    private void initView() {
        mMyImageView = (MyImageView) findViewById(R.id.main_image);
    }

    private void initPhoto() {
        String picPath = "/storage/emulated/0/Tencent/QQ_Images/-663c6adb1540c36f.jpg";
        Bitmap srcBitmap = BitmapUtils.loadBitmap(picPath, 1000, 1000, true);
        if (srcBitmap == null) {
            Toast.makeText(getApplicationContext(), "處理圖片失敗", Toast.LENGTH_SHORT).show();
        } else {
            // 圖片的寬必須為偶數(shù),不然系統(tǒng)無法進(jìn)行人臉識(shí)別
            if (srcBitmap.getWidth() % 2 != 0) {
                srcBitmap = Bitmap.createBitmap(srcBitmap, 0, 0, srcBitmap.getWidth() - 1
                        , srcBitmap.getHeight());
            }
            // 最多的人臉數(shù)
            int maxCount = 50;
            FaceDetector.Face[] faces = new FaceDetector.Face[maxCount];
            FaceDetector faceDetector = new FaceDetector(srcBitmap.getWidth(), srcBitmap.getHeight(), maxCount);
            // 這一步比較耗時(shí)間建蹄,大概一秒左右,跟bitmap的大小有關(guān)(1000左右最佳洞慎,識(shí)別結(jié)果準(zhǔn)確并且時(shí)間較少)痛单,建議使用EventBus異步處理
            int faceCount = faceDetector.findFaces(srcBitmap, faces);
            PointF pointF = new PointF();
            // 過濾原本就不完整的臉
            for (int i = 0; i < faceCount; i++) {
                float eyesDistance = faces[i].eyesDistance();
                faces[i].getMidPoint(pointF);
                if (pointF.x < eyesDistance                                              // 左邊超出
                        || pointF.y < eyesDistance * 2f                                // 上邊超出
                        || srcBitmap.getWidth() - pointF.x < eyesDistance                // 右邊超出
                        || srcBitmap.getHeight() - pointF.y < eyesDistance * 2f) {     // 下邊超出
                    faces[i] = null;
                }
            }
            // 必須設(shè)置LayoutParams劲腿,這樣在自定義ImageView中使用getLayoutParams才能得到正確的params
            FrameLayout.LayoutParams photoParams = new FrameLayout.LayoutParams(mMyImageView.getLayoutParams());
            photoParams.gravity = Gravity.CENTER;
            photoParams.width = 1000;
            photoParams.height = 1000;
            mMyImageView.setLayoutParams(photoParams);
            mMyImageView.setImageBitmap(srcBitmap, faces, 1f);
        }
    }

}
  • 其中的BitmapUtils類為提供根據(jù)圖片本地路徑picPath加載bitmap的方法,具體實(shí)現(xiàn)可以通過GitHub上的項(xiàng)目查看
  • 這里臉部范圍的定義是:以兩只眼睛的中點(diǎn)為重心焦人,寬為兩倍的眼睛之間的距離,高為4倍的眼睛之間的距離

接下來是自定義的ImageView花椭,也是整個(gè)demo的重點(diǎn)
  • 這里著重解釋一些mIsVerticalFit這個(gè)變量忽匈,這是根據(jù)圖片的寬高和ImageView的寬高做FitCenter得到的值矿辽,F(xiàn)itCenter的意思就是將圖片剛好完整的居中顯示在ImageView中丹允,這里邊涉及到水平Fit和豎直Fit袋倔,這個(gè)值便是判斷是否為豎直Fit的變量,詳細(xì)的FitCenter和CenterCrop信息可以自行百度~
    @Override
    public void setImageDrawable(Drawable drawable) {
        mLastX = mLastY = 0;
        mIntrinsicWidth = drawable.getIntrinsicWidth();
        mIntrinsicHeight = drawable.getIntrinsicHeight();
        centerCropImage();
        mTempRectF = getMatrixRectF();
        // 判斷圖片的寬高view的寬高FitCenter狀態(tài)是豎直fit還是水平fit奕污,如果不能理解可以查看IamgeView源碼中的CenterCrop
        // 和FitCenter這兩個(gè)屬性萎羔,其實(shí)這里是照搬源碼
        mIsVerticalFit = mIntrinsicWidth * getLayoutParams().height < getLayoutParams().width * mIntrinsicHeight;
        checkFace();
        super.setImageDrawable(drawable);
    }
  • 我們主要來看一下單指和雙指移動(dòng)過程中的代碼碳默,?這里設(shè)置放大倍數(shù)為View寬高的3倍缘眶,如果是豎直Fit就以寬來計(jì)算,否則以高來計(jì)算
  • 如果是放大髓废,并且放大倍數(shù)已經(jīng)超過3倍了,便設(shè)置mResetScale以供手指放開時(shí)恢復(fù)放大的極限慌洪,縮小同理,最多能縮小到FitCenter冈爹,具體該使用寬還是高做判斷取決于mIsVerticalFit
 case MotionEvent.ACTION_MOVE: {
                if (mTouchMode == COUPLE_OPERATION) {
                    mTempMatrix.set(mSavedMatrix);
                    float newDist = spacing(ev);
                    float scale = newDist / mLastDist;
                    mTempMatrix.postScale(scale, scale, mMidPoint.x, mMidPoint.y);
                    mDrawMatrix.set(mTempMatrix);
                    this.setImageMatrix(mDrawMatrix);
                    // 縮放到極限時(shí)設(shè)置恢復(fù)scale
                    boolean isOut = mIsVerticalFit
                            ? getMatrixRectF().width() / getLayoutParams().width > THE_MAX_SCALE
                            : getMatrixRectF().height() / getLayoutParams().height > THE_MAX_SCALE;
                    if (scale >= 1 && isOut) {
                        if (mIsVerticalFit) {
                            // 豎直fit取寬
                            mResetScale = THE_MAX_SCALE * getLayoutParams().width / getMatrixRectF().width();
                        } else {
                            // 水平fit取高
                            mResetScale = THE_MAX_SCALE * getLayoutParams().height / getMatrixRectF().height();
                        }
                    } else if (scale <= 1 && (int) getMatrixRectF().width() <= getLayoutParams().width
                            && (int) getMatrixRectF().height() <= getLayoutParams().height) {
                        // 保證當(dāng)圖片全部縮小在顯示范圍內(nèi)便不能再縮小
                        if (mIsVerticalFit) {
                            // 豎直取高
                            mResetScale = getLayoutParams().height / getMatrixRectF().height();
                        } else {
                            // 水平取寬
                            mResetScale = getLayoutParams().width / getMatrixRectF().width();
                        }
                    } else {
                        mResetScale = 0;
                    }
                } else if (mTouchMode == SINGLE_OPERATION) {
                    mTempMatrix.set(mSavedMatrix);
                    float tx = ev.getX() - mLastX;
                    float ty = ev.getY() - mLastY;
                    mIsMove = true;
                    RectF rectF = getMatrixRectF();
                    mIsCheckRAndL = (int) rectF.width() <= getLayoutParams().width;
                    mIsCheckBAndT = (int) rectF.height() <= getLayoutParams().height;
                    mTempMatrix.postTranslate(tx, ty);
                    mDrawMatrix.set(mTempMatrix);
                    this.setImageMatrix(mDrawMatrix);
                }
                checkFace();
                break;
            }
  • 當(dāng)手指抬起來時(shí)同時(shí)進(jìn)行邊界回彈檢查和縮放倍數(shù)檢查,如果超過縮放極限便根據(jù)mResetScale將圖片縮放至極限大小频伤,具體代碼如下:
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_POINTER_UP:
                if (mTouchMode == COUPLE_OPERATION) {
                    if (mResetScale != 0) {
                        mTempMatrix.postScale(mResetScale, mResetScale, mMidPoint.x, mMidPoint.y);
                        mResetScale = 0;
                    }
                    mDrawMatrix.set(mTempMatrix);
                    center(true, true);
                    this.setImageMatrix(mDrawMatrix);
                    checkFace();
                    invalidate();
                    if (getMatrixRectF().left > 0 || getMatrixRectF().top > 0) {
                        mTempRectF = getMatrixRectF();
                    }
                } else if (mTouchMode == SINGLE_OPERATION && mIsMove) {
                    float[] dxDyBounds = checkDxDyBounds();
                    if (dxDyBounds == null) {
                        mTempMatrix.postTranslate(0, 0);
                        mDrawMatrix.set(mTempMatrix);
                        this.setImageMatrix(mDrawMatrix);
                        checkFace();
                    } else {
                        setAnimation(dxDyBounds);
                    }
                }
                mIsMove = false;
                mTouchMode = NONE_OPERATION;
                break;
  • 其中由于系統(tǒng)自帶的回彈動(dòng)畫太快,Matrix.postTranslate()方法不可以傳遞動(dòng)作時(shí)間因痛,因此使用ValueAnimator將移動(dòng)的操作拆分按傳入時(shí)間完成,以達(dá)到動(dòng)畫效果
/**
     * 設(shè)置邊界回彈的動(dòng)畫
     *
     * @param bounds 需要回彈的偏移量
     */
    private void setAnimation(float[] bounds) {
        setEnabled(false);
        final float floatDx = bounds[0];
        final float floatDy = bounds[1];
        ValueAnimator animator = new ValueAnimator();
        animator.setInterpolator(new DecelerateInterpolator());
        animator.setFloatValues(0, 1);
        animator.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                setEnabled(true);
                checkFace();
            }
        });
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            float lastDx = 0;
            float lastDy = 0;

            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                float offsetX = floatDx * animation.getAnimatedFraction() - lastDx;
                float offsetY = floatDy * animation.getAnimatedFraction() - lastDy;
                lastDx += offsetX;
                lastDy += offsetY;
                mTempMatrix.postTranslate(offsetX, offsetY);
                mDrawMatrix.set(mTempMatrix);
                MyImageView.this.setImageMatrix(mDrawMatrix);
            }
        });
        animator.setDuration(BORDER_BACK_DURATION).start();
    }
檢測(cè)移動(dòng)和縮放過程中是否有人臉超出顯示范圍鸵膏,將超出范圍的人臉用藍(lán)色方框標(biāo)注出來
  • 根據(jù)人臉范圍(眼睛中點(diǎn)為重心,寬為兩倍眼睛之間的距離谭企,高為四倍)到自定義ImageView邊界的距離判斷是否超過顯示范圍,這里的坐標(biāo)均為相對(duì)坐標(biāo)结胀,即ImageView的左上角即為原點(diǎn)(絕對(duì)坐標(biāo)指的是屏幕左上角為原點(diǎn))
    /**
     * 檢查是否有人臉超出顯示范圍
     */
    private void checkFace() {
        for (int i = 0; i < mFaceCount; i++) {
            if (mFaces[i] == null) continue;
            mFaces[i].getMidPoint(mPoint);
            RectF rectF = new RectF(0, 0, mPoint.x * mAdjustScale, mPoint.y * mAdjustScale);
            mDrawMatrix.mapRect(rectF);
            float eyesDistance = mFaces[i].eyesDistance() * mAdjustScale * getMatrixRectF().width() / mIntrinsicWidth;
            float distanceH = rectF.left + rectF.width();
            float distanceV = rectF.top + rectF.height();
            mIsNeedDraws[i] = distanceH > getLayoutParams().width - eyesDistance                    // 右邊超出
                    || distanceH < eyesDistance                                                     // 左邊超出
                    || distanceV > getLayoutParams().height - eyesDistance * FACE_VERTICAL          // 下邊超出
                    || distanceV < eyesDistance * FACE_VERTICAL;                                    // 上邊超出
        }
    }
  • 繪制每個(gè)需要繪制的矩形,根據(jù)mIsNeedDraws這個(gè)數(shù)組判斷糟港,如果mIsNeedDraws[i]為true證明這個(gè)矩形需要繪制院仿,否則continue
    @Override
    public void onDraw(Canvas canvas) {
        final Drawable drawable = getDrawable();
        if (drawable != null) {
            canvas.save();                                          // 保存畫布秸抚,接下來的操作在新的圖層繪制
            canvas.translate(getPaddingLeft(), getPaddingTop());
            canvas.concat(mDrawMatrix);
            drawable.draw(canvas);
            drawFace(canvas);
            canvas.restore();                                       // 合并圖層
        }
    }

    /**
     * 繪制超過顯示區(qū)域的人臉矩形
     */
    private void drawFace(Canvas canvas) {
        for (int i = 0; i < mFaceCount; i++) {
            if (mFaces[i] != null && mIsNeedDraws[i]) {
                mFaces[i].getMidPoint(mPoint);
                float distance = mFaces[i].eyesDistance() * mAdjustScale;
                RectF rectF = new RectF(mPoint.x * mAdjustScale - distance
                        , mPoint.y * mAdjustScale - FACE_VERTICAL * distance
                        , mPoint.x * mAdjustScale + distance
                        , mPoint.y * mAdjustScale + FACE_VERTICAL * distance);
                canvas.drawRect(rectF, mPaint);
            }
        }
    }
至此歹垫,整個(gè)圖片的縮放,平移和人臉識(shí)別基本結(jié)束排惨,關(guān)于檢查邊界并計(jì)算回彈距離吭敢,以及BitmapUtils加載本地圖片相關(guān)的代碼大家可以查看完整的demo暮芭,地址:https://github.com/gsy13213009/FaceRecognition.git
  • 有關(guān)縮放平移的代碼鹿驼,我也是參考了這個(gè)哥們的分享完成的,因此邊界回彈等相關(guān)代碼基本照搬畜晰,只是多了一些比如人臉識(shí)別砾莱,縮放的極限設(shè)置以及圖片的填充方向等工作凄鼻,該博客的地址為:額腊瑟,找不到了块蚌,以后遇到再填吧

歡迎大家交流和指正哈~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市峭范,隨后出現(xiàn)的幾起案子财松,更是在濱河造成了極大的恐慌虎敦,老刑警劉巖游岳,帶你破解...
    沈念sama閱讀 211,817評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件其徙,死亡現(xiàn)場(chǎng)離奇詭異胚迫,居然都是意外死亡唾那,警方通過查閱死者的電腦和手機(jī)访锻,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,329評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門闹获,熙熙樓的掌柜王于貴愁眉苦臉地迎上來期犬,“玉大人避诽,你說我怎么就攤上這事龟虎∩陈” “怎么了鲤妥?”我有些...
    開封第一講書人閱讀 157,354評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵拱雏,是天一觀的道長(zhǎng)棉安。 經(jīng)常有香客問我铸抑,道長(zhǎng)贡耽,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,498評(píng)論 1 284
  • 正文 為了忘掉前任蒲赂,我火速辦了婚禮,結(jié)果婚禮上凳宙,老公的妹妹穿的比我還像新娘熙揍。我一直安慰自己氏涩,他們只是感情好届囚,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,600評(píng)論 6 386
  • 文/花漫 我一把揭開白布是尖。 她就那樣靜靜地躺著意系,像睡著了一般饺汹。 火紅的嫁衣襯著肌膚如雪蛔添。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,829評(píng)論 1 290
  • 那天迎瞧,我揣著相機(jī)與錄音,去河邊找鬼凶硅。 笑死,一個(gè)胖子當(dāng)著我的面吹牛扫皱,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播韩脑,決...
    沈念sama閱讀 38,979評(píng)論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼段多!你這毒婦竟也來了首量?” 一聲冷哼從身側(cè)響起进苍,我...
    開封第一講書人閱讀 37,722評(píng)論 0 266
  • 序言:老撾萬榮一對(duì)情侶失蹤蕾总,失蹤者是張志新(化名)和其女友劉穎琅捏,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體递雀,經(jīng)...
    沈念sama閱讀 44,189評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,519評(píng)論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了市俊。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,654評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡滤奈,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出蜒程,到底是詐尸還是另有隱情绅你,我是刑警寧澤昭躺,帶...
    沈念sama閱讀 34,329評(píng)論 4 330
  • 正文 年R本政府宣布忌锯,位于F島的核電站领炫,受9級(jí)特大地震影響偶垮,放射性物質(zhì)發(fā)生泄漏帝洪。R本人自食惡果不足惜似舵,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,940評(píng)論 3 313
  • 文/蒙蒙 一葱峡、第九天 我趴在偏房一處隱蔽的房頂上張望砚哗。 院中可真熱鬧族沃,春花似錦频祝、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,762評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)盖溺。三九已至,卻和暖如春烘嘱,著一層夾襖步出監(jiān)牢的瞬間昆禽,已是汗流浹背蝇庭。 一陣腳步聲響...
    開封第一講書人閱讀 31,993評(píng)論 1 266
  • 我被黑心中介騙來泰國(guó)打工醉鳖, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留哮内,地道東北人盗棵。 一個(gè)月前我還...
    沈念sama閱讀 46,382評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像纹因,于是被迫代替她去往敵國(guó)和親喷屋。 傳聞我的和親對(duì)象是個(gè)殘疾皇子瞭恰,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,543評(píng)論 2 349

推薦閱讀更多精彩內(nèi)容

  • 源碼地址 實(shí)現(xiàn)原理概覽 我們要實(shí)現(xiàn)手指控制圖片的平移驳棱、旋轉(zhuǎn)、縮放农曲,首先得知道手指做了什么動(dòng)作,比如用戶兩指間距離是...
    籬開羅閱讀 5,829評(píng)論 15 12
  • 手勢(shì)圖片控件 PinchImageView 點(diǎn)擊圖片框架 photoView packagecom.example...
    Ztufu閱讀 718評(píng)論 0 1
  • 先上效果圖 源碼 一冻辩、需求分析 單點(diǎn)拖動(dòng)圖片對(duì)圖片進(jìn)行平移操作猖腕。雙手縮放圖片大小和旋轉(zhuǎn)圖片到一定的角度。圖片縮放的...
    YC1995閱讀 11,299評(píng)論 10 26
  • 概述 項(xiàng)目開發(fā)中倘感,大家APP開發(fā)一般都會(huì)用到上傳圖片,比如是上傳了自己的生活照咙咽,然后在某個(gè)界面處查看上傳的圖片,這...
    青蛙要fly閱讀 6,185評(píng)論 2 20
  • 把愛展開钧敞,放大蜡豹,給它敦厚和遼闊的境界溉苛,豈有不恒定亙久之理镜廉。 靈魂的尊貴是在愛的博大里體現(xiàn)出來的。 不要因?yàn)椴荒芙o對(duì)...
    紅蔚閱讀 323評(píng)論 1 3