热门标签 | 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;要等很久的样子。有时候闪退。


推荐阅读
  • 处理Android EditText中数字输入与parseInt方法
    本文探讨了如何在Android应用中从EditText组件安全地获取并解析用户输入的数字,特别是用于设置端口号的情况。通过示例代码和异常处理策略,展示了有效的方法来避免因非法输入导致的应用崩溃。 ... [详细]
  • 本文深入探讨了WPF框架下的数据验证机制,包括内置验证规则的使用、自定义验证规则的实现方法、错误信息的有效展示策略以及验证时机的选择,旨在帮助开发者构建更加健壮和用户友好的应用程序。 ... [详细]
  • 在1995年,Simon Plouffe 发现了一种特殊的求和方法来表示某些常数。两年后,Bailey 和 Borwein 在他们的论文中发表了这一发现,这种方法被命名为 Bailey-Borwein-Plouffe (BBP) 公式。该问题要求计算圆周率 π 的第 n 个十六进制数字。 ... [详细]
  • 本文详细介绍了 `org.apache.tinkerpop.gremlin.structure.VertexProperty` 类中的 `key()` 方法,并提供了多个实际应用的代码示例。通过这些示例,读者可以更好地理解该方法在图数据库操作中的具体用途。 ... [详细]
  • 二维码的实现与应用
    本文介绍了二维码的基本概念、分类及其优缺点,并详细描述了如何使用Java编程语言结合第三方库(如ZXing和qrcode.jar)来实现二维码的生成与解析。 ... [详细]
  • Beetl是一款先进的Java模板引擎,以其丰富的功能、直观的语法、卓越的性能和易于维护的特点著称。它不仅适用于高响应需求的大型网站,也适合功能复杂的CMS管理系统,提供了一种全新的模板开发体验。 ... [详细]
  • 本文介绍了如何通过C#语言调用动态链接库(DLL)中的函数来实现IC卡的基本操作,包括初始化设备、设置密码模式、获取设备状态等,并详细展示了将TextBox中的数据写入IC卡的具体实现方法。 ... [详细]
  • 深入解析C语言中的关键字及其分类
    本文将全面介绍C语言中的关键字,并按照功能将其分为数据类型关键字、控制结构关键字、存储类别关键字和其他关键字四大类,旨在帮助读者更好地理解和运用这些基本元素。C语言中共有32个关键字。 ... [详细]
  • Zabbix自定义监控与邮件告警配置实践
    本文详细介绍了如何在Zabbix中添加自定义监控项目,配置邮件告警功能,并解决测试告警时遇到的邮件不发送问题。 ... [详细]
  • 本文详细探讨了在Java编程语言中,构造函数、静态代码块和构造代码块的执行顺序。首先明确了静态代码块、构造代码块以及构造函数方法体的执行优先级,随后深入分析了构造函数体执行前的具体步骤,包括父类构造器的调用、非静态变量的初始化等。 ... [详细]
  • Maven + Spring + MyBatis + MySQL 环境搭建与实例解析
    本文详细介绍如何使用MySQL数据库进行环境搭建,包括创建数据库表并插入示例数据。随后,逐步指导如何配置Maven项目,整合Spring框架与MyBatis,实现高效的数据访问。 ... [详细]
  • publicclassBindActionextendsActionSupport{privateStringproString;privateStringcitString; ... [详细]
  • JUnit下的测试和suite
    nsitionalENhttp:www.w3.orgTRxhtml1DTDxhtml1-transitional.dtd ... [详细]
  • 本文深入探讨了Go语言中的接口型函数,通过实例分析其灵活性和强大功能,帮助开发者更好地理解和运用这一特性。 ... [详细]
  • Go从入门到精通系列视频之go编程语言密码学哈希算法(二) ... [详细]
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社区 版权所有