Преглед изворни кода

refactor(更新) :
- 指纹和人脸完成

周文健 пре 4 месеци
родитељ
комит
dd063e450e

+ 1 - 1
app/src/main/java/com/grkj/iscs_mars/model/vo/characteristic/CharacteristicPageRespVO.kt

@@ -25,7 +25,7 @@ data class CharacteristicPageRespVO(
 
         val imagePath: String?,
 
-        val group: String?,
+        val groupCode: String?,
 
         val orderNum: Int?,
 

+ 46 - 43
app/src/main/java/com/grkj/iscs_mars/util/ArcSoftUtil.kt

@@ -25,6 +25,7 @@ import com.grkj.iscs_mars.util.log.LogUtil
 import com.grkj.iscs_mars.view.activity.test.face.arcsoft.CameraHelper
 import com.grkj.iscs_mars.view.activity.test.face.arcsoft.CameraListener
 import com.grkj.iscs_mars.view.activity.test.face.arcsoft.NV21ToBitmap
+import com.grkj.iscs_mars.view.widget.FaceOverlayView
 
 object ArcSoftUtil {
     private var cameraHelper: CameraHelper? = null
@@ -79,10 +80,10 @@ object ArcSoftUtil {
         afCode = faceEngine!!.init(
             context,
             DetectMode.ASF_DETECT_MODE_VIDEO,
-            DetectFaceOrientPriority.valueOf("ASF_OP_0_ONLY"),
+            DetectFaceOrientPriority.ASF_OP_0_ONLY,
             16,
             1,
-            FaceEngine.ASF_FACE_DETECT or FaceEngine.ASF_AGE or FaceEngine.ASF_FACE3DANGLE or FaceEngine.ASF_GENDER or FaceEngine.ASF_LIVENESS
+            FaceEngine.ASF_FACE_DETECT or FaceEngine.ASF_AGE or FaceEngine.ASF_FACE3DANGLE or FaceEngine.ASF_GENDER or FaceEngine.ASF_LIVENESS or FaceEngine.ASF_FACE_RECOGNITION
         )
         LogUtil.i("initEngine:  init: $afCode")
         if (afCode != ErrorInfo.MOK) {
@@ -97,11 +98,11 @@ object ArcSoftUtil {
         }
     }
 
-    @JvmOverloads
     fun initCamera(
         context: Context,
         windowManager: WindowManager,
         preview: View,
+        faceOverlayView: FaceOverlayView?,
         needCheckCenter: Boolean = false,
         callBack: (Bitmap?, Int, Boolean) -> Unit
     ) {
@@ -117,6 +118,7 @@ object ArcSoftUtil {
             ) {
                 LogUtil.i("onCameraOpened: $cameraId  $displayOrientation $isMirror")
                 previewSize = camera.parameters.previewSize
+                faceOverlayView?.setCameraPreviewSize(previewSize!!.width, previewSize!!.height)
             }
 
 
@@ -129,52 +131,53 @@ object ArcSoftUtil {
                     FaceEngine.CP_PAF_NV21,
                     faceInfoList
                 )
-                if (code == ErrorInfo.MOK && faceInfoList.isNotEmpty()) {
-                    code = faceEngine!!.process(
-                        nv21,
+                faceOverlayView?.setFaceRect(faceInfoList.map { it.rect })
+                if (!needCheckCenter || (faceInfoList[0].rect.isInCenterArea(
                         previewSize!!.width,
-                        previewSize!!.height,
-                        FaceEngine.CP_PAF_NV21,
-                        faceInfoList,
-                        processMask
-                    )
-                    if (code != ErrorInfo.MOK) {
+                        previewSize!!.height
+                    ))
+                ) {
+                    if (code == ErrorInfo.MOK && faceInfoList.isNotEmpty()) {
+                        code = faceEngine!!.process(
+                            nv21,
+                            previewSize!!.width,
+                            previewSize!!.height,
+                            FaceEngine.CP_PAF_NV21,
+                            faceInfoList,
+                            processMask
+                        )
+                        if (code != ErrorInfo.MOK) {
+                            return
+                        }
+                    } else {
                         return
                     }
-                } else {
-                    return
-                }
 
-                val ageInfoList: List<AgeInfo> = ArrayList()
-                val genderInfoList: List<GenderInfo> = ArrayList()
-                val face3DAngleList: List<Face3DAngle> = ArrayList()
-                val faceLivenessInfoList: List<LivenessInfo> = ArrayList()
-                val ageCode = faceEngine!!.getAge(ageInfoList)
-                val genderCode = faceEngine!!.getGender(genderInfoList)
-                val face3DAngleCode = faceEngine!!.getFace3DAngle(face3DAngleList)
-                val livenessCode = faceEngine!!.getLiveness(faceLivenessInfoList)
-
-                // 有其中一个的错误码不为ErrorInfo.MOK,return
-                if ((ageCode or genderCode or face3DAngleCode or livenessCode) != ErrorInfo.MOK) {
-                    LogUtil.d("人脸检测结果:年龄、性别、角度、获取验证失败")
-                    return
-                }
+                    val ageInfoList: List<AgeInfo> = ArrayList()
+                    val genderInfoList: List<GenderInfo> = ArrayList()
+                    val face3DAngleList: List<Face3DAngle> = ArrayList()
+                    val faceLivenessInfoList: List<LivenessInfo> = ArrayList()
+                    val ageCode = faceEngine!!.getAge(ageInfoList)
+                    val genderCode = faceEngine!!.getGender(genderInfoList)
+                    val face3DAngleCode = faceEngine!!.getFace3DAngle(face3DAngleList)
+                    val livenessCode = faceEngine!!.getLiveness(faceLivenessInfoList)
+
+                    // 有其中一个的错误码不为ErrorInfo.MOK,return
+                    if ((ageCode or genderCode or face3DAngleCode or livenessCode) != ErrorInfo.MOK) {
+                        LogUtil.d("人脸检测结果:年龄、性别、角度、获取验证失败")
+                        return
+                    }
 
-                // 自己加的,必须有活体检测
-                if (faceLivenessInfoList.none { it.liveness == LivenessInfo.ALIVE }) {
-                    callBack(null, faceInfoList.size, false)
-                    return
-                }
-                val bitmap = NV21ToBitmap(context).nv21ToBitmap(
-                    nv21,
-                    previewSize!!.width,
-                    previewSize!!.height
-                )
-                if (!needCheckCenter || (faceInfoList[0].rect.isInCenterArea(
+                    // 自己加的,必须有活体检测
+                    if (faceLivenessInfoList.none { it.liveness == LivenessInfo.ALIVE }) {
+                        callBack(null, faceInfoList.size, false)
+                        return
+                    }
+                    val bitmap = NV21ToBitmap(context).nv21ToBitmap(
+                        nv21,
                         previewSize!!.width,
                         previewSize!!.height
-                    ))
-                ) {
+                    )
                     LogUtil.d("人脸检测结果-识别结果 : ${bitmap == null} - $faceInfoList")
                     callBack(bitmap, faceInfoList.size, true)
                 }
@@ -211,7 +214,7 @@ object ArcSoftUtil {
         callBack: (Bitmap?, Int, Boolean) -> Unit
     ) {
         initEngine(context)
-        initCamera(context, windowManager, preview, false, callBack)
+        initCamera(context, windowManager, preview, null, false, callBack)
     }
 
     fun stop() {

+ 26 - 15
app/src/main/java/com/grkj/iscs_mars/view/dialog/FaceCaptureDialog.kt

@@ -22,16 +22,18 @@ class FaceCaptureDialog(val ctx: BaseActivity<*>, var callback: (Bitmap?) -> Uni
     private var isFaceDetect = false
     private var isInCountDown: Boolean = false
     private val captureTimer = CancellableTimer(3000, 1000, {
-        viewBinding.countDownTip.text = "${(3000 - it) / 1000 + 1}"
+        LogUtil.i("检测到人脸,倒计时:${(3000 - it) / 1000 + 1}")
+        mBinding?.countDownTip?.text = "${(3000 - it) / 1000 + 1}"
     }) {
-        ArcSoftUtil.stop()
+        LogUtil.i("检测到人脸,倒计时结束")
         isFaceDetect = true
-        viewBinding.previewLayout.visibility = View.INVISIBLE
-        viewBinding.image.visibility = View.VISIBLE
-        viewBinding.cbRecapture.visibility = View.VISIBLE
-        viewBinding.cbConfirm.visibility = View.VISIBLE
-        viewBinding.countDownTip.text = ctx.getString(R.string.detect_face_tip)
-        viewBinding.countDownTip.isVisible = false
+        mBinding?.previewLayout?.visibility = View.INVISIBLE
+        mBinding?.image?.visibility = View.VISIBLE
+        mBinding?.cbRecapture?.visibility = View.VISIBLE
+        mBinding?.cbConfirm?.visibility = View.VISIBLE
+        mBinding?.countDownTip?.text = ctx.getString(R.string.detect_face_tip)
+        mBinding?.countDownTip?.isVisible = false
+        mBinding?.faceOverlayView?.setFaceRect(null)
     }
     private val reCaptureTimer = CancellableTimer(2000, 1000, {}) {
         isFaceDetect = false
@@ -42,10 +44,11 @@ class FaceCaptureDialog(val ctx: BaseActivity<*>, var callback: (Bitmap?) -> Uni
 
     override fun initView() {
         mBinding?.cbRecapture?.setOnClickListener {
-            mBinding?.image?.isVisible =false
+            mBinding?.image?.isVisible = false
             mBinding?.previewLayout?.visibility = View.VISIBLE
             mBinding?.cbRecapture?.visibility = View.GONE
             mBinding?.cbConfirm?.visibility = View.GONE
+            mBinding?.faceOverlayView?.setFaceRect(null)
             reCaptureTimer.start()
             startFace()
         }
@@ -53,10 +56,13 @@ class FaceCaptureDialog(val ctx: BaseActivity<*>, var callback: (Bitmap?) -> Uni
         mBinding?.cbCancel?.setOnClickListener {
             mBinding?.cbRecapture?.visibility = View.GONE
             mBinding?.cbConfirm?.visibility = View.GONE
+            mBinding?.faceOverlayView?.setFaceRect(null)
             dismiss()
         }
 
         mBinding?.cbConfirm?.setOnClickListener {
+            ArcSoftUtil.stop()
+            mBinding?.faceOverlayView?.setFaceRect(null)
             callback(mCapturedBitmap)
             dismiss()
         }
@@ -73,11 +79,13 @@ class FaceCaptureDialog(val ctx: BaseActivity<*>, var callback: (Bitmap?) -> Uni
     }
 
     private fun startFace() {
+        isInCountDown = false
         ArcSoftUtil.initEngine(context)
         ArcSoftUtil.initCamera(
             context,
             ctx.windowManager,
             mBinding?.preview!!,
+            mBinding?.faceOverlayView!!,
             true
         ) { bitmap, faceSize, alive ->
             mBinding?.tipTv?.isVisible = faceSize > 1 || alive == false
@@ -93,10 +101,11 @@ class FaceCaptureDialog(val ctx: BaseActivity<*>, var callback: (Bitmap?) -> Uni
                 stopCountDown()
                 return@initCamera
             }
-            if (!isInCountDown){
+            if (!isInCountDown) {
+                LogUtil.i("检测到人脸,开始倒计时")
                 startCountDown()
             }
-            if (!isFaceDetect){
+            if (!isFaceDetect) {
                 mCapturedBitmap = bitmap
                 mBinding?.image?.setImageBitmap(bitmap)
             }
@@ -105,17 +114,19 @@ class FaceCaptureDialog(val ctx: BaseActivity<*>, var callback: (Bitmap?) -> Uni
 
     private fun startCountDown() {
         isInCountDown = true
-        viewBinding.countDownTip.text = ctx.getString(R.string.detect_face_tip)
-        viewBinding.countDownTip.isVisible = true
+        mBinding?.countDownTip?.text = ctx.getString(R.string.detect_face_tip)
+        mBinding?.countDownTip?.isVisible = true
         captureTimer.start()
     }
 
     private fun stopCountDown() {
+        LogUtil.i("倒计时被取消")
         isInCountDown = false
-        viewBinding.countDownTip.text = ctx.getString(R.string.detect_face_tip)
-        viewBinding.countDownTip.isVisible = false
+        mBinding?.countDownTip?.text = ctx.getString(R.string.detect_face_tip)
+        mBinding?.countDownTip?.isVisible = false
         captureTimer.cancel()
     }
+
     override fun dismiss() {
         super.dismiss()
         mCapturedBitmap = null

+ 5 - 3
app/src/main/java/com/grkj/iscs_mars/view/dialog/LoginDialog.kt

@@ -132,14 +132,16 @@ class LoginDialog(
         ArcSoftUtil.initCamera(
             context,
             ctx.windowManager,
-            mBinding?.preview!!
+            mBinding?.preview!!,
+            null,
+            false
         ) { bitmap, faceSize, alive ->
             bitmap?.let { itBitmap ->
-                if (faceSize>1){
+                if (faceSize > 1) {
                     ToastUtils.tip(R.string.only_one_person_allowed)
                     return@initCamera
                 }
-                if (alive==false){
+                if (alive == false) {
                     ToastUtils.tip(R.string.real_person_verification_required)
                     return@initCamera
                 }

+ 32 - 11
app/src/main/java/com/grkj/iscs_mars/view/fragment/FingerprintConfigFragment.kt

@@ -4,6 +4,7 @@ import com.grkj.iscs_mars.R
 import com.grkj.iscs_mars.databinding.FragmentFingerprintConfigBinding
 import com.grkj.iscs_mars.model.vo.characteristic.CharacteristicPageRespVO
 import com.grkj.iscs_mars.util.ToastUtils
+import com.grkj.iscs_mars.util.log.LogUtil
 import com.grkj.iscs_mars.view.base.BaseMvpFragment
 import com.grkj.iscs_mars.view.dialog.FingerScanDialog
 import com.grkj.iscs_mars.view.dialog.TipDialog
@@ -11,6 +12,8 @@ import com.grkj.iscs_mars.view.iview.IFingerprintConfigView
 import com.grkj.iscs_mars.view.presenter.FingerprintConfigPresenter
 import com.zhy.adapter.recyclerview.CommonAdapter
 import com.zhy.adapter.recyclerview.base.ViewHolder
+import java.util.UUID
+import kotlin.collections.joinToString
 
 /**
  * 指纹设置页
@@ -24,8 +27,9 @@ class FingerprintConfigFragment :
     private var mFingerprintLimit: Int = 5
     private var mFingerprintPressTimes: Int = 0
     private var mFingerprintInputErrorTimes: Int = 0
-    private var mFingerprintGroupName: Long = 0L
+    private var mFingerprintGroupName: String = ""
     private val maxPressTimes = 3
+    private val inputFingerprintErrorTimes = 3
     private val inputFingerprintIds: MutableList<Long> = mutableListOf()
 
     override val viewBinding: FragmentFingerprintConfigBinding
@@ -33,8 +37,8 @@ class FingerprintConfigFragment :
 
     override fun initView() {
         mBinding?.cbAddFinger?.setOnClickListener {
-            if (mFingerList.size >= mFingerprintLimit) {
-                ToastUtils.tip(getString(R.string.fingerprint_config_tip, mFingerprintLimit))
+            if (mFingerList.size >= mFingerprintLimit / 3) {
+                ToastUtils.tip(getString(R.string.fingerprint_config_tip, mFingerprintLimit / 3))
                 return@setOnClickListener
             }
             mFingerprintPressTimes = 0
@@ -72,14 +76,14 @@ class FingerprintConfigFragment :
         presenter?.getFingerprintLimit {
             mFingerprintLimit = it
             mBinding?.fingerprintLimitTv?.text =
-                getString(R.string.fingerprint_config_tip, mFingerprintLimit)
+                getString(R.string.fingerprint_config_tip, mFingerprintLimit / 3)
         }
     }
 
     private fun refreshFingerList() {
         presenter?.getFingerPage {
             mFingerList.clear()
-            it?.records?.groupBy { it.group }?.let {
+            it?.records?.groupBy { it.groupCode }?.let {
                 val fingerList = it.mapNotNull { it.key to it.value }.toMutableList()
                 mFingerList.addAll(fingerList)
             }
@@ -107,26 +111,40 @@ class FingerprintConfigFragment :
 
     private fun showFingerScanDialog() {
         mFingerDialog ?: let {
-            mFingerprintGroupName = System.currentTimeMillis()
             mFingerDialog = FingerScanDialog(requireContext()) {
                 if (it != null) {
                     presenter?.insertFinger(it, "$mFingerprintGroupName") {
-                        if (it!=null) {
+                        if (it != null) {
+                            LogUtil.i("添加指纹:${it}")
+                            inputFingerprintIds.add(it)
                             mFingerprintPressTimes++
-                            if (mFingerprintPressTimes == 3) {
+                            if (mFingerprintPressTimes == maxPressTimes) {
                                 mFingerDialog?.dismiss()
                                 showTipDialog(
                                     getString(R.string.fingerprint_add_success_tip),
                                     isAdd = true
                                 )
                                 refreshFingerList()
+                            } else if (mFingerprintInputErrorTimes == inputFingerprintErrorTimes) {
+                                mFingerprintGroupName = UUID.randomUUID().toString()
+                                mFingerprintPressTimes = 0
+                                mFingerprintInputErrorTimes = 0
+                                presenter?.deleteFinger(inputFingerprintIds.joinToString(",")) {
+                                    refreshFingerList()
+                                }
+                                ToastUtils.tip(R.string.please_re_press_fingerprint_again)
                             } else {
                                 ToastUtils.tip(R.string.please_press_fingerprint_again)
                             }
-                        }else{
+                        } else {
                             mFingerprintInputErrorTimes++
-                            if (mFingerprintInputErrorTimes+mFingerprintPressTimes>maxPressTimes){
-                                mFingerprintGroupName = System.currentTimeMillis()
+                            if (mFingerprintInputErrorTimes == inputFingerprintErrorTimes) {
+                                mFingerprintGroupName = UUID.randomUUID().toString()
+                                mFingerprintPressTimes = 0
+                                mFingerprintInputErrorTimes = 0
+                                presenter?.deleteFinger(inputFingerprintIds.joinToString(",")) {
+                                    refreshFingerList()
+                                }
                                 ToastUtils.tip(R.string.please_re_press_fingerprint_again)
                             }
                         }
@@ -134,6 +152,9 @@ class FingerprintConfigFragment :
                 }
             }
         }
+        mFingerprintPressTimes = 0
+        mFingerprintInputErrorTimes = 0
+        mFingerprintGroupName = UUID.randomUUID().toString()
         mFingerDialog?.show()
     }
 

+ 84 - 0
app/src/main/java/com/grkj/iscs_mars/view/widget/FaceOverlayView.kt

@@ -0,0 +1,84 @@
+package com.grkj.iscs_mars.view.widget
+
+import android.content.Context
+import android.graphics.Canvas
+import android.graphics.Color
+import android.graphics.Paint
+import android.graphics.Rect
+import android.graphics.RectF
+import android.util.AttributeSet
+import android.view.View
+import kotlin.apply
+import kotlin.collections.forEach
+import kotlin.collections.map
+
+/**
+ * 自定义人脸框绘制控件。
+ * 根据摄像头预览尺寸与 View 尺寸的等比缩放,对应绘制人脸检测到的 Rect 区域。
+ *
+ * 使用方法:
+ * 1. 在布局中引入 FaceOverlayView
+ * 2. 调用 setCameraPreviewSize(width, height)
+ * 3. 调用 setFaceRect(Rect 或 RectF)
+ */
+class FaceOverlayView @JvmOverloads constructor(
+    context: Context,
+    attrs: AttributeSet? = null,
+    defStyleAttr: Int = 0
+) : View(context, attrs, defStyleAttr) {
+
+    private var cameraWidth = 0f
+    private var cameraHeight = 0f
+    private var faceRectF: List<RectF>? = null
+
+    private val paint = Paint().apply {
+        color = Color.GREEN
+        style = Paint.Style.STROKE
+        strokeWidth = 5f
+        isAntiAlias = true
+    }
+
+    /**
+     * 设置摄像头预览的分辨率,用于等比缩放映射
+     */
+    fun setCameraPreviewSize(width: Int, height: Int) {
+        cameraWidth = width.toFloat()
+        cameraHeight = height.toFloat()
+        invalidate()
+    }
+
+    /**
+     * 设置人脸检测得到的 Rect 坐标(基于摄像头分辨率)
+     */
+    fun setFaceRect(rects: List<Rect>?) {
+        faceRectF = rects?.map { RectF(it) }
+        invalidate()
+    }
+
+    override fun onDraw(canvas: Canvas) {
+        super.onDraw(canvas)
+        val rects = faceRectF ?: return
+        if (cameraWidth <= 0 || cameraHeight <= 0) return
+
+        // View 尺寸
+        val vw = width.toFloat()
+        val vh = height.toFloat()
+        // 等比缩放系数
+        val scale = kotlin.comparisons.minOf(vw / cameraWidth, vh / cameraHeight)
+        // 计算预览内容缩放后大小
+        val scaledW = cameraWidth * scale
+        val scaledH = cameraHeight * scale
+        // 居中偏移
+        val dx = (vw - scaledW) / 2
+        val dy = (vh - scaledH) / 2
+        rects.forEach { rect ->
+            // 映射到 View 坐标系
+            val left = rect.left * scale + dx
+            val top = rect.top * scale + dy
+            val right = rect.right * scale + dx
+            val bottom = rect.bottom * scale + dy
+
+            canvas.drawRect(left, top, right, bottom, paint)
+        }
+    }
+}

+ 37 - 26
app/src/main/res/layout/dialog_face_capture.xml

@@ -87,36 +87,47 @@
                         android:layout_width="match_parent"
                         android:layout_height="match_parent" />
 
-                    <ImageView
-                        android:layout_width="match_parent"
-                        android:layout_height="match_parent"
-                        android:scaleType="fitXY"
-                        android:src="@drawable/mask_vector_circle" />
 
-                    <TextView
-                        android:id="@+id/count_down_tip"
-                        android:layout_width="wrap_content"
-                        android:layout_height="wrap_content"
-                        android:layout_gravity="center|bottom"
-                        android:layout_marginBottom="@dimen/common_spacing"
-                        android:textColor="@color/dialogxColorBlue"
-                        android:textSize="@dimen/common_text_size_big"
-                        android:textStyle="bold"
-                        android:visibility="gone"
-                        android:text="@string/detect_face_tip"
-                        tools:text="检测到人脸,即将拍摄" />
+                    <com.grkj.iscs_mars.view.widget.FaceOverlayView
+                        android:id="@+id/face_overlay_view"
+                        android:layout_width="match_parent"
+                        android:layout_height="match_parent"/>
 
-                    <TextView
-                        android:id="@+id/tip_tv"
+                    <FrameLayout
                         android:layout_width="match_parent"
                         android:layout_height="wrap_content"
-                        android:layout_marginTop="10dp"
-                        android:gravity="center"
-                        android:text="@string/only_one_person_allowed"
-                        android:textColor="@color/common_status_red"
-                        android:textSize="@dimen/common_text_size_big"
-                        android:visibility="gone"
-                        tools:text="请保证画面中只有自己" />
+                        android:layout_gravity="bottom"
+                        android:background="@color/common_transparent_half">
+
+                        <TextView
+                            android:id="@+id/count_down_tip"
+                            android:layout_width="wrap_content"
+                            android:layout_height="wrap_content"
+                            android:layout_gravity="center"
+                            android:paddingVertical="@dimen/common_spacing_small"
+                            android:text="@string/detect_face_tip"
+                            android:gravity="center"
+                            android:textColor="@color/dialogxColorBlue"
+                            android:textSize="@dimen/common_text_size"
+                            android:textStyle="bold"
+                            android:visibility="gone"
+                            tools:text="检测到人脸,即将拍摄" />
+
+                        <TextView
+                            android:id="@+id/tip_tv"
+                            android:layout_width="match_parent"
+                            android:layout_height="wrap_content"
+                            android:layout_gravity="center"
+                            android:layout_marginTop="10dp"
+                            android:layout_marginBottom="@dimen/common_spacing"
+                            android:gravity="center"
+                            android:paddingVertical="@dimen/common_spacing_small"
+                            android:text="@string/only_one_person_allowed"
+                            android:textColor="@color/common_status_red"
+                            android:textSize="@dimen/common_text_size"
+                            android:visibility="gone"
+                            tools:text="请保证画面中只有自己" />
+                    </FrameLayout>
                 </FrameLayout>
 
                 <ImageView

+ 1 - 0
app/src/main/res/values/colors.xml

@@ -45,4 +45,5 @@
     <color name="common_switch_disable">#f0f0f0</color>
     <color name="color_d7d2d2">#d7d2d2</color>
     <color name="dialogxColorBlue">#2196F3</color>
+    <color name="common_transparent_half">#33ffffff</color>
 </resources>