Pārlūkot izejas kodu

feat(人脸识别):
- 增加人脸框居中判断
- 增加人脸识别时的圆形遮罩
- 修复人脸识别卡顿问题

周文健 4 mēneši atpakaļ
vecāks
revīzija
56e7e7ab57

+ 4 - 0
app/src/main/java/com/grkj/iscs/features/login/dialog/LoginDialog.kt

@@ -12,6 +12,7 @@ import com.grkj.iscs.features.login.activity.LoginActivity
 import com.grkj.iscs.features.login.viewmodel.LoginViewModel
 import com.grkj.iscs.features.main.activity.MainActivity
 import com.grkj.shared.utils.ArcSoftUtil
+import com.grkj.shared.utils.ArcSoftUtil.inDetecting
 import com.grkj.ui_base.config.ISCSConfig
 import com.grkj.ui_base.utils.CommonUtils
 import com.grkj.ui_base.utils.event.LoadingEvent
@@ -189,9 +190,11 @@ class LoginDialog(
             ) { bitmap, faceSize, alive ->
                 bitmap?.let { itBitmap ->
                     if (faceSize == 0) {
+                        inDetecting = false
                         return@let
                     }
                     if (inFaceChecking) {
+                        inDetecting = false
                         return@let
                     }
                     inFaceChecking = true
@@ -200,6 +203,7 @@ class LoginDialog(
                     ).observe(lifecycleOwner) {
                         if (it == false) {
                             ThreadUtils.runOnMainDelayed(1000) {
+                                inDetecting = false
                                 inFaceChecking = false
                             }
                         } else {

+ 3 - 0
app/src/main/java/com/grkj/iscs/features/main/dialog/CheckFaceDialog.kt

@@ -171,9 +171,11 @@ class CheckFaceDialog(
             ) { bitmap, faceSize, alive ->
                 bitmap?.let { itBitmap ->
                     if (faceSize == 0) {
+                        ArcSoftUtil.inDetecting = false
                         return@let
                     }
                     if (inFaceChecking) {
+                        ArcSoftUtil.inDetecting = false
                         return@let
                     }
                     inFaceChecking = true
@@ -182,6 +184,7 @@ class CheckFaceDialog(
                     ).observe(lifecycleOwner) {
                         if (it == false) {
                             ThreadUtils.runOnMainDelayed(1000) {
+                                ArcSoftUtil.inDetecting = false
                                 inFaceChecking = false
                             }
                         } else {

+ 8 - 4
app/src/main/java/com/grkj/iscs/features/main/fragment/user_info/SetFaceFragment.kt

@@ -72,7 +72,7 @@ class SetFaceFragment : BaseFragment<FragmentSetFaceBinding>() {
         binding.recapture.setDebouncedClickListener {
             isFaceChecking = false
             binding.image.isVisible = false
-            binding.preview.isVisible = true
+            binding.previewLayout.isVisible = true
         }
     }
 
@@ -105,15 +105,17 @@ class SetFaceFragment : BaseFragment<FragmentSetFaceBinding>() {
     }
 
     private fun startFace() {
-        binding.preview.isVisible = true
+        binding.previewLayout.isVisible = true
         binding.image.isVisible = false
         ArcSoftUtil.initEngine(requireContext())
         ArcSoftUtil.initCamera(
             requireContext(),
             requireActivity().windowManager,
-            binding.preview
+            binding.preview,
+            true
         ) { bitmap, faceSize, alive ->
             if (isFaceChecking) {
+                ArcSoftUtil.inDetecting = false
                 return@initCamera
             }
             isFaceChecking = true
@@ -122,15 +124,17 @@ class SetFaceFragment : BaseFragment<FragmentSetFaceBinding>() {
             if (faceSize > 1) {
                 binding.tipTv.text = getString(R.string.only_one_person_allowed)
                 isFaceChecking = false
+                ArcSoftUtil.inDetecting = false
                 return@initCamera
             }
             if (alive == false) {
                 binding.tipTv.text =
                     getString(R.string.real_person_verification_required)
                 isFaceChecking = false
+                ArcSoftUtil.inDetecting = false
                 return@initCamera
             }
-            binding.preview.visibility = View.INVISIBLE
+            binding.previewLayout.visibility = View.INVISIBLE
             binding.image.visibility = View.VISIBLE
             mCapturedBitmap = bitmap
             binding.image.setImageBitmap(bitmap)

+ 3 - 0
app/src/main/java/com/grkj/iscs/features/main/fragment/user_info/UserInfoFragment.kt

@@ -139,6 +139,7 @@ class UserInfoFragment : BaseFragment<FragmentUserInfoBinding>() {
             binding.preview
         ) { bitmap, faceSize, alive ->
             if (isFaceChecking) {
+                ArcSoftUtil.inDetecting = false
                 return@initCamera
             }
             isFaceChecking = true
@@ -147,12 +148,14 @@ class UserInfoFragment : BaseFragment<FragmentUserInfoBinding>() {
             if (faceSize > 1) {
                 binding.tipTv.text = getString(R.string.only_one_person_allowed)
                 isFaceChecking = false
+                ArcSoftUtil.inDetecting = false
                 return@initCamera
             }
             if (alive == false) {
                 binding.tipTv.text =
                     getString(R.string.real_person_verification_required)
                 isFaceChecking = false
+                ArcSoftUtil.inDetecting = false
                 return@initCamera
             }
             binding.preview.visibility = View.INVISIBLE

+ 27 - 0
app/src/main/res/drawable/mask_vector_circle.xml

@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24">
+
+    <!-- 白色全图块 + 中心半径8dp的透明圆孔 -->
+    <path
+        android:fillColor="#FFFFFFFF"
+        android:fillType="evenOdd"
+        android:pathData="
+            M0,0
+            H24
+            V24
+            H0
+            Z
+            M12,12
+            m-8,0
+            a8,8 0 1,0 16,0
+            a8,8 0 1,0 -16,0" />
+    <path
+        android:fillType="nonZero"
+        android:pathData="M0,0L24,0L24,24L0,24L0,0Z"
+        android:strokeWidth="0.2"
+        android:strokeColor="@color/black" />
+</vector>

+ 15 - 3
app/src/main/res/layout-land/fragment_set_face.xml

@@ -124,11 +124,23 @@
                         android:layout_weight="1"
                         android:background="@drawable/common_card_bg">
 
-                        <TextureView
-                            android:id="@+id/preview"
+                        <FrameLayout
+                            android:id="@+id/preview_layout"
                             android:layout_width="match_parent"
                             android:layout_height="match_parent"
-                            android:visibility="invisible" />
+                            android:visibility="invisible">
+
+                            <TextureView
+                                android:id="@+id/preview"
+                                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" />
+                        </FrameLayout>
 
                         <ImageView
                             android:id="@+id/image"

+ 15 - 3
app/src/main/res/layout/fragment_set_face.xml

@@ -118,11 +118,23 @@
                     android:layout_weight="1"
                     android:background="@drawable/common_card_bg">
 
-                    <TextureView
-                        android:id="@+id/preview"
+                    <FrameLayout
+                        android:id="@+id/preview_layout"
                         android:layout_width="match_parent"
                         android:layout_height="match_parent"
-                        android:visibility="invisible" />
+                        android:visibility="invisible">
+
+                        <TextureView
+                            android:id="@+id/preview"
+                            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" />
+                    </FrameLayout>
 
                     <ImageView
                         android:id="@+id/image"

+ 37 - 14
shared/src/main/java/com/grkj/shared/utils/ArcSoftUtil.kt

@@ -24,9 +24,12 @@ import com.arcsoft.face.enums.DetectFaceOrientPriority
 import com.arcsoft.face.enums.DetectMode
 import com.arcsoft.face.enums.ExtractType
 import com.grkj.shared.config.Constants
+import com.grkj.shared.utils.extension.expandToPadCenter
+import com.grkj.shared.utils.extension.isInCenterArea
 import com.grkj.shared.utils.face.arcsoft.CameraHelper
 import com.grkj.shared.utils.face.arcsoft.CameraListener
 import com.grkj.shared.utils.face.arcsoft.NV21ToBitmap
+import com.sik.sikimage.CropImageUtils
 import org.slf4j.Logger
 import org.slf4j.LoggerFactory
 
@@ -48,6 +51,7 @@ object ArcSoftUtil {
     private const val ACTION_REQUEST_PERMISSIONS: Int = 0x001
     var isActivated = false
     private var isInit = false
+    var inDetecting = false
 
     /**
      * 所需的所有权限信息
@@ -93,7 +97,7 @@ object ArcSoftUtil {
         faceEngine = FaceEngine()
         afCode = faceEngine!!.init(
             context,
-            DetectMode.ASF_DETECT_MODE_IMAGE,
+            DetectMode.ASF_DETECT_MODE_VIDEO,
             DetectFaceOrientPriority.ASF_OP_0_ONLY,
             1,
             FaceEngine.ASF_FACE_DETECT or FaceEngine.ASF_AGE or FaceEngine.ASF_MASK_DETECT or FaceEngine.ASF_GENDER or FaceEngine.ASF_LIVENESS or FaceEngine.ASF_FACE_RECOGNITION
@@ -112,10 +116,12 @@ object ArcSoftUtil {
         }
     }
 
+    @JvmOverloads
     fun initCamera(
         context: Context,
         windowManager: WindowManager,
         preview: View,
+        needCheckCenter: Boolean = false,
         callBack: (Bitmap?, Int, Boolean) -> Unit
     ) {
         val metrics = DisplayMetrics()
@@ -134,6 +140,10 @@ object ArcSoftUtil {
 
 
             override fun onPreview(nv21: ByteArray?, camera: Camera?) {
+                if (inDetecting) {
+                    return
+                }
+                inDetecting = true
                 val faceInfoList: List<FaceInfo> = ArrayList()
                 var code = faceEngine!!.detectFaces(
                     nv21,
@@ -152,9 +162,11 @@ object ArcSoftUtil {
                         processMask
                     )
                     if (code != ErrorInfo.MOK) {
+                        inDetecting = false
                         return
                     }
                 } else {
+                    inDetecting = false
                     return
                 }
 
@@ -169,26 +181,37 @@ object ArcSoftUtil {
                 // 有其中一个的错误码不为ErrorInfo.MOK,return
                 if ((ageCode or genderCode or livenessCode) != ErrorInfo.MOK) {
                     logger.debug("人脸检测结果:年龄、性别、角度、获取验证失败")
+                    inDetecting = false
                     return
                 }
 
                 // 自己加的,必须有活体检测
                 if (faceLivenessInfoList.none { it.liveness == LivenessInfo.ALIVE }) {
                     callBack(null, faceInfoList.size, false)
+                    inDetecting = false
+                    return
+                }
+                if (!needCheckCenter || (faceInfoList[0].rect.isInCenterArea(
+                        previewSize!!.width,
+                        previewSize!!.height
+                    ))
+                ) {
+                    val bitmap = NV21ToBitmap(context).nv21ToBitmap(
+                        nv21,
+                        previewSize!!.width,
+                        previewSize!!.height
+                    )
+                    val faceRect = faceInfoList[0].rect.expandToPadCenter()
+                    logger.debug("人脸检测结果-识别结果 : ${bitmap == null} - $faceInfoList")
+                    callBack(bitmap, faceInfoList.size, true)
+                    bitmap?.let {
+                        val faceBitmap = CropImageUtils.cropBitmap(bitmap, faceRect)
+                        callBack(faceBitmap, faceInfoList.size, true)
+                    } ?: callBack(null, faceInfoList.size, true)
+                } else {
+                    inDetecting = false
                     return
                 }
-                val bitmap = NV21ToBitmap(context).nv21ToBitmap(
-                    nv21,
-                    previewSize!!.width,
-                    previewSize!!.height
-                )
-//                val faceRect = faceInfoList[0].rect
-                logger.debug("人脸检测结果-识别结果 : ${bitmap == null} - $faceInfoList")
-                callBack(bitmap, faceInfoList.size, true)
-//                bitmap?.let {
-//                    val faceBitmap = CropImageUtils.cropBitmap(bitmap, faceRect)
-//                    callBack(faceBitmap, faceInfoList.size, true)
-//                } ?: callBack(null, faceInfoList.size, true)
             }
 
             override fun onCameraClosed() {
@@ -222,7 +245,7 @@ object ArcSoftUtil {
         callBack: (Bitmap?, Int, Boolean) -> Unit
     ) {
         initEngine(context)
-        initCamera(context, windowManager, preview, callBack)
+        initCamera(context, windowManager, preview, false, callBack)
     }
 
     fun stop() {

+ 28 - 1
shared/src/main/java/com/grkj/shared/utils/extension/Rect.kt

@@ -27,4 +27,31 @@ inline fun Rect.expandToPadCenter(padW: Int = 4, padH: Int = 4): Rect {
     val top = (cy - newH / 2f).roundToInt()
 
     return Rect(left, top, left + newW, top + newH)
-}
+}
+
+
+/**
+ * 判断 Rect 是否在父容器中心区域内。
+ *
+ * @param parentWidth 父容器宽度(px)
+ * @param parentHeight 父容器高度(px)
+ * @param horizontalMarginPercent 水平边距百分比,范围 0f…0.5f(比如 0.1667f 表示左右各留 16.67%)
+ * @param verticalMarginPercent   垂直边距百分比,范围 0f…0.5f(同理)
+ * @return 如果 rect 完全在这块中心区域内,返回 true
+ */
+fun Rect.isInCenterArea(
+    parentWidth: Int,
+    parentHeight: Int,
+    horizontalMarginPercent: Float = 0.1667f,
+    verticalMarginPercent: Float = 0.1667f
+): Boolean {
+    // 计算左右、上下的像素边距
+    val marginX = parentWidth * horizontalMarginPercent
+    val marginY = parentHeight * verticalMarginPercent
+
+    // rect 的四条边都要在“margin 区域”之外
+    return left   >= marginX &&
+            right  <= parentWidth - marginX &&
+            top    >= marginY &&
+            bottom <= parentHeight - marginY
+}

+ 0 - 1
ui-base/src/main/java/com/grkj/ui_base/utils/GraphicUtils.kt

@@ -70,5 +70,4 @@ object GraphicUtils {
             (centerY + halfHeight).toInt()
         )
     }
-
 }

+ 1 - 0
ui-base/src/main/java/com/grkj/ui_base/utils/extension/View.kt

@@ -2,6 +2,7 @@ package com.grkj.ui_base.utils.extension
 
 import android.content.Context
 import android.content.res.Resources
+import android.graphics.Rect
 import android.view.Gravity
 import android.view.View
 import android.view.ViewGroup