热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

安卓开发(二)人脸识别相册FaceMap

本篇主要讲本科时做的一个应用,人脸识别相册。主要包含JNI和业务逻辑。最终代码会公布在github。算法部分当时深度学习还没有很火,所以用的是经典的

本篇主要讲本科时做的一个应用,人脸识别相册。主要包含JNI和业务逻辑。最终代码会公布在github。

算法部分

当时深度学习还没有很火,所以用的是经典的PCA方法,降维之后直接作为特征。人脸检测部分用的也是Opencv的Haar特征人脸检测。现在来看性能比较差了。这里就不介绍算法了,感兴趣的可以看看我的其他博文。

系统架构

本次系统架构分成2部分,一是算法;二是界面和业务逻辑。算法部分由于是C++写的,所以需要用Java的JNI接口封装一下。界面方面采用瀑布流界面,三栏极简风格。
业务逻辑是:
1.用户打开相册,自动读取系统照片;
2.用户点击某一张图像,若此照片此前未检测,则检测人脸并提取特征;若此照片此前已经检测,则显示人脸位置。
3.用户点击人脸,手工标注。
4.在第三栏,按照人脸识别实现自动分类。
这个逻辑的缺点是一开始需要用户手动标记,现在看来大概可以用聚类算法代替。

算法的JNI封装

因为我已经不做开发了,所以现在并不知道JNI怎么写了,已经不会写了。还是贴贴代码吧:

package cn.edu.zju.srtp.facemap.algorithm;import java.io.File;
import java.util.Vector;import org.opencv.core.Mat;import android.content.Context;
import android.util.Log;public class NativeFaceRecognizer {public static final int LBPH_FACERECOGNIZER &#61;1;public static final int FISHER_FACERECOGNIZER &#61;2;public static final int EIGEN_FACERECOGNIZER &#61;3;private final static String TAG &#61;"NativeFaceRecognizer";public final static String LBPH_FILENAME &#61;"lbph_model.xml";public final static String FISHER_FILENAME &#61;"fisher_model.xml";public final static String EIGEN_FILENAME &#61;"eigen_model.xml";/**Note which faceRecognizer has been created* */private int mState &#61;1;/**The pointer to faceRecognizer with the initial value -1(important)* */private long mNativeObj &#61;-1;private Context mContext;static{Log.d(TAG, "NativeFaceRecognizer static initial block enter");System.loadLibrary("opencv_java");System.loadLibrary("face_rec");Log.d(TAG, "NativeFaceRecognizer static initial block exit");}public NativeFaceRecognizer(Context context,int state) {mContext&#61;context;mState&#61;state;createFaceRecognizer();load();}/**Destroy the model first and create a FisherFaceRecognizer model.* */public void createFisherFaceRecognizer(){destroy();mState&#61;FISHER_FACERECOGNIZER;mNativeObj&#61;nativeCreateFisherFaceRecognizer();}/**Destroy the model first and create a LBPHFaceRecognizer model.* */public void createLBPHFaceRecognizer(){destroy();mState&#61;LBPH_FACERECOGNIZER;mNativeObj&#61;nativeCreateLBPHFaceRecognizer();}/**Destroy the model first and create a EigenFaceRecognizer model.* */public void createEigenFaceRecognizer(){destroy();mState&#61;EIGEN_FACERECOGNIZER;mNativeObj&#61;nativeCreateEigenFaceRecognizer();}/**Train the model.The input image should be grayscale.* The number of input images and labels should be equal.* &#64;param images* &#64;param labels_vec*/public void train(Vectorimages,Vectorlabels_vec){if(mNativeObj&#61;&#61;-1){Log.d(TAG,"Error:No faceRecognizer for training");return;}if(images.size()!&#61;labels_vec.size()){Log.d(TAG, "The number of input images and labels are not eaual!");return;}//get the array of Mat addresslong imagesAddr[]&#61;new long[images.size()];int i&#61;0;int[]labels&#61;new int[labels_vec.size()];for(Mat image:images){if(image.width()!&#61;image.height()||image.channels()!&#61;1||image.width()<&#61;0){Log.d(TAG, "The intput images for training is illegal!" &#43;"image.width&#61;"&#43;image.width()&#43;" image.height&#61;"&#43;image.height()&#43;"image.channels&#61;"&#43;image.channels());return;}imagesAddr[i]&#61;image.getNativeObjAddr();labels[i]&#61;labels_vec.elementAt(i).intValue();i&#43;&#43;;}nativeTrain(mNativeObj,imagesAddr,labels);}public int predict(Mat image){if(mNativeObj&#61;&#61;-1){Log.d(TAG,"Error:No faceRecognizer for predict.");return -1;}return nativePredict(mNativeObj,image.getNativeObjAddr());}/**Only LBPHfaceRecognizer can be updated.* * &#64;param images* &#64;param labels_vec*/public void update(Vectorimages,Vectorlabels_vec){if(mState!&#61;LBPH_FACERECOGNIZER){Log.d(TAG,"Error:This kind of model"&#43;" cannot be updated.");return;}if(mNativeObj&#61;&#61;-1){Log.d(TAG,"Error:No faceRecognizer for model update.");createLBPHFaceRecognizer();}//get the array of Mat addresslong imagesAddr[]&#61;new long[images.size()];int i&#61;0;int[]labels&#61;new int[labels_vec.size()];for(Mat image:images){if(image.width()!&#61;image.height()||image.channels()!&#61;1||image.width()<&#61;0){Log.d(TAG, "The intput images for training is illegal!" &#43;"image.width&#61;"&#43;image.width()&#43;" image.height&#61;"&#43;image.height()&#43;"image.channels&#61;"&#43;image.channels());return;}imagesAddr[i]&#61;image.getNativeObjAddr();labels[i]&#61;labels_vec.elementAt(i).intValue();i&#43;&#43;;}nativeUpdate(mNativeObj, imagesAddr, labels);return;}/**Save the model to file.* */public void save(){if(mNativeObj&#61;&#61;-1){Log.d(TAG,"Error:No faceRecognizer to save");return;}String path&#61;mContext.getFilesDir()&#43;"/"&#43;getFileName();nativeSave(mNativeObj, path);}public void load(){if(mNativeObj&#61;&#61;-1){Log.d(TAG,"Error:No faceRecognizer to load");return;}String path&#61;mContext.getFilesDir()&#43;"/"&#43;getFileName();File f&#61;new File(path);if(f.exists()){try{nativeLoad(mNativeObj, path);}catch(Exception e){Log.d(TAG,"load file Alert:model doesn&#39;t exists.");}}}public void destroy(){if(mNativeObj!&#61;-1){nativeDestroyObject(mNativeObj);mNativeObj&#61;-1;}else{Log.d(TAG,"Cannot delete an illegal pointer");}}/***Clear the model file. */public Boolean clear(){String fileName&#61;getFileName();File f&#61;new File(mContext.getFilesDir()&#43;"/"&#43;fileName);if(f.exists()){return mContext.deleteFile(fileName); }return false;}private String getFileName(){String fileName&#61;new String();if(mState&#61;&#61;LBPH_FACERECOGNIZER){fileName&#61;LBPH_FILENAME;}else if(mState&#61;&#61;FISHER_FACERECOGNIZER){fileName&#61;FISHER_FILENAME;}else if(mState&#61;&#61;EIGEN_FACERECOGNIZER){fileName&#61;EIGEN_FILENAME;}else{return null;}return fileName;}private void createFaceRecognizer(){if(mState&#61;&#61;LBPH_FACERECOGNIZER){createLBPHFaceRecognizer(); }else if(mState&#61;&#61;FISHER_FACERECOGNIZER){createFisherFaceRecognizer();}else if(mState&#61;&#61;EIGEN_FACERECOGNIZER){createEigenFaceRecognizer();}}public int getState(){return mState;}private static native long nativeCreateFisherFaceRecognizer();private static native long nativeCreateLBPHFaceRecognizer();private static native long nativeCreateEigenFaceRecognizer();private static native void nativeTrain(long thiz,long[]images,int[]labels);private static native void nativeUpdate(long thiz,long[]images,int[]labels);private static native int nativePredict(long thiz,long image);private static native void nativeSave(long thiz,String fileName);private static native void nativeLoad(long thiz,String fileName);private static native void nativeDestroyObject(long thiz);}

依稀记得当时写这个碰到了一些问题。主要的问题是&#xff0c;我在C&#43;&#43;里写的类&#xff0c;需要动态分配&#xff0c;然后把它的指针保存在Java里。每次Java把这个指针再传给C&#43;&#43;。这种方式是以Java为主的编程方式。不知道还有没有其他更好的写法。

图像缩放

当时为了实现图像缩放&#xff0c;在图书馆写了一上午。主要觉得挺好玩的。年少。

package cn.edu.zju.srtp.facemap.album;import java.util.ArrayList;
import java.util.Calendar;import org.opencv.core.Rect;import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.text.format.Time;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.widget.EditText;
import android.widget.Toast;
import cn.edu.zju.srtp.facemap.R;
import cn.edu.zju.srtp.facemap.algorithm.Face;
import cn.edu.zju.srtp.facemap.algorithm.FaceRecManagerSingleInstance;public class ZoomImageView extends View{private static final String TAG &#61;"ZoomImageView";/** * 初始化状态常量 */ public static final int STATUS_INIT &#61; 1; /** * 图片放大状态常量 */ public static final int STATUS_ZOOM_OUT &#61; 2; /** * 图片缩小状态常量 */ public static final int STATUS_ZOOM_IN &#61; 3; /** * 图片拖动状态常量 */ public static final int STATUS_MOVE &#61; 4; /** * 用于对图片进行移动和缩放变换的矩阵 */ private Matrix matrix &#61; new Matrix(); /** * 待展示的Bitmap对象 */ private Bitmap sourceBitmap;/***图片边框 */private Bitmap borderBitmap;/** * 记录当前操作的状态&#xff0c;可选值为STATUS_INIT、STATUS_ZOOM_OUT、STATUS_ZOOM_IN和STATUS_MOVE */ private int currentStatus; private ArrayList mFaces&#61;new ArrayList();private int mInSampleSize &#61;1;private Paint mPaint&#61;new Paint();private Bitmap mBackground;private Time mTime &#61;new Time();private long mStartTime;private Face mFaceClicked;/** * ZoomImageView控件的宽度 */ private int width; /** * ZoomImageView控件的高度 */ private int height; /** * 记录两指同时放在屏幕上时&#xff0c;中心点的横坐标值 */ private float centerPointX; /** * 记录两指同时放在屏幕上时&#xff0c;中心点的纵坐标值 */ private float centerPointY; /** * 记录当前图片的宽度&#xff0c;图片被缩放时&#xff0c;这个值会一起变动 */ private float currentBitmapWidth; /** * 记录当前图片的高度&#xff0c;图片被缩放时&#xff0c;这个值会一起变动 */ private float currentBitmapHeight; /** * 记录上次手指移动时的横坐标 */ private float lastXMove &#61; -1; /** * 记录上次手指移动时的纵坐标 */ private float lastYMove &#61; -1; /** * 记录手指在横坐标方向上的移动距离 */ private float movedDistanceX; /** * 记录手指在纵坐标方向上的移动距离 */ private float movedDistanceY; /** * 记录图片在矩阵上的横向偏移值 */ private float totalTranslateX; /** * 记录图片在矩阵上的纵向偏移值 */ private float totalTranslateY; /** * 记录图片在矩阵上的总缩放比例 */ private float totalRatio; /** * 记录手指移动的距离所造成的缩放比例 */ private float scaledRatio; /** * 记录图片初始化时的缩放比例 */ private float initRatio; /** * 记录上次两指之间的距离 */ private double lastFingerDis; /** * ZoomImageView构造函数&#xff0c;将当前操作状态设为STATUS_INIT。 * * &#64;param context * &#64;param attrs */ public ZoomImageView(Context context, AttributeSet attrs) { super(context, attrs); currentStatus &#61; STATUS_INIT; mPaint.setColor(Color.WHITE);mPaint.setStrokeWidth(2);mPaint.setStyle(Paint.Style.STROKE);borderBitmap&#61;BitmapFactory.decodeResource(getResources(), R.drawable.picture_frame_default);} /** * 将待展示的图片设置进来。 * * &#64;param bitmap * 待展示的Bitmap对象 */ public void setImageBitmap(Bitmap bitmap) { sourceBitmap &#61; bitmap; invalidate(); } public void setInSampleSize(int inSampleSize){mInSampleSize&#61;inSampleSize;}public void setFaces(ArrayListfaces){mFaces&#61;(ArrayList) faces.clone();invalidate();}&#64;Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); if (changed) { // 分别获取到ZoomImageView的宽度和高度 width &#61; getWidth(); height &#61; getHeight(); } } &#64;Override public boolean onTouchEvent(MotionEvent event) { if(totalRatio>initRatio){ getParent().requestDisallowInterceptTouchEvent(true);}else{getParent().requestDisallowInterceptTouchEvent(false); }switch (event.getActionMasked()) { case MotionEvent.ACTION_POINTER_DOWN: if (event.getPointerCount() &#61;&#61; 2) { // 当有两个手指按在屏幕上时&#xff0c;计算两指之间的距离 lastFingerDis &#61; distanceBetweenFingers(event); }break; case MotionEvent.ACTION_DOWN:
// mTime.setToNow();mStartTime&#61;Calendar.getInstance().getTimeInMillis();case MotionEvent.ACTION_MOVE: if (event.getPointerCount() &#61;&#61; 1) { // 只有单指按在屏幕上移动时&#xff0c;为拖动状态 float xMove &#61; event.getX(); float yMove &#61; event.getY(); if (lastXMove &#61;&#61; -1 && lastYMove &#61;&#61; -1) { lastXMove &#61; xMove; lastYMove &#61; yMove; } currentStatus &#61; STATUS_MOVE;movedDistanceX &#61; xMove - lastXMove; movedDistanceY &#61; yMove - lastYMove; // 进行边界检查&#xff0c;不允许将图片拖出边界 if (totalTranslateX &#43; movedDistanceX > 0) { movedDistanceX &#61; 0; } else if (width - (totalTranslateX &#43; movedDistanceX) > currentBitmapWidth) { movedDistanceX &#61; 0; } if (totalTranslateY &#43; movedDistanceY > 0) { movedDistanceY &#61; 0; } else if (height - (totalTranslateY &#43; movedDistanceY) > currentBitmapHeight) { movedDistanceY &#61; 0; } // 调用onDraw()方法绘制图片 invalidate(); lastXMove &#61; xMove; lastYMove &#61; yMove; }else if(event.getPointerCount() &#61;&#61; 2){// 有两个手指按在屏幕上移动时&#xff0c;为缩放状态 centerPointBetweenFingers(event); double fingerDis &#61; distanceBetweenFingers(event); if (fingerDis > lastFingerDis) { currentStatus &#61; STATUS_ZOOM_OUT; } else { currentStatus &#61; STATUS_ZOOM_IN; } // 进行缩放倍数检查&#xff0c;最大只允许将图片放大4倍&#xff0c;最小可以缩小到初始化比例 if ((currentStatus &#61;&#61; STATUS_ZOOM_OUT && totalRatio <4 * initRatio) || (currentStatus &#61;&#61; STATUS_ZOOM_IN && totalRatio > initRatio)) { scaledRatio &#61; (float) (fingerDis / lastFingerDis); totalRatio &#61; totalRatio * scaledRatio;if (totalRatio > 4 * initRatio) { totalRatio &#61; 4 * initRatio; } else if (totalRatio // 调用onDraw()方法绘制图片 invalidate(); lastFingerDis &#61; fingerDis; }}break;case MotionEvent.ACTION_POINTER_UP:if (event.getPointerCount() &#61;&#61; 2) { // 手指离开屏幕时将临时值还原 lastXMove &#61; -1; lastYMove &#61; -1; } break; case MotionEvent.ACTION_UP: // 手指离开屏幕时将临时值还原 lastXMove &#61; -1; lastYMove &#61; -1; //Time t&#61;new Time();//t.setToNow();long t&#61;Calendar.getInstance().getTimeInMillis();if(t-mStartTime>&#61;100){break;}float fX&#61;event.getX();float fY&#61;event.getY();mFaceClicked&#61;null;for(Face f:mFaces){Rect faceRect&#61;f.getFaceRect();float left&#61;faceRect.x*totalRatio/mInSampleSize&#43;totalTranslateX;float top&#61;faceRect.y*totalRatio/mInSampleSize&#43;totalTranslateY;float right&#61;(faceRect.x&#43;faceRect.width)*totalRatio/mInSampleSize&#43;totalTranslateX;float bottom&#61;(faceRect.y&#43;faceRect.height)*totalRatio/mInSampleSize&#43;totalTranslateY;if(fX>&#61;left&&fX<&#61;right&&fY>&#61;top&&fY<&#61;bottom){mFaceClicked&#61;f;}}final Face face&#61;mFaceClicked;if(face&#61;&#61;null)break;invalidate();if(face.getPeopleName()&#61;&#61;null){final EditText mText &#61; new EditText(getContext());AlertDialog.Builder builder &#61; new AlertDialog.Builder(getContext());builder.setTitle("请输入名字").setIcon(android.R.drawable.ic_dialog_info).setView(mText).setPositiveButton("确定",new DialogInterface.OnClickListener() {&#64;Overridepublic void onClick(DialogInterface dialog,int which) {FaceRecManagerSingleInstance.getInstance(getContext()).update(face.getFaceId(),mText.getText().toString());invalidate();}}).setNegativeButton("取消", null).show();}else{AlertDialog.Builder builder &#61; new AlertDialog.Builder(getContext());builder.setMessage("这是 " &#43; face.getPeopleName() &#43; " 吗&#xff1f;").setIcon(android.R.drawable.ic_dialog_info).setPositiveButton("是",new DialogInterface.OnClickListener() {&#64;Overridepublic void onClick(DialogInterface dialog,int which) {FaceRecManagerSingleInstance.getInstance(getContext()).update(face.getFaceId(),face.getPeopleName());invalidate();}}).setNegativeButton("不是",new DialogInterface.OnClickListener() {&#64;Overridepublic void onClick(DialogInterface dialog,int which) {final EditText mText &#61; new EditText(getContext());AlertDialog.Builder builder &#61; new AlertDialog.Builder(getContext()); builder.setTitle("请输入名字").setIcon(android.R.drawable.ic_dialog_info).setView(mText).setPositiveButton("确定",new DialogInterface.OnClickListener() {&#64;Overridepublic void onClick(DialogInterface dialog,int which) {FaceRecManagerSingleInstance.getInstance(getContext()).update(face.getFaceId(),mText.getText().toString());invalidate();}}).setNegativeButton("取消",null).show();}}).show();} break; default: break; }return true;}public Bitmap getSrcBitmap(){return sourceBitmap;}private void drawFaces(Canvas canvas){Log.d(TAG,"drawFaces");for(Face face:mFaces){Rect faceRect&#61;face.getFaceRect();float left&#61;faceRect.x*totalRatio/mInSampleSize&#43;totalTranslateX;float top&#61;faceRect.y*totalRatio/mInSampleSize&#43;totalTranslateY;float right&#61;(faceRect.x&#43;faceRect.width)*totalRatio/mInSampleSize&#43;totalTranslateX;float bottom&#61;(faceRect.y&#43;faceRect.height)*totalRatio/mInSampleSize&#43;totalTranslateY;if(!face.getVerified()){mPaint.setColor(Color.GREEN);}if(face.equals(mFaceClicked)){mPaint.setColor(Color.YELLOW);mFaceClicked&#61;null;}canvas.drawRect(left,top,right,bottom, mPaint);if(face.getPeopleName()!&#61;null){mPaint.setTextSize((float) ((right-left)*0.15));mPaint.setStrokeWidth(0);canvas.drawText(face.getPeopleName(),left ,top-4, mPaint);mPaint.setStrokeWidth(2);}mPaint.setColor(Color.WHITE);}}/** * 根据currentStatus的值来决定对图片进行什么样的绘制操作。 */ &#64;Override protected void onDraw(Canvas canvas) { try{ super.onDraw(canvas);switch (currentStatus) { case STATUS_ZOOM_OUT: case STATUS_ZOOM_IN: zoom(canvas); break;case STATUS_MOVE: move(canvas); break; case STATUS_INIT: initBitmap(canvas); default: canvas.drawBitmap(sourceBitmap, matrix, null);drawFaces(canvas);break; }}catch(Exception e){Log.e(TAG,"Error in onDraw:",e);}}/** * 对图片进行缩放处理。 * * &#64;param canvas */ private void zoom(Canvas canvas) { matrix.reset(); // 将图片按总缩放比例进行缩放 matrix.postScale(totalRatio, totalRatio); float scaledWidth &#61; sourceBitmap.getWidth() * totalRatio; float scaledHeight &#61; sourceBitmap.getHeight() * totalRatio; float translateX &#61; 0f; float translateY &#61; 0f; // 如果当前图片宽度小于屏幕宽度&#xff0c;则按屏幕中心的横坐标进行水平缩放。//否则按两指的中心点的横坐标进行水平缩放if (currentBitmapWidth 2f; } else { translateX &#61; totalTranslateX * scaledRatio &#43; centerPointX * (1 - scaledRatio); // 进行边界检查&#xff0c;保证图片缩放后在水平方向上不会偏移出屏幕 if (translateX > 0) { translateX &#61; 0; } else if (width - translateX > scaledWidth) { translateX &#61; width - scaledWidth; } } // 如果当前图片高度小于屏幕高度&#xff0c;则按屏幕中心的纵//坐标进行垂直缩放。否则按两指的中心点的纵坐标进行垂直缩放if (currentBitmapHeight 2f; }else{translateY &#61; totalTranslateY * scaledRatio &#43; centerPointY * (1 - scaledRatio); // 进行边界检查&#xff0c;保证图片缩放后在垂直方向上不会偏移出屏幕 if (translateY > 0) { translateY &#61; 0; } else if (height - translateY > scaledHeight) { translateY &#61; height - scaledHeight; } }// 缩放后对图片进行偏移&#xff0c;以保证缩放后中心点位置不变 matrix.postTranslate(translateX, translateY); totalTranslateX &#61; translateX; totalTranslateY &#61; translateY; currentBitmapWidth &#61; scaledWidth; currentBitmapHeight &#61; scaledHeight; canvas.drawBitmap(sourceBitmap, matrix, null); drawFaces(canvas);}/** * 对图片进行平移处理 * * &#64;param canvas */ private void move(Canvas canvas) { matrix.reset(); // 根据手指移动的距离计算出总偏移值 float translateX &#61; totalTranslateX &#43; movedDistanceX; float translateY &#61; totalTranslateY &#43; movedDistanceY; // 先按照已有的缩放比例对图片进行缩放 matrix.postScale(totalRatio, totalRatio); // 再根据移动距离进行偏移 matrix.postTranslate(translateX, translateY); totalTranslateX &#61; translateX; totalTranslateY &#61; translateY; canvas.drawBitmap(sourceBitmap, matrix, null); drawFaces(canvas);}/** * 对图片进行初始化操作&#xff0c;包括让图片居中&#xff0c;以及当图片大于屏幕宽高时对图片进行压缩。 * * &#64;param canvas */ private void initBitmap(Canvas canvas) { if (sourceBitmap !&#61; null) { matrix.reset(); int bitmapWidth &#61; sourceBitmap.getWidth(); int bitmapHeight &#61; sourceBitmap.getHeight(); if (bitmapWidth > width || bitmapHeight > height) { if (bitmapWidth - width > bitmapHeight - height) { // 当图片宽度大于屏幕宽度时&#xff0c;将图片等比例压缩&#xff0c;使它可以完全显示出来 float ratio &#61; width / (bitmapWidth * 1.0f); matrix.postScale(ratio, ratio); float translateY &#61; (height - (bitmapHeight * ratio)) / 2f; // 在纵坐标方向上进行偏移&#xff0c;以保证图片居中显示 matrix.postTranslate(0, translateY); totalTranslateY &#61; translateY; totalRatio &#61; initRatio &#61; ratio; }else { // 当图片高度大于屏幕高度时&#xff0c;将图片等比例压缩&#xff0c;使它可以完全显示出来 float ratio &#61; height / (bitmapHeight * 1.0f); matrix.postScale(ratio, ratio); float translateX &#61; (width - (bitmapWidth * ratio)) / 2f; // 在横坐标方向上进行偏移&#xff0c;以保证图片居中显示 matrix.postTranslate(translateX, 0); totalTranslateX &#61; translateX; totalRatio &#61; initRatio &#61; ratio; }currentBitmapWidth &#61; bitmapWidth * initRatio; currentBitmapHeight &#61; bitmapHeight * initRatio;}else{// 当图片的宽高都小于屏幕宽高时&#xff0c;直接让图片居中显示 float translateX &#61; (width - sourceBitmap.getWidth()) / 2f; float translateY &#61; (height - sourceBitmap.getHeight()) / 2f; matrix.postTranslate(translateX, translateY); totalTranslateX &#61; translateX; totalTranslateY &#61; translateY; totalRatio &#61; initRatio &#61; 1f; currentBitmapWidth &#61; bitmapWidth; currentBitmapHeight &#61; bitmapHeight; }canvas.drawBitmap(sourceBitmap, matrix, null); drawFaces(canvas);} }/** * 计算两个手指之间的距离。 * * &#64;param event * &#64;return 两个手指之间的距离 */ private double distanceBetweenFingers(MotionEvent event) { float disX &#61; Math.abs(event.getX(0) - event.getX(1)); float disY &#61; Math.abs(event.getY(0) - event.getY(1)); return Math.sqrt(disX * disX &#43; disY * disY); } /** * 计算两个手指之间中心点的坐标。 * * &#64;param event */ private void centerPointBetweenFingers(MotionEvent event) { float xPoint0 &#61; event.getX(0); float yPoint0 &#61; event.getY(0); float xPoint1 &#61; event.getX(1); float yPoint1 &#61; event.getY(1); centerPointX &#61; (xPoint0 &#43; xPoint1) / 2; centerPointY &#61; (yPoint0 &#43; yPoint1) / 2; }
}

存在的问题

瀑布流渲染似乎有点慢&#xff0c;要等很久的样子。有时候闪退。


推荐阅读
  • 深入解析Java枚举及其高级特性
    本文详细介绍了Java枚举的概念、语法、使用规则和应用场景,并探讨了其在实际编程中的高级应用。所有相关内容已收录于GitHub仓库[JavaLearningmanual](https://github.com/Ziphtracks/JavaLearningmanual),欢迎Star并持续关注。 ... [详细]
  • Redux入门指南
    本文介绍Redux的基本概念和工作原理,帮助初学者理解如何使用Redux管理应用程序的状态。Redux是一个用于JavaScript应用的状态管理库,特别适用于React项目。 ... [详细]
  • 本文介绍如何从字符串中移除大写、小写、特殊、数字和非数字字符,并提供了多种编程语言的实现示例。 ... [详细]
  • 深入解析 Android IPC 中的 Messenger 机制
    本文详细介绍了 Android 中基于消息传递的进程间通信(IPC)机制——Messenger。通过实例和源码分析,帮助开发者更好地理解和使用这一高效的通信工具。 ... [详细]
  • 本文详细探讨了JavaScript中的作用域链和闭包机制,解释了它们的工作原理及其在实际编程中的应用。通过具体的代码示例,帮助读者更好地理解和掌握这些概念。 ... [详细]
  • 本文介绍如何使用MFC和ADO技术调用SQL Server中的存储过程,以查询指定小区在特定时间段内的通话统计数据。通过用户界面选择小区ID、开始时间和结束时间,系统将计算并展示小时级的通话量、拥塞率及半速率通话比例。 ... [详细]
  • 实用正则表达式有哪些
    小编给大家分享一下实用正则表达式有哪些,相信大部分人都还不怎么了解,因此分享这篇文章给大家参考一下,希望大家阅读完这篇文章后大有收获,下 ... [详细]
  • 我有一个SpringRestController,它处理API调用的版本1。继承在SpringRestControllerpackagerest.v1;RestCon ... [详细]
  • 简化报表生成:EasyReport工具的全面解析
    本文详细介绍了EasyReport,一个易于使用的开源Web报表工具。该工具支持Hadoop、HBase及多种关系型数据库,能够将SQL查询结果转换为HTML表格,并提供Excel导出、图表显示和表头冻结等功能。 ... [详细]
  • 深入理解Vue.js:从入门到精通
    本文详细介绍了Vue.js的基础知识、安装方法、核心概念及实战案例,帮助开发者全面掌握这一流行的前端框架。 ... [详细]
  • LeetCode 690:计算员工的重要性评分
    在解决LeetCode第690题时,我记录了详细的解题思路和方法。该问题要求根据员工的ID计算其重要性评分,包括直接和间接下属的重要性。本文将深入探讨如何使用哈希表(Map)来高效地实现这一目标。 ... [详细]
  • 本文探讨了为何相同的HTTP请求在两台不同操作系统(Windows与Ubuntu)的机器上会分别返回200 OK和429 Too Many Requests的状态码。我们将分析代码、环境差异及可能的影响因素。 ... [详细]
  • 深入解析动态代理模式:23种设计模式之三
    在设计模式中,动态代理模式是应用最为广泛的一种代理模式。它允许我们在运行时动态创建代理对象,并在调用方法时进行增强处理。本文将详细介绍动态代理的实现机制及其应用场景。 ... [详细]
  • Python + Pytest 接口自动化测试中 Token 关联登录的实现方法
    本文将深入探讨 Python 和 Pytest 在接口自动化测试中如何实现 Token 关联登录,内容详尽、逻辑清晰,旨在帮助读者掌握这一关键技能。 ... [详细]
  • 为了解决不同服务器间共享图片的需求,我们最初考虑建立一个FTP图片服务器。然而,考虑到项目是一个简单的CMS系统,为了简化流程,团队决定探索七牛云存储的解决方案。本文将详细介绍使用七牛云存储的过程和心得。 ... [详细]
author-avatar
赵庭洪
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有