diff --git a/app/build.gradle b/app/build.gradle index 56e9da7..e011fb9 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -7,7 +7,6 @@ if (isNeedPackage.toBoolean() && isUseBooster.toBoolean()) { android { compileSdk 30 - defaultConfig { applicationId "org.easydarwin.easypusher" minSdk 29 diff --git a/app/src/main/java/com/bonus/canteen/activity/SplashActivity.java b/app/src/main/java/com/bonus/canteen/activity/SplashActivity.java index 0ab22b5..8f7c3fe 100644 --- a/app/src/main/java/com/bonus/canteen/activity/SplashActivity.java +++ b/app/src/main/java/com/bonus/canteen/activity/SplashActivity.java @@ -62,9 +62,9 @@ public class SplashActivity extends BaseSplashActivity implements CancelAdapt { private void loginOrGoMainPage() { if (TokenUtils.hasToken()) { - ActivityUtils.startActivity(InitializationActivity.class); + ActivityUtils.startActivity(OperationActivity.class); } else { - ActivityUtils.startActivity(InitializationActivity.class); + ActivityUtils.startActivity(OperationActivity.class); } finish(); } diff --git a/app/src/main/java/com/bonus/canteen/face/util/FaceRegisterInfo.java b/app/src/main/java/com/bonus/canteen/face/entity/FaceRegisterInfo.java similarity index 98% rename from app/src/main/java/com/bonus/canteen/face/util/FaceRegisterInfo.java rename to app/src/main/java/com/bonus/canteen/face/entity/FaceRegisterInfo.java index 16ef490..675d922 100644 --- a/app/src/main/java/com/bonus/canteen/face/util/FaceRegisterInfo.java +++ b/app/src/main/java/com/bonus/canteen/face/entity/FaceRegisterInfo.java @@ -1,4 +1,4 @@ -package com.bonus.canteen.face.util; +package com.bonus.canteen.face.entity; public class FaceRegisterInfo { private byte[] featureData; diff --git a/app/src/main/java/com/bonus/canteen/face/util/DrawInfo.java b/app/src/main/java/com/bonus/canteen/face/model/DrawInfo.java similarity index 90% rename from app/src/main/java/com/bonus/canteen/face/util/DrawInfo.java rename to app/src/main/java/com/bonus/canteen/face/model/DrawInfo.java index bc1fa06..3edd03e 100644 --- a/app/src/main/java/com/bonus/canteen/face/util/DrawInfo.java +++ b/app/src/main/java/com/bonus/canteen/face/model/DrawInfo.java @@ -1,4 +1,4 @@ -package com.bonus.canteen.face.util; +package com.bonus.canteen.face.model; import android.graphics.Rect; @@ -10,7 +10,7 @@ public class DrawInfo { private int color; private String name = null; - public DrawInfo(Rect rect, int sex, int age, int liveness, int color, String name) { + public DrawInfo(Rect rect, int sex, int age,int liveness,int color,String name) { this.rect = rect; this.sex = sex; this.age = age; diff --git a/app/src/main/java/com/bonus/canteen/face/util/CameraHelper.java b/app/src/main/java/com/bonus/canteen/face/util/CameraHelper.java index 45f1bb1..7757082 100644 --- a/app/src/main/java/com/bonus/canteen/face/util/CameraHelper.java +++ b/app/src/main/java/com/bonus/canteen/face/util/CameraHelper.java @@ -21,7 +21,7 @@ import java.util.List; */ public class CameraHelper implements Camera.PreviewCallback { private static final String TAG = "CameraHelper"; - private static Camera mCamera; + private Camera mCamera; private int mCameraId; private Point previewViewSize; private View previewDisplayView; @@ -98,7 +98,6 @@ public class CameraHelper implements Camera.PreviewCallback { previewSize = getBestSupportedSize(supportedPreviewSizes, previewViewSize); } parameters.setPreviewSize(previewSize.width, previewSize.height); - //对焦模式设置 List supportedFocusModes = parameters.getSupportedFocusModes(); if (supportedFocusModes != null && supportedFocusModes.size() > 0) { @@ -244,14 +243,14 @@ public class CameraHelper implements Camera.PreviewCallback { return bestSize; } - public static List getSupportedPreviewSizes() { + public List getSupportedPreviewSizes() { if (mCamera == null) { return null; } return mCamera.getParameters().getSupportedPreviewSizes(); } - public static List getSupportedPictureSizes() { + public List getSupportedPictureSizes() { if (mCamera == null) { return null; } diff --git a/app/src/main/java/com/bonus/canteen/face/util/CameraListener.java b/app/src/main/java/com/bonus/canteen/face/util/CameraListener.java index 6d03888..1b3e266 100644 --- a/app/src/main/java/com/bonus/canteen/face/util/CameraListener.java +++ b/app/src/main/java/com/bonus/canteen/face/util/CameraListener.java @@ -37,5 +37,4 @@ public interface CameraListener { * @param displayOrientation 相机旋转方向 */ void onCameraConfigurationChanged(int cameraID, int displayOrientation); - } diff --git a/app/src/main/java/com/bonus/canteen/face/util/ConfigUtil.java b/app/src/main/java/com/bonus/canteen/face/util/ConfigUtil.java index dfd1b8b..842008b 100644 --- a/app/src/main/java/com/bonus/canteen/face/util/ConfigUtil.java +++ b/app/src/main/java/com/bonus/canteen/face/util/ConfigUtil.java @@ -9,7 +9,6 @@ public class ConfigUtil { private static final String APP_NAME = "ArcFaceDemo"; private static final String TRACKED_FACE_COUNT = "trackedFaceCount"; private static final String FT_ORIENT = "ftOrientPriority"; - private static final String MAC_PRIORITY = "macPriority"; public static boolean setTrackedFaceCount(Context context, int trackedFaceCount) { if (context == null) { diff --git a/app/src/main/java/com/bonus/canteen/face/util/DrawHelper.java b/app/src/main/java/com/bonus/canteen/face/util/DrawHelper.java index c554c57..9eefc35 100644 --- a/app/src/main/java/com/bonus/canteen/face/util/DrawHelper.java +++ b/app/src/main/java/com/bonus/canteen/face/util/DrawHelper.java @@ -8,12 +8,13 @@ import android.hardware.Camera; import com.arcsoft.face.AgeInfo; import com.arcsoft.face.GenderInfo; -import com.arcsoft.face.LivenessInfo; +import com.bonus.canteen.face.model.DrawInfo; +import com.bonus.canteen.face.widget.FaceRectView; import java.util.List; /** - * 绘制人脸框帮助类,用于在{@link FaceRectView}上绘制矩形 + * 绘制人脸框帮助类,用于在{@link com.bonus.canteen.face.widget.FaceRectView}上绘制矩形 */ public class DrawHelper { private int previewWidth, previewHeight, canvasWidth, canvasHeight, cameraDisplayOrientation, cameraId; @@ -211,12 +212,9 @@ public class DrawHelper { if (drawInfo.getName() == null) { paint.setStyle(Paint.Style.FILL_AND_STROKE); paint.setTextSize(rect.width() / 8); - String str = (drawInfo.getSex() == GenderInfo.MALE ? "MALE" : (drawInfo.getSex() == GenderInfo.FEMALE ? "FEMALE" : "UNKNOWN")) + "," - + (drawInfo.getAge() == AgeInfo.UNKNOWN_AGE ? "UNKNOWN" : drawInfo.getAge()) - + "," - + (drawInfo.getLiveness() == LivenessInfo.ALIVE ? "ALIVE" : (drawInfo.getLiveness() == LivenessInfo.NOT_ALIVE ? "NOT_ALIVE" : "UNKNOWN")); + + (drawInfo.getAge() == AgeInfo.UNKNOWN_AGE ? "UNKNOWN" : drawInfo.getAge()); canvas.drawText(str, rect.left, rect.top - 10, paint); } else { paint.setStyle(Paint.Style.FILL_AND_STROKE); @@ -296,5 +294,4 @@ public class DrawHelper { public void setMirrorVertical(boolean mirrorVertical) { this.mirrorVertical = mirrorVertical; } - } diff --git a/app/src/main/java/com/bonus/canteen/face/util/FacePreviewInfo.java b/app/src/main/java/com/bonus/canteen/face/util/FacePreviewInfo.java index 195c504..1457b29 100644 --- a/app/src/main/java/com/bonus/canteen/face/util/FacePreviewInfo.java +++ b/app/src/main/java/com/bonus/canteen/face/util/FacePreviewInfo.java @@ -28,4 +28,11 @@ public class FacePreviewInfo { this.trackId = trackId; } + @Override + public String toString() { + return "FacePreviewInfo{" + + "faceInfo=" + faceInfo + + ", trackId=" + trackId + + '}'; + } } diff --git a/app/src/main/java/com/bonus/canteen/face/util/FaceSearchResultAdapter.java b/app/src/main/java/com/bonus/canteen/face/util/FaceSearchResultAdapter.java new file mode 100644 index 0000000..5148372 --- /dev/null +++ b/app/src/main/java/com/bonus/canteen/face/util/FaceSearchResultAdapter.java @@ -0,0 +1,65 @@ +package com.bonus.canteen.face.util; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import com.bumptech.glide.Glide; + +import org.easydarwin.easypusher.R; + +import java.util.List; + +public class FaceSearchResultAdapter extends RecyclerView.Adapter { + private List compareResultList; + private LayoutInflater inflater; + + public FaceSearchResultAdapter(List compareResultList, Context context) { + inflater = LayoutInflater.from(context); + this.compareResultList = compareResultList; + } + + @NonNull + @Override + public CompareResultHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View itemView = inflater.inflate(R.layout.recycler_item_search_result, null, false); + CompareResultHolder compareResultHolder = new CompareResultHolder(itemView); + compareResultHolder.textView = itemView.findViewById(R.id.tv_item_name); + compareResultHolder.imageView = itemView.findViewById(R.id.iv_item_head_img); + return compareResultHolder; + } + + @Override + public void onBindViewHolder(@NonNull CompareResultHolder holder, int position) { + if (compareResultList == null) { + return; + } +// File imgFile = new File(FaceServer.ROOT_PATH + File.separator + FaceServer.SAVE_IMG_DIR + File.separator + compareResultList.get(position).getUserName() + FaceServer.IMG_SUFFIX); + String path = compareResultList.get(position).getFacePath(); + Glide.with(holder.imageView) + .load(path) + .into(holder.imageView); + holder.textView.setText(compareResultList.get(position).getUserName()); + } + + @Override + public int getItemCount() { + return compareResultList == null ? 0 : compareResultList.size(); + } + + class CompareResultHolder extends RecyclerView.ViewHolder { + + TextView textView; + ImageView imageView; + + CompareResultHolder(@NonNull View itemView) { + super(itemView); + } + } +} diff --git a/app/src/main/java/com/bonus/canteen/face/util/FaceServer.java b/app/src/main/java/com/bonus/canteen/face/util/FaceServer.java index 80fec65..7b2b311 100644 --- a/app/src/main/java/com/bonus/canteen/face/util/FaceServer.java +++ b/app/src/main/java/com/bonus/canteen/face/util/FaceServer.java @@ -20,6 +20,7 @@ import com.bonus.canteen.adapter.menu.utils.StringHelper; import com.bonus.canteen.adapter.menu.utils.WorkConfig; import com.bonus.canteen.face.UserFeatureManager; import com.bonus.canteen.face.entity.BasePersonBean; +import com.bonus.canteen.face.entity.FaceRegisterInfo; import java.io.File; import java.util.ArrayList; diff --git a/app/src/main/java/com/bonus/canteen/face/util/RecognizeColor.java b/app/src/main/java/com/bonus/canteen/face/util/RecognizeColor.java new file mode 100644 index 0000000..bf271a1 --- /dev/null +++ b/app/src/main/java/com/bonus/canteen/face/util/RecognizeColor.java @@ -0,0 +1,22 @@ +package com.bonus.canteen.face.util; + +import android.graphics.Color; + +/** + * 识别过程中人脸框的颜色 + */ +public class RecognizeColor { + /** + * 未知情况的颜色 + */ + public static final int COLOR_UNKNOWN = Color.YELLOW; + /** + * 成功的颜色 + */ + public static final int COLOR_SUCCESS = Color.GREEN; + /** + * 失败的颜色 + */ + public static final int COLOR_FAILED = Color.RED; + +} diff --git a/app/src/main/java/com/bonus/canteen/face/util/FaceRectView.java b/app/src/main/java/com/bonus/canteen/face/widget/FaceRectView.java similarity index 91% rename from app/src/main/java/com/bonus/canteen/face/util/FaceRectView.java rename to app/src/main/java/com/bonus/canteen/face/widget/FaceRectView.java index ea8d1c6..a9ba198 100644 --- a/app/src/main/java/com/bonus/canteen/face/util/FaceRectView.java +++ b/app/src/main/java/com/bonus/canteen/face/widget/FaceRectView.java @@ -1,11 +1,16 @@ -package com.bonus.canteen.face.util; +package com.bonus.canteen.face.widget; import android.content.Context; import android.graphics.Canvas; import android.graphics.Paint; import android.util.AttributeSet; import android.view.View; + import androidx.annotation.Nullable; + +import com.bonus.canteen.face.model.DrawInfo; +import com.bonus.canteen.face.util.DrawHelper; + import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; diff --git a/app/src/main/java/com/bonus/canteen/presentation/MyPresentation.java b/app/src/main/java/com/bonus/canteen/presentation/MyPresentation.java index f35974d..c7ecc48 100644 --- a/app/src/main/java/com/bonus/canteen/presentation/MyPresentation.java +++ b/app/src/main/java/com/bonus/canteen/presentation/MyPresentation.java @@ -17,41 +17,68 @@ package com.bonus.canteen.presentation; -import static com.arcsoft.face.enums.DetectFaceOrientPriority.ASF_OP_ALL_OUT; - +import android.Manifest; import android.app.Presentation; import android.content.Context; +import android.graphics.Point; import android.hardware.Camera; import android.os.Build; import android.os.Bundle; import android.util.Log; import android.view.Display; -import android.view.SurfaceView; +import android.view.Surface; import android.view.View; +import android.view.Window; import android.view.WindowManager; import android.widget.ListView; +import androidx.annotation.Nullable; + +import com.arcsoft.face.AgeInfo; import com.arcsoft.face.ErrorInfo; import com.arcsoft.face.FaceEngine; +import com.arcsoft.face.FaceFeature; +import com.arcsoft.face.FaceInfo; +import com.arcsoft.face.GenderInfo; +import com.arcsoft.face.LivenessInfo; import com.arcsoft.face.enums.DetectFaceOrientPriority; import com.arcsoft.face.enums.DetectMode; import com.bonus.canteen.adapter.menu.PayMenuAdapter; import com.bonus.canteen.adapter.menu.entity.SalesMenuEntity; +import com.bonus.canteen.face.model.DrawInfo; import com.bonus.canteen.face.util.CameraHelper; +import com.bonus.canteen.face.util.CameraListener; import com.bonus.canteen.face.util.CompareResult; import com.bonus.canteen.face.util.ConfigUtil; import com.bonus.canteen.face.util.DrawHelper; import com.bonus.canteen.face.util.FaceHelper; -import com.bonus.canteen.face.util.FaceRectView; +import com.bonus.canteen.face.util.FaceListener; +import com.bonus.canteen.face.util.FacePreviewInfo; +import com.bonus.canteen.face.util.FaceSearchResultAdapter; +import com.bonus.canteen.face.util.FaceServer; +import com.bonus.canteen.face.util.LivenessType; +import com.bonus.canteen.face.util.RecognizeColor; +import com.bonus.canteen.face.util.RequestFeatureStatus; +import com.bonus.canteen.face.util.RequestLivenessStatus; +import com.bonus.canteen.face.widget.FaceRectView; +import com.hjq.permissions.OnPermissionCallback; +import com.hjq.permissions.Permission; +import com.hjq.permissions.XXPermissions; import com.xuexiang.xui.utils.XToastUtils; import org.easydarwin.easypusher.R; import java.util.ArrayList; +import java.util.Enumeration; import java.util.List; +import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; +import io.reactivex.Observable; +import io.reactivex.Observer; import io.reactivex.disposables.CompositeDisposable; +import io.reactivex.disposables.Disposable; public class MyPresentation extends Presentation { @@ -60,41 +87,35 @@ public class MyPresentation extends Presentation { private Context context; private Display display; - /** - * VIDEO模式人脸检测引擎,用于预览帧人脸追踪 - */ - private FaceEngine ftEngine; - /** - * 用于特征提取的引擎 - */ - private FaceEngine frEngine; - /** - * IMAGE模式活体检测引擎,用于预览帧人脸活体检测 - */ - private FaceEngine flEngine; + List salesMenuEntityList = new ArrayList<>(); + PayMenuAdapter menuAdapter; - private int ftInitCode = -1; - private int frInitCode = -1; - private int flInitCode = -1; + ListView mListView; - private static final int MAX_DETECT_NUM = 10; + private CameraHelper cameraHelper; + private DrawHelper drawHelper; + private Camera.Size previewSize; + private Integer rgbCameraId = Camera.CameraInfo.CAMERA_FACING_FRONT; + private int afCode = -1; + private int processMask = FaceEngine.ASF_AGE | FaceEngine.ASF_FACE3DANGLE | FaceEngine.ASF_GENDER | FaceEngine.ASF_LIVENESS; /** - * 当FR成功,活体未成功时,FR等待活体的时间 + * 相机预览显示的控件,可为SurfaceView或TextureView */ - private static final int WAIT_LIVENESS_INTERVAL = 100; + private View previewView; + private FaceRectView faceRectView; + /** - * 失败重试间隔时间(ms) + * 所需的所有权限信息 */ - private static final long FAIL_RETRY_INTERVAL = 1000; - /** - * 出错重试最大次数 - */ - private static final int MAX_RETRY_TIME = 3; - private List compareResultList; + private static final String[] NEEDED_PERMISSIONS = new String[]{ + Manifest.permission.CAMERA, + Manifest.permission.READ_PHONE_STATE + }; /** * 用于存储活体值 */ private ConcurrentHashMap livenessMap = new ConcurrentHashMap<>(); + /** * 活体检测的开关 */ @@ -107,41 +128,45 @@ public class MyPresentation extends Presentation { * 用于记录人脸特征提取出错重试次数 */ private ConcurrentHashMap extractErrorRetryMap = new ConcurrentHashMap<>(); - private CompositeDisposable getFeatureDelayedDisposables = new CompositeDisposable(); - private CompositeDisposable delayFaceTaskCompositeDisposable = new CompositeDisposable(); /** * 用于存储活体检测出错重试次数 */ private ConcurrentHashMap livenessErrorRetryMap = new ConcurrentHashMap<>(); + /** + * 当FR成功,活体未成功时,FR等待活体的时间 + */ + private static final int WAIT_LIVENESS_INTERVAL = 100; + /** + * 失败重试间隔时间(ms) + */ + private static final long FAIL_RETRY_INTERVAL = 1000; + /** + * 出错重试最大次数 + */ + private static final int MAX_RETRY_TIME = 3; + /** + * VIDEO模式人脸检测引擎,用于预览帧人脸追踪 + */ + private FaceEngine ftEngine; + /** + * 用于特征提取的引擎 + */ + private FaceEngine frEngine; + /** + * IMAGE模式活体检测引擎,用于预览帧人脸活体检测 + */ + private FaceEngine flEngine; + private int ftInitCode = -1; + private int frInitCode = -1; + private int flInitCode = -1; + private static final int MAX_DETECT_NUM = 10; private FaceHelper faceHelper; - private Camera.Size previewSize; - private CameraHelper cameraHelper; - private DrawHelper drawHelper; - /** - * 相机预览显示的控件,可为SurfaceView或TextureView - */ - private View previewView; - /** - * 绘制人脸框的控件 - */ - private FaceRectView faceRectView; - /** - * 优先打开的摄像头,本界面主要用于单目RGB摄像头设备,因此默认打开前置 - */ - private Integer rgbCameraID = Camera.CameraInfo.CAMERA_FACING_BACK; - /** - * 识别阈值 - */ - private static final float SIMILAR_THRESHOLD = 0.8F; + private CompositeDisposable getFeatureDelayedDisposables = new CompositeDisposable(); - List salesMenuEntityList = new ArrayList<>(); - PayMenuAdapter menuAdapter; - - ListView mListView; - - private SurfaceView surfaceView; - + private CompositeDisposable delayFaceTaskCompositeDisposable = new CompositeDisposable(); + private List compareResultList; + private FaceSearchResultAdapter adapter; public void setSalesData(List rawData) { salesMenuEntityList = new ArrayList<>(rawData); menuAdapter.notifyDataSetChanged(); @@ -159,38 +184,139 @@ public class MyPresentation extends Presentation { } } } - + // 获取副屏旋转方向 + public int getMyPresentationRotation() { + if (display != null) { + return display.getRotation(); + } + return Surface.ROTATION_0; // 默认值 + } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - try { - setContentView(R.layout.activity_canteen_payment); - initView(); - initEngine(); - } catch (Exception e) { - e.printStackTrace(); + setContentView(R.layout.activity_canteen_payment); + FaceServer.getInstance().loadFace(context); + // 设置副屏窗口常亮 + Window window = getWindow(); + if (window != null) { + window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); } + initView(); + checkPermissions(NEEDED_PERMISSIONS); } private void initView() { + try { + Log.d(TAG, "initView: "); + if (cameraHelper != null) { + cameraHelper.release(); + cameraHelper = null; + } + unInitEngine(); + } catch (Exception e) { + Log.e(TAG, "initView: 销毁失败 或者 无需销毁"); + } mListView = findViewById(R.id.product_list); salesMenuEntityList = new ArrayList<>(); menuAdapter = new PayMenuAdapter(context, salesMenuEntityList); mListView.setAdapter(menuAdapter); menuAdapter.notifyDataSetChanged(); + previewView = findViewById(R.id.texture_preview); + faceRectView = findViewById(R.id.face_rect_view); - surfaceView = findViewById(R.id.face_pay); } + private void checkPermissions(String[] neededPermissions) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { + XXPermissions.with(context) + .permission(Permission.READ_PHONE_STATE) + .permission(Permission.CAMERA) + .request(new OnPermissionCallback() { + @Override + public void onGranted(List permissions, boolean all) { + if (all) { + XToastUtils.success("获取权限成功"); + initModular(); + } + } + + @Override + public void onDenied(List permissions, boolean never) { + if (never) { + XToastUtils.error("被永久拒绝授权,请手动授予存储权限"); + // 如果是被永久拒绝就跳转到应用权限系统设置页面 + XXPermissions.startPermissionActivity(context, permissions); + } else { + XToastUtils.error("获取存储权限失败"); + } + } + }); + } else { + XXPermissions.with(context) + .permission(Permission.READ_PHONE_STATE) + .permission(Permission.CAMERA) + .request(new OnPermissionCallback() { + @Override + public void onGranted(List permissions, boolean all) { + if (all) { + XToastUtils.success("获取权限成功"); + initModular(); + } + } + + @Override + public void onDenied(List permissions, boolean never) { + if (never) { + XToastUtils.error("被永久拒绝授权,请手动授予存储权限"); + // 如果是被永久拒绝就跳转到应用权限系统设置页面 + XXPermissions.startPermissionActivity(context, permissions); + } else { + XToastUtils.error("获取存储权限失败"); + } + } + }); + } + } + + public static final String APP_ID = "52XE2dQBtdmMsfDMvyKmPCCPyFsc4jvo8TKvAdaYfr28"; + public static final String SDK_KEY = "9YFPa6eiuNQAFnzJUadn4LaR8w1bcw3a5ZWYZB6FB57Y"; + + public void initModular() { + new Thread(new Runnable() { + @Override + public void run() { + activeFaceEngine(); + } + }).start(); + } + + private void activeFaceEngine() { + int code = FaceEngine.activeOnline(context, APP_ID, SDK_KEY); + if (code == ErrorInfo.MOK) { + Log.i(TAG, "activeOnline success"); + initEngine(); + initCamera(); +// if (cameraHelper != null) { +// boolean success = cameraHelper.switchCamera(); +// if (!success) { +// Log.e(TAG, "切换失败"); +// } else { +// Log.i(TAG, "切换成功"); +// } +// } else { +// Log.e(TAG, "cameraHelper 为空"); +// } + } else if (code == ErrorInfo.MERR_ASF_ALREADY_ACTIVATED) { + Log.i(TAG, "already activated"); + } else { + Log.i(TAG, "activeOnline failed, code is : " + code); + } + } - /** - * 初始化引擎 - */ private void initEngine() { - ConfigUtil.setFtOrient(context, ASF_OP_ALL_OUT); ftEngine = new FaceEngine(); - ftInitCode = ftEngine.init(context, DetectMode.ASF_DETECT_MODE_VIDEO, ConfigUtil.getFtOrient(context), + ftInitCode = ftEngine.init(context, DetectMode.ASF_DETECT_MODE_VIDEO, DetectFaceOrientPriority.ASF_OP_0_ONLY, 16, MAX_DETECT_NUM, FaceEngine.ASF_FACE_DETECT); frEngine = new FaceEngine(); @@ -201,24 +327,458 @@ public class MyPresentation extends Presentation { flInitCode = flEngine.init(context, DetectMode.ASF_DETECT_MODE_IMAGE, DetectFaceOrientPriority.ASF_OP_0_ONLY, 16, MAX_DETECT_NUM, FaceEngine.ASF_LIVENESS); - Log.i(TAG, "initEngine: init: " + ftInitCode); + Log.i(TAG, "initEngine: ftInitCode: " + ftInitCode); + Log.i(TAG, "initEngine: frInitCode: " + frInitCode); + Log.i(TAG, "initEngine: flInitCode: " + flInitCode); if (ftInitCode != ErrorInfo.MOK) { - String error = "初始化失败,错误码为:" + ftInitCode; + String error = "初始化失败,错误码为:"+ ftInitCode; Log.i(TAG, "initEngine: " + error); - XToastUtils.error(error); } if (frInitCode != ErrorInfo.MOK) { - String error = "初始化失败,错误码为:" + frInitCode; + String error = "初始化失败,错误码为:"+ frInitCode; Log.i(TAG, "initEngine: " + error); - XToastUtils.error(error); } if (flInitCode != ErrorInfo.MOK) { - String error = "初始化失败,错误码为:" + flInitCode; + String error = "初始化失败,错误码为:"+ flInitCode; Log.i(TAG, "initEngine: " + error); - XToastUtils.error(error); } } + private void unInitEngine() { + + if (afCode == 0) { + afCode = ftEngine.unInit(); + afCode = frEngine.unInit(); + afCode = flEngine.unInit(); + Log.i(TAG, "unInitEngine: " + afCode); + } + } + + private void initCamera() { + Log.i(TAG, "initCamera: 开始初始化"); + + final FaceListener faceListener = new FaceListener() { + @Override + public void onFail(Exception e) { + Log.e(TAG, "onFail: " + e.getMessage()); + } + + @Override + public void onFaceFeatureInfoGet(@Nullable byte[] nv21Data, @Nullable final FaceFeature faceFeature, Integer requestId, Integer errorCode, FaceInfo faceInfo) { + if (faceFeature != null) { + Log.i(TAG, "onPreview: fr end = " + System.currentTimeMillis() + " trackId = " + requestId); + Integer liveness = livenessMap.get(requestId); + Log.e(TAG, "liveness " +liveness); + if (!livenessDetect) { + //不做活体检测的情况,直接搜索 +// searchFace(faceFeature, requestId); + Log.i(TAG, "不做活体检测的情况,直接搜索"); + } else if (liveness != null && liveness == LivenessInfo.ALIVE) { + //活体检测通过,搜索特征 +// searchFace(faceFeature, requestId); + Log.i(TAG, "活体检测通过,搜索特征"); + } else { + Log.i(TAG, "活体检测未出结果,或者非活体,延迟执行该函数"); + //活体检测未出结果,或者非活体,延迟执行该函数 + if (requestFeatureStatusMap.containsKey(requestId)) { + Log.i(TAG, "活体检测未出结果,或者非活体,延迟执行该函数requestFeatureStatusMap.containsKey(requestId)"); + Observable.timer(WAIT_LIVENESS_INTERVAL, TimeUnit.MILLISECONDS) + .subscribe(new Observer() { + Disposable disposable; + + @Override + public void onSubscribe(Disposable d) { + Log.i(TAG, "活体检测未出结果,或者非活体,延迟执行该函数onSubscribe"); + disposable = d; + getFeatureDelayedDisposables.add(disposable); + } + + @Override + public void onNext(Long aLong) { + Log.i(TAG, "活体检测未出结果,或者非活体,延迟执行该函数onNext"); + onFaceFeatureInfoGet(nv21Data,faceFeature, requestId, errorCode,faceInfo); + } + + @Override + public void onError(Throwable e) { + Log.e(TAG, "onFaceFeatureInfoGet onError: fr error"); + } + + @Override + public void onComplete() { + Log.i(TAG, "活体检测未出结果,或者非活体,延迟执行该函数onComplete"); + getFeatureDelayedDisposables.remove(disposable); + } + }); + } + } + + } else { + //特征提取失败 + Log.e(TAG, "onFaceFeatureInfoGet: fr error 特征提取失败"); + if (increaseAndGetValue(extractErrorRetryMap, requestId) > MAX_RETRY_TIME) { + Log.e(TAG, "onFaceFeatureInfoGet: fr error 特征提取失败次数超过最大次数"); + extractErrorRetryMap.put(requestId, 0); + + String msg; + // 传入的FaceInfo在指定的图像上无法解析人脸,此处使用的是RGB人脸数据,一般是人脸模糊 + if (errorCode != null && errorCode == ErrorInfo.MERR_FSDK_FACEFEATURE_LOW_CONFIDENCE_LEVEL) { + msg = "人脸置信度低"; + } else { + msg = "ExtractCode:" + errorCode; + } + Log.e(TAG, "onFaceFeatureInfoGet: fr error msg" + msg); + faceHelper.setName(requestId, "未通过:" + msg); + // 在尝试最大次数后,特征提取仍然失败,则认为识别未通过 + requestFeatureStatusMap.put(requestId, RequestFeatureStatus.FAILED); + retryRecognizeDelayed(requestId); + } else { + Log.e(TAG, "onFaceFeatureInfoGet: fr error 特征提取失败次数未超过最大次数"); + requestFeatureStatusMap.put(requestId, RequestFeatureStatus.TO_RETRY); + } + } + } + + @Override + public void onFaceLivenessInfoGet(@Nullable LivenessInfo livenessInfo, final Integer requestId, Integer errorCode) { + if (livenessInfo != null) { + int liveness = livenessInfo.getLiveness(); + Log.e(TAG, "onFaceLivenessInfoGet: liveness " + liveness); + livenessMap.put(requestId, liveness); + // 非活体,重试 + if (liveness == LivenessInfo.NOT_ALIVE) { + Log.e(TAG, "onFaceLivenessInfoGet: liveness NOT_ALIVE" + requestId); + faceHelper.setName(requestId, "未通过:NOT_ALIVE"); + // 延迟 FAIL_RETRY_INTERVAL 后,将该人脸状态置为UNKNOWN,帧回调处理时会重新进行活体检测 + retryLivenessDetectDelayed(requestId); + } + } else { + if (increaseAndGetValue(livenessErrorRetryMap, requestId) > MAX_RETRY_TIME) { + Log.e(TAG, "onFaceLivenessInfoGet: liveness error LivenessInfo.NOT_ALIVE"); + livenessErrorRetryMap.put(requestId, 0); + String msg; + // 传入的FaceInfo在指定的图像上无法解析人脸,此处使用的是RGB人脸数据,一般是人脸模糊 + if (errorCode != null && errorCode == ErrorInfo.MERR_FSDK_FACEFEATURE_LOW_CONFIDENCE_LEVEL) { + msg = "人脸置信度低"; + } else { + msg = "ProcessCode:" + errorCode; + } + Log.e(TAG, "onFaceLivenessInfoGet: liveness error msg" + msg); + faceHelper.setName(requestId, "未通过:" + msg); + retryLivenessDetectDelayed(requestId); + } else { + Log.e(TAG, "onFaceLivenessInfoGet: liveness error LivenessInfo.NOT_ALIVE"); + livenessMap.put(requestId, LivenessInfo.UNKNOWN); + } + } + } + + }; + + + CameraListener cameraListener = new CameraListener() { + @Override + public void onCameraOpened(Camera camera, int cameraId, int displayOrientation, boolean isMirror) { + Log.i(TAG, "onCameraOpened: " + cameraId + " " + displayOrientation + " " + isMirror); + Camera.Size lastPreviewSize = previewSize; + previewSize = camera.getParameters().getPreviewSize(); + drawHelper = new DrawHelper(previewSize.width, previewSize.height, previewView.getWidth(), previewView.getHeight(), displayOrientation + , cameraId, isMirror, false, false); + + // 切换相机的时候可能会导致预览尺寸发生变化 + if (faceHelper == null || + lastPreviewSize == null || + lastPreviewSize.width != previewSize.width || lastPreviewSize.height != previewSize.height) { + Log.i(TAG, "onCameraOpened: previewSize changed"); + faceHelper = new FaceHelper.Builder() + .ftEngine(ftEngine) + .frEngine(frEngine) + .flEngine(flEngine) + .frQueueSize(MAX_DETECT_NUM) + .flQueueSize(MAX_DETECT_NUM) + .previewSize(previewSize) + .faceListener(faceListener) + .trackedFaceCount(ConfigUtil.getTrackedFaceCount(context)) + .build(); + Log.i(TAG, "onCameraOpened: trackedFaceCount jieshu"); + }else{ + Log.i(TAG, "onCameraOpened: trackedFaceCount 切换相机的时候可能会导致预览尺寸发生变化" ); + } + } + + + @Override + public void onPreview(byte[] nv21, Camera camera) { + + if (faceRectView != null) { + faceRectView.clearFaceInfo(); + } + List facePreviewInfoList = faceHelper.onPreviewFrame(nv21); + if (facePreviewInfoList != null && faceRectView != null && drawHelper != null) { + drawPreviewInfo(facePreviewInfoList); + } + clearLeftFace(facePreviewInfoList); + Log.i(TAG, "onPreview: " + facePreviewInfoList.size()); + if (facePreviewInfoList != null && facePreviewInfoList.size() > 0 && previewSize != null) { + for (int i = 0; i < facePreviewInfoList.size(); i++) { + Integer status = requestFeatureStatusMap.get(facePreviewInfoList.get(i).getTrackId()); + Log.e(TAG, "onPreview: status " + status); + /** + * 在活体检测开启,在人脸识别状态不为成功或人脸活体状态不为处理中(ANALYZING)且不为处理完成(ALIVE、NOT_ALIVE)时重新进行活体检测 + */ + if (livenessDetect && (status == null || status != RequestFeatureStatus.SUCCEED)) { + Integer liveness = livenessMap.get(facePreviewInfoList.get(i).getTrackId()); + Log.e(TAG, "onPreview: liveness " + liveness); + if (liveness == null + || (liveness != LivenessInfo.ALIVE && liveness != LivenessInfo.NOT_ALIVE && liveness != RequestLivenessStatus.ANALYZING)) { + livenessMap.put(facePreviewInfoList.get(i).getTrackId(), RequestLivenessStatus.ANALYZING); + faceHelper.requestFaceLiveness(nv21, facePreviewInfoList.get(i).getFaceInfo(), previewSize.width, previewSize.height, FaceEngine.CP_PAF_NV21, facePreviewInfoList.get(i).getTrackId(), LivenessType.RGB); + } + } + /** + * 对于每个人脸,若状态为空或者为失败,则请求特征提取(可根据需要添加其他判断以限制特征提取次数), + * 特征提取回传的人脸特征结果在{@link FaceListener#onFaceFeatureInfoGet(FaceFeature, Integer, Integer)}中回传 + */ + if (status == null + || status == RequestFeatureStatus.TO_RETRY) { + requestFeatureStatusMap.put(facePreviewInfoList.get(i).getTrackId(), RequestFeatureStatus.SEARCHING); + faceHelper.requestFaceFeature(nv21, facePreviewInfoList.get(i).getFaceInfo(), previewSize.width, previewSize.height, FaceEngine.CP_PAF_NV21, facePreviewInfoList.get(i).getTrackId()); + } + } + } +// List faceInfoList = new ArrayList<>(); +// +// int code = ftEngine.detectFaces(nv21, previewSize.width, previewSize.height, FaceEngine.CP_PAF_NV21, faceInfoList); +// Log.i(TAG, " faceInfoList.szie:" + faceInfoList.size()); +// if (code == ErrorInfo.MOK && faceInfoList.size() > 0) { +// code = ftEngine.process(nv21, previewSize.width, previewSize.height, FaceEngine.CP_PAF_NV21, faceInfoList, processMask); +// if (code != ErrorInfo.MOK) { +// return; +// } +// } else { +// return; +// } +// Log.i(TAG, "faceEngine.process:" + code); +// List ageInfoList = new ArrayList<>(); +// List genderInfoList = new ArrayList<>(); +// List face3DAngleList = new ArrayList<>(); +// List faceLivenessInfoList = new ArrayList<>(); +// int ageCode = ftEngine.getAge(ageInfoList); +// int genderCode = ftEngine.getGender(genderInfoList); +// int face3DAngleCode = ftEngine.getFace3DAngle(face3DAngleList); +// int livenessCode = ftEngine.getLiveness(faceLivenessInfoList); +// Log.i(TAG, "onPreview: " + ageCode + " " + genderCode + " " + face3DAngleCode + " " + livenessCode); +// // 有其中一个的错误码不为ErrorInfo.MOK,return +// if ((ageCode | genderCode | face3DAngleCode | livenessCode) != ErrorInfo.MOK) { +// return; +// } +// if (faceRectView != null && drawHelper != null) { +// List drawInfoList = new ArrayList<>(); +// for (int i = 0; i < faceInfoList.size(); i++) { +// drawInfoList.add(new DrawInfo(drawHelper.adjustRect(faceInfoList.get(i).getRect()), genderInfoList.get(i).getGender(), ageInfoList.get(i).getAge(), faceLivenessInfoList.get(i).getLiveness(), RecognizeColor.COLOR_UNKNOWN, null)); +// } +// drawHelper.draw(faceRectView, drawInfoList); +// } + } + + @Override + public void onCameraClosed() { + Log.i(TAG, "onCameraClosed: "); + } + + @Override + public void onCameraError(Exception e) { + Log.i(TAG, "onCameraError: " + e.getMessage()); + } + + @Override + public void onCameraConfigurationChanged(int cameraID, int displayOrientation) { + if (drawHelper != null) { + drawHelper.setCameraDisplayOrientation(displayOrientation); + } + Log.i(TAG, "onCameraConfigurationChanged: " + cameraID + " " + displayOrientation); + } + }; + cameraHelper = new CameraHelper.Builder() + .previewViewSize(new Point(previewView.getMeasuredWidth(), previewView.getMeasuredHeight())) + .rotation(getMyPresentationRotation()) + .isMirror(false) + .previewOn(previewView) + .cameraListener(cameraListener) + .specificCameraId(0) + .build(); + cameraHelper.init(); + cameraHelper.start(); + } + + /** + * 将map中key对应的value增1回传 + * + * @param countMap map + * @param key key + * @return 增1后的value + */ + public int increaseAndGetValue(Map countMap, int key) { + if (countMap == null) { + return 0; + } + Integer value = countMap.get(key); + if (value == null) { + value = 0; + } + countMap.put(key, ++value); + return value; + } + + /** + * 延迟 FAIL_RETRY_INTERVAL 重新进行活体检测 + * + * @param requestId 人脸ID + */ + private void retryLivenessDetectDelayed(final Integer requestId) { + Observable.timer(FAIL_RETRY_INTERVAL, TimeUnit.MILLISECONDS) + .subscribe(new Observer() { + Disposable disposable; + + @Override + public void onSubscribe(Disposable d) { + disposable = d; + delayFaceTaskCompositeDisposable.add(disposable); + } + + @Override + public void onNext(Long aLong) { + + } + + @Override + public void onError(Throwable e) { + e.printStackTrace(); + } + + @Override + public void onComplete() { + // 将该人脸状态置为UNKNOWN,帧回调处理时会重新进行活体检测 + if (livenessDetect) { + faceHelper.setName(requestId, Integer.toString(requestId)); + } + livenessMap.put(requestId, LivenessInfo.UNKNOWN); + delayFaceTaskCompositeDisposable.remove(disposable); + } + }); + } + + /** + * 延迟 FAIL_RETRY_INTERVAL 重新进行人脸识别 + * + * @param requestId 人脸ID + */ + private void retryRecognizeDelayed(final Integer requestId) { + requestFeatureStatusMap.put(requestId, RequestFeatureStatus.FAILED); + Observable.timer(FAIL_RETRY_INTERVAL, TimeUnit.MILLISECONDS) + .subscribe(new Observer() { + Disposable disposable; + + @Override + public void onSubscribe(Disposable d) { + disposable = d; + delayFaceTaskCompositeDisposable.add(disposable); + } + + @Override + public void onNext(Long aLong) { + + } + + @Override + public void onError(Throwable e) { + e.printStackTrace(); + } + + @Override + public void onComplete() { + // 将该人脸特征提取状态置为FAILED,帧回调处理时会重新进行活体检测 + faceHelper.setName(requestId, Integer.toString(requestId)); + requestFeatureStatusMap.put(requestId, RequestFeatureStatus.TO_RETRY); + delayFaceTaskCompositeDisposable.remove(disposable); + } + }); + } + + private void drawPreviewInfo(List facePreviewInfoList) { + List drawInfoList = new ArrayList<>(); + for (int i = 0; i < facePreviewInfoList.size(); i++) { + String name = faceHelper.getName(facePreviewInfoList.get(i).getTrackId()); + Integer liveness = livenessMap.get(facePreviewInfoList.get(i).getTrackId()); + Integer recognizeStatus = requestFeatureStatusMap.get(facePreviewInfoList.get(i).getTrackId()); + + // 根据识别结果和活体结果设置颜色 + int color = RecognizeColor.COLOR_UNKNOWN; + if (recognizeStatus != null) { + if (recognizeStatus == RequestFeatureStatus.FAILED) { + color = RecognizeColor.COLOR_FAILED; + } + if (recognizeStatus == RequestFeatureStatus.SUCCEED) { + color = RecognizeColor.COLOR_SUCCESS; + } + } + if (liveness != null && liveness == LivenessInfo.NOT_ALIVE) { + color = RecognizeColor.COLOR_FAILED; + } + + drawInfoList.add(new DrawInfo(drawHelper.adjustRect(facePreviewInfoList.get(i).getFaceInfo().getRect()), + GenderInfo.UNKNOWN, AgeInfo.UNKNOWN_AGE, liveness == null ? LivenessInfo.UNKNOWN : liveness, color, + name == null ? String.valueOf(facePreviewInfoList.get(i).getTrackId()) : name)); + } + drawHelper.draw(faceRectView, drawInfoList); + } + + + + /** + * 删除已经离开的人脸 + * + * @param facePreviewInfoList 人脸和trackId列表 + */ + private void clearLeftFace(List facePreviewInfoList) { + if (compareResultList != null) { + for (int i = compareResultList.size() - 1; i >= 0; i--) { + if (!requestFeatureStatusMap.containsKey(compareResultList.get(i).getTrackId())) { + compareResultList.remove(i); + adapter.notifyItemRemoved(i); + } + } + } + if (facePreviewInfoList == null || facePreviewInfoList.size() == 0) { + requestFeatureStatusMap.clear(); + livenessMap.clear(); + livenessErrorRetryMap.clear(); + extractErrorRetryMap.clear(); + if (getFeatureDelayedDisposables != null) { + getFeatureDelayedDisposables.clear(); + } + return; + } + Enumeration keys = requestFeatureStatusMap.keys(); + while (keys.hasMoreElements()) { + int key = keys.nextElement(); + boolean contained = false; + for (FacePreviewInfo facePreviewInfo : facePreviewInfoList) { + if (facePreviewInfo.getTrackId() == key) { + contained = true; + break; + } + } + if (!contained) { + requestFeatureStatusMap.remove(key); + livenessMap.remove(key); + livenessErrorRetryMap.remove(key); + extractErrorRetryMap.remove(key); + } + } + + + } } diff --git a/app/src/main/res/drawable/bg_camera_scan_border.xml b/app/src/main/res/drawable/bg_camera_scan_border.xml new file mode 100644 index 0000000..fb9b5ac --- /dev/null +++ b/app/src/main/res/drawable/bg_camera_scan_border.xml @@ -0,0 +1,13 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/common_bg_camera_mask_layer.xml b/app/src/main/res/drawable/common_bg_camera_mask_layer.xml new file mode 100644 index 0000000..38932ef --- /dev/null +++ b/app/src/main/res/drawable/common_bg_camera_mask_layer.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_canteen_payment.xml b/app/src/main/res/layout/activity_canteen_payment.xml index 8be5fdd..16c1a56 100644 --- a/app/src/main/res/layout/activity_canteen_payment.xml +++ b/app/src/main/res/layout/activity_canteen_payment.xml @@ -1,5 +1,4 @@ - - + android:backgroundTint="#F1E9E9"> + + android:src="@drawable/ic_menu_meal" /> + - + - + - + - - - - + + + + + + + + + @@ -118,18 +124,21 @@ + android:background="@color/white" /> + + + + android:layout_marginTop="5dp" + android:orientation="vertical"> + + + + android:src="@drawable/ic_nutrition" /> + + + + android:layout_weight="1" + android:gravity="center" + android:orientation="vertical"> + + + android:layout_weight="1" + android:gravity="center" + android:orientation="vertical"> + + + + android:layout_weight="1" + android:gravity="center" + android:orientation="vertical"> + + + + android:layout_weight="1" + android:gravity="center" + android:orientation="vertical"> + + + + android:layout_weight="1" + android:gravity="center" + android:orientation="vertical"> + + + android:padding="20dp"> - + + + + + @@ -344,13 +380,26 @@ android:layout_height="wrap_content" android:src="@drawable/ic_qrcode_scan" /> - + + + + @@ -366,14 +415,26 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@drawable/ic_card_scan" /> - - + + + + diff --git a/app/src/main/res/layout/recycler_item_search_result.xml b/app/src/main/res/layout/recycler_item_search_result.xml new file mode 100644 index 0000000..bdb6332 --- /dev/null +++ b/app/src/main/res/layout/recycler_item_search_result.xml @@ -0,0 +1,22 @@ + + + + + + + +