热门标签 | 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开发实现的计时器功能示例
    本文分享了Android开发实现的计时器功能示例,包括效果图、布局和按钮的使用。通过使用Chronometer控件,可以实现计时器功能。该示例适用于Android平台,供开发者参考。 ... [详细]
  • Java太阳系小游戏分析和源码详解
    本文介绍了一个基于Java的太阳系小游戏的分析和源码详解。通过对面向对象的知识的学习和实践,作者实现了太阳系各行星绕太阳转的效果。文章详细介绍了游戏的设计思路和源码结构,包括工具类、常量、图片加载、面板等。通过这个小游戏的制作,读者可以巩固和应用所学的知识,如类的继承、方法的重载与重写、多态和封装等。 ... [详细]
  • YOLOv7基于自己的数据集从零构建模型完整训练、推理计算超详细教程
    本文介绍了关于人工智能、神经网络和深度学习的知识点,并提供了YOLOv7基于自己的数据集从零构建模型完整训练、推理计算的详细教程。文章还提到了郑州最低生活保障的话题。对于从事目标检测任务的人来说,YOLO是一个熟悉的模型。文章还提到了yolov4和yolov6的相关内容,以及选择模型的优化思路。 ... [详细]
  • 本文介绍了解决Netty拆包粘包问题的一种方法——使用特殊结束符。在通讯过程中,客户端和服务器协商定义一个特殊的分隔符号,只要没有发送分隔符号,就代表一条数据没有结束。文章还提供了服务端的示例代码。 ... [详细]
  • Iamtryingtomakeaclassthatwillreadatextfileofnamesintoanarray,thenreturnthatarra ... [详细]
  • 本文介绍了OC学习笔记中的@property和@synthesize,包括属性的定义和合成的使用方法。通过示例代码详细讲解了@property和@synthesize的作用和用法。 ... [详细]
  • XML介绍与使用的概述及标签规则
    本文介绍了XML的基本概念和用途,包括XML的可扩展性和标签的自定义特性。同时还详细解释了XML标签的规则,包括标签的尖括号和合法标识符的组成,标签必须成对出现的原则以及特殊标签的使用方法。通过本文的阅读,读者可以对XML的基本知识有一个全面的了解。 ... [详细]
  • 关键词:Golang, Cookie, 跟踪位置, net/http/cookiejar, package main, golang.org/x/net/publicsuffix, io/ioutil, log, net/http, net/http/cookiejar ... [详细]
  • Android系统移植与调试之如何修改Android设备状态条上音量加减键在横竖屏切换的时候的显示于隐藏
    本文介绍了如何修改Android设备状态条上音量加减键在横竖屏切换时的显示与隐藏。通过修改系统文件system_bar.xml实现了该功能,并分享了解决思路和经验。 ... [详细]
  • [大整数乘法] java代码实现
    本文介绍了使用java代码实现大整数乘法的过程,同时也涉及到大整数加法和大整数减法的计算方法。通过分治算法来提高计算效率,并对算法的时间复杂度进行了研究。详细代码实现请参考文章链接。 ... [详细]
  • 本文介绍了Android 7的学习笔记总结,包括最新的移动架构视频、大厂安卓面试真题和项目实战源码讲义。同时还分享了开源的完整内容,并提醒读者在使用FileProvider适配时要注意不同模块的AndroidManfiest.xml中配置的xml文件名必须不同,否则会出现问题。 ... [详细]
  • Android源码深入理解JNI技术的概述和应用
    本文介绍了Android源码中的JNI技术,包括概述和应用。JNI是Java Native Interface的缩写,是一种技术,可以实现Java程序调用Native语言写的函数,以及Native程序调用Java层的函数。在Android平台上,JNI充当了连接Java世界和Native世界的桥梁。本文通过分析Android源码中的相关文件和位置,深入探讨了JNI技术在Android开发中的重要性和应用场景。 ... [详细]
  • 本文介绍了Hyperledger Fabric外部链码构建与运行的相关知识,包括在Hyperledger Fabric 2.0版本之前链码构建和运行的困难性,外部构建模式的实现原理以及外部构建和运行API的使用方法。通过本文的介绍,读者可以了解到如何利用外部构建和运行的方式来实现链码的构建和运行,并且不再受限于特定的语言和部署环境。 ... [详细]
  • 本文介绍了Perl的测试框架Test::Base,它是一个数据驱动的测试框架,可以自动进行单元测试,省去手工编写测试程序的麻烦。与Test::More完全兼容,使用方法简单。以plural函数为例,展示了Test::Base的使用方法。 ... [详细]
  • 本文介绍了在多平台下进行条件编译的必要性,以及具体的实现方法。通过示例代码展示了如何使用条件编译来实现不同平台的功能。最后总结了只要接口相同,不同平台下的编译运行结果也会相同。 ... [详细]
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社区 版权所有