圖片的縮放,平移這些需求還是挺常見的君账,我通過自定義ImageView實(shí)現(xiàn)縮放和平移沈善,結(jié)合系統(tǒng)提供API實(shí)現(xiàn)人臉的識(shí)別
圖片的縮放闻牡,平移,人臉識(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è)置以及圖片的填充方向等工作凄鼻,該博客的地址為:額腊瑟,找不到了块蚌,以后遇到再填吧
歡迎大家交流和指正哈~