Przeglądaj źródła

添加人脸检测+拍照测试页

Frankensteinly 8 miesięcy temu
rodzic
commit
f0a2342f84

+ 9 - 0
app/build.gradle

@@ -115,4 +115,13 @@ dependencies {
 
     // https://github.com/onlylemi/MapView
     implementation 'com.github.onlylemi:mapview:v1.0'
+
+    // CameraX 核心库
+    implementation "androidx.camera:camera-core:1.2.0"
+    implementation "androidx.camera:camera-camera2:1.2.0"
+    implementation "androidx.camera:camera-lifecycle:1.2.0"
+    implementation "androidx.camera:camera-view:1.2.0"
+
+    // ML Kit 面部检测库
+    implementation 'com.google.mlkit:face-detection:16.1.5'
 }

+ 8 - 2
app/src/main/AndroidManifest.xml

@@ -19,6 +19,8 @@
 
     <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
     <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
+    <uses-permission android:name="android.permission.CAMERA" />
+    <uses-feature android:name="android.hardware.camera" />
 
     <application
         android:name=".MyApplication"
@@ -32,13 +34,16 @@
         android:supportsRtl="true"
         android:theme="@style/Theme.ISCS"
         tools:targetApi="31">
+        <activity
+            android:name=".FaceActivity"
+            android:exported="false" />
         <activity
             android:name=".view.activity.HomeActivity"
             android:exported="false" />
         <activity
             android:name=".view.activity.LoginActivity"
-            android:launchMode="singleTask"
-            android:exported="false" />
+            android:exported="false"
+            android:launchMode="singleTask" />
         <activity
             android:name=".view.activity.CreateTicketActivity"
             android:exported="false" />
@@ -85,6 +90,7 @@
             android:exported="true">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
+
                 <category android:name="android.intent.category.LAUNCHER" />
             </intent-filter>
         </activity>

+ 64 - 0
app/src/main/java/com/grkj/iscs/FaceActivity.kt

@@ -0,0 +1,64 @@
+package com.grkj.iscs
+
+import android.view.View
+import com.grkj.iscs.databinding.ActivityFaceBinding
+import com.grkj.iscs.util.Executor
+import com.grkj.iscs.view.base.BaseActivity
+
+class FaceActivity : BaseActivity<ActivityFaceBinding>() {
+
+    private val faceDetectorHelper by lazy {
+        FaceDetectorHelper()
+    }
+
+    override val viewBinding: ActivityFaceBinding
+        get() = ActivityFaceBinding.inflate(layoutInflater)
+
+    override fun initView() {
+        // 启动预览
+        mBinding?.preview?.visibility = View.VISIBLE
+        mBinding?.preview?.bringToFront()
+        faceDetectorHelper.startPreview(this, mBinding?.preview!!) {
+            // 注意切换线程
+            Executor.runOnMain {
+                // 关闭识别
+                faceDetectorHelper.stopDetector()
+                mBinding?.startDetector?.text = "开始识别"
+                // 设置图片
+                mBinding?.image?.setImageBitmap(it)
+                mBinding?.image?.bringToFront()
+                // 隐藏预览页面
+                mBinding?.preview?.visibility = View.INVISIBLE
+            }
+        }
+
+
+        mBinding?.startDetector?.setOnClickListener {
+            faceDetectorHelper.let {
+                if (it.isOpenDetector) {
+                    it.stopDetector()
+                    mBinding?.startDetector?.text = "开始识别"
+                } else {
+                    it.startDetector()
+                    mBinding?.startDetector?.text = "关闭识别"
+                }
+            }
+        }
+
+        mBinding?.stopDetector?.setOnClickListener {
+            faceDetectorHelper.switchCamera(this)
+        }
+
+        mBinding?.continueDetector?.setOnClickListener {
+            mBinding?.preview?.visibility = View.VISIBLE
+            mBinding?.preview?.bringToFront()
+            faceDetectorHelper.startDetector()
+            mBinding?.startDetector?.text = "关闭识别"
+        }
+    }
+
+    override fun onDestroy() {
+        super.onDestroy()
+        faceDetectorHelper.release()
+    }
+}

+ 241 - 0
app/src/main/java/com/grkj/iscs/FaceDetectorHelper.kt

@@ -0,0 +1,241 @@
+package com.grkj.iscs
+
+import android.annotation.SuppressLint
+import android.graphics.Bitmap
+import android.graphics.BitmapFactory
+import android.graphics.ImageFormat
+import android.graphics.Matrix
+import android.graphics.Rect
+import android.graphics.YuvImage
+import android.media.FaceDetector
+import android.media.Image
+import android.util.Log
+import androidx.activity.ComponentActivity
+import androidx.camera.core.CameraSelector
+import androidx.camera.core.ImageAnalysis
+import androidx.camera.core.ImageAnalysis.Analyzer
+import androidx.camera.core.ImageProxy
+import androidx.camera.core.Preview
+import androidx.camera.lifecycle.ProcessCameraProvider
+import androidx.camera.view.PreviewView
+import androidx.core.content.ContextCompat
+import androidx.core.util.Consumer
+import java.io.ByteArrayOutputStream
+import java.util.concurrent.ExecutorService
+import java.util.concurrent.Executors
+
+
+/**
+ * 使用cameraX和FaceDetector进行人脸识别
+ *
+ * @author fdk
+ * @date 2024-04-22
+ */
+class FaceDetectorHelper(
+    private var mCameraExecutor: ExecutorService = Executors.newSingleThreadExecutor(),
+    private var mSelector: CameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
+) {
+    // 提供相机服务
+    private lateinit var mCameraProvider: ProcessCameraProvider
+
+    private lateinit var imagePreview: Preview
+
+    private lateinit var imageAnalysis: ImageAnalysis
+
+    // 人脸识别控制
+    var isOpenDetector = false
+
+    // 摄像头方向
+    private var currentLensFacing = CameraSelector.LENS_FACING_BACK
+
+    /**
+     * 使用CameraX API进行预览
+     *
+     * @param activity 带lifecycle的activity,提供context,并且便于使用协程
+     * @param view Camera API使用的 PreviewView
+     */
+    @SuppressLint("RestrictedApi")
+    fun startPreview(
+        activity: ComponentActivity,
+        view: PreviewView,
+        callback: Consumer<Bitmap>
+    ) {
+        val cameraProviderFuture = ProcessCameraProvider.getInstance(activity)
+        cameraProviderFuture.addListener({
+            // 用于将相机的生命周期绑定到生命周期所有者
+            // 消除了打开和关闭相机的任务,因为 CameraX 具有生命周期感知能力
+            mCameraProvider = cameraProviderFuture.get()
+
+            // 预览
+            imagePreview = Preview.Builder()
+                .build()
+                .also {
+                    it.setSurfaceProvider(view.surfaceProvider)
+                }
+
+            // 配置图像分析
+            imageAnalysis = ImageAnalysis.Builder()
+                .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
+                .build()
+            imageAnalysis.setAnalyzer(mCameraExecutor, FaceAnalyzer(callback))
+
+            // 选择摄像头,省去了去判断摄像头ID
+            val cameraSelector = mSelector
+
+            try {
+                // Unbind use cases before rebinding
+                mCameraProvider.unbindAll()
+
+                // 将相机绑定到 lifecycleOwner,就不用手动关闭了
+                mCameraProvider.bindToLifecycle(
+                    activity, cameraSelector, imagePreview, imageAnalysis)
+
+            } catch(exc: Exception) {
+                Log.e("TAG", "Use case binding failed", exc)
+            }
+
+            // 回调代码在主线程处理
+        }, ContextCompat.getMainExecutor(activity))
+    }
+
+    fun switchCamera(activity: ComponentActivity) {
+        currentLensFacing = if (currentLensFacing == CameraSelector.LENS_FACING_BACK) {
+            CameraSelector.LENS_FACING_FRONT
+        } else {
+            CameraSelector.LENS_FACING_BACK
+        }
+
+        val newCameraSelector = CameraSelector.Builder()
+            .requireLensFacing(currentLensFacing)
+            .build()
+
+        try {
+            mCameraProvider.unbindAll()
+            mCameraProvider.bindToLifecycle(activity, newCameraSelector, imagePreview, imageAnalysis)
+        } catch (e: Exception) {
+            e.printStackTrace()
+        }
+    }
+
+    /**
+     * 开启人脸识别
+     */
+    fun startDetector() {
+        isOpenDetector = true
+    }
+
+    /**
+     * 停止人脸识别
+     */
+    fun stopDetector() {
+        isOpenDetector = false
+    }
+
+    /**
+     * 释放资源
+     */
+    fun release() {
+        // 取消绑定生命周期观察者
+        mCameraProvider.unbindAll()
+    }
+
+    private inner class FaceAnalyzer(
+        private val callback: Consumer<Bitmap>
+    ) : Analyzer {
+
+        private val maxFaces = 1
+        private var faceDetector: FaceDetector? = null
+
+        @SuppressLint("UnsafeOptInUsageError")
+        override fun analyze(image: ImageProxy) {
+            // Log.d("TAG", "analyze: ${image.format}")
+
+            // 控制人脸检测
+            if (!isOpenDetector) {
+                image.close()
+                return
+            }
+
+            val bitmap = if (image.format == ImageFormat.YUV_420_888) {
+                val rotation = image.imageInfo.rotationDegrees
+                imageProxyToBitmap(image.image!!, rotation)
+            }else {
+                imageProxyToBitmap(image)
+            }
+
+            // 创建FaceDetector
+            if (faceDetector == null) {
+                faceDetector = FaceDetector(bitmap.width, bitmap.height, maxFaces)
+            }
+
+            // 进行人脸检测
+            val faces = arrayOfNulls<FaceDetector.Face>(maxFaces)
+            val faceCount = faceDetector!!.findFaces(bitmap, faces)
+
+            // 处理人脸检测结果
+            if (faceCount > 0) {
+
+                // 第一张脸信息
+                val face1 = faces[0]!!
+                // 人脸的可信度,0 - 1
+                val confidence = face1.confidence()
+                // 双眼的间距
+                // val eyesDistance = face1.eyesDistance()
+                // 角度
+                // val angle = face1.pose(FaceDetector.Face.EULER_X)
+
+                Log.d("TAG", "analyze: confidence = $confidence")
+
+                // 加点判断,传出结果
+                if (confidence > 0.5){
+                    callback.accept(bitmap)
+                }else {
+                    bitmap.recycle()
+                }
+            }else {
+                bitmap.recycle()
+            }
+
+            image.close()
+        }
+
+        private fun imageProxyToBitmap(image: Image, rotationDegrees: Int): Bitmap {
+            val yBuffer = image.planes[0].buffer
+            val uBuffer = image.planes[1].buffer
+            val vBuffer = image.planes[2].buffer
+
+            val ySize = yBuffer.remaining()
+            val uSize = uBuffer.remaining()
+            val vSize = vBuffer.remaining()
+
+            val nv21 = ByteArray(ySize + uSize + vSize)
+            yBuffer.get(nv21, 0, ySize)
+            vBuffer.get(nv21, ySize, vSize)
+            uBuffer.get(nv21, ySize + vSize, uSize)
+
+            val yuvImage = YuvImage(nv21, ImageFormat.NV21, image.width, image.height, null)
+            val outputStream = ByteArrayOutputStream()
+            yuvImage.compressToJpeg(Rect(0, 0, image.width, image.height), 100, outputStream)
+            val jpegArray = outputStream.toByteArray()
+
+            val options = BitmapFactory.Options()
+            options.inPreferredConfig = Bitmap.Config.RGB_565
+            val bitmap = BitmapFactory.decodeByteArray(jpegArray, 0, jpegArray.size, options)
+
+            val matrix = Matrix()
+            matrix.postRotate(rotationDegrees.toFloat())
+
+            return Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true)
+        }
+
+        private fun imageProxyToBitmap(image: ImageProxy): Bitmap {
+            val planeProxy = image.planes[0]
+            val buffer = planeProxy.buffer
+
+            val bytes = ByteArray(buffer.remaining())
+            buffer.get(bytes)
+
+            return BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
+        }
+    }
+}

+ 5 - 0
app/src/main/java/com/grkj/iscs/view/activity/MainActivity.kt

@@ -1,6 +1,7 @@
 package com.grkj.iscs.view.activity
 
 import android.content.Intent
+import com.grkj.iscs.FaceActivity
 import com.grkj.iscs.view.base.BaseActivity
 import com.grkj.iscs.databinding.ActivityMainBinding
 import com.grkj.iscs.presentation.PresentationActivity
@@ -56,5 +57,9 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
         mBinding?.createTicket?.setOnClickListener {
             startActivity(Intent(this, CreateTicketActivity::class.java))
         }
+
+        mBinding?.face?.setOnClickListener {
+            startActivity(Intent(this, FaceActivity::class.java))
+        }
     }
 }

+ 77 - 0
app/src/main/res/layout/activity_face.xml

@@ -0,0 +1,77 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:id="@+id/main"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    tools:context=".FaceActivity">
+
+    <FrameLayout
+        android:id="@+id/frameLayout"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:background="@color/black"
+        app:layout_constraintLeft_toLeftOf="parent"
+        app:layout_constraintRight_toRightOf="parent">
+
+        <androidx.camera.view.PreviewView
+            android:id="@+id/preview"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:visibility="invisible" />
+
+        <androidx.appcompat.widget.LinearLayoutCompat
+            android:id="@+id/imageContainer"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:background="@color/black"
+            android:gravity="center"
+            android:orientation="vertical">
+
+            <ImageView
+                android:id="@+id/image"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:scaleType="centerCrop"
+                app:layout_constraintBottom_toBottomOf="parent"
+                app:layout_constraintTop_toBottomOf="@+id/cameraType"
+                tools:ignore="ContentDescription" />
+
+        </androidx.appcompat.widget.LinearLayoutCompat>
+
+    </FrameLayout>
+
+    <androidx.appcompat.widget.LinearLayoutCompat
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="horizontal"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent">
+
+        <Button
+            android:id="@+id/startDetector"
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:layout_weight="1"
+            android:text="开始识别" />
+
+        <Button
+            android:id="@+id/stopDetector"
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:layout_marginStart="10dp"
+            android:layout_marginEnd="10dp"
+            android:layout_weight="1"
+            android:text="切换镜头" />
+
+        <Button
+            android:id="@+id/continueDetector"
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:layout_weight="1"
+            android:text="继续识别" />
+
+    </androidx.appcompat.widget.LinearLayoutCompat>
+</androidx.constraintlayout.widget.ConstraintLayout>

+ 10 - 0
app/src/main/res/layout/activity_main.xml

@@ -126,4 +126,14 @@
             android:textSize="10sp"
             android:layout_margin="5dp"/>
     </LinearLayout>
+
+    <Button
+        android:id="@+id/face"
+        android:layout_width="wrap_content"
+        android:layout_height="50dp"
+        android:layout_margin="5dp"
+        android:minWidth="0dp"
+        android:minHeight="0dp"
+        android:text="Face"
+        android:textSize="10sp" />
 </LinearLayout>

BIN
app/src/main/res/mipmap/ic_face_frame.png