fixed some errors

This commit is contained in:
2026-05-10 08:23:15 +08:00
parent 37530a0309
commit db8bc18c64
6 changed files with 44 additions and 53 deletions

View File

@@ -2,6 +2,7 @@ package com.example.studentfaceregistry.face
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.Matrix import android.graphics.Matrix
import kotlin.math.absoluteValue
import kotlin.math.cos import kotlin.math.cos
import kotlin.math.sin import kotlin.math.sin
import kotlin.math.sqrt import kotlin.math.sqrt

View File

@@ -1,7 +1,6 @@
package com.example.studentfaceregistry.face package com.example.studentfaceregistry.face
import com.example.studentfaceregistry.data.Student import com.example.studentfaceregistry.data.Student
import kotlin.math acos
import kotlin.math.sqrt import kotlin.math.sqrt
/** /**

View File

@@ -7,6 +7,7 @@ import com.google.mlkit.vision.common.InputImage
import com.google.mlkit.vision.face.Face import com.google.mlkit.vision.face.Face
import com.google.mlkit.vision.face.FaceDetection import com.google.mlkit.vision.face.FaceDetection
import com.google.mlkit.vision.face.FaceDetectorOptions import com.google.mlkit.vision.face.FaceDetectorOptions
import com.google.mlkit.vision.face.FaceLandmark
import kotlinx.coroutines.tasks.await import kotlinx.coroutines.tasks.await
/** /**
@@ -84,10 +85,8 @@ class FaceProcessor(context: Context) : AutoCloseable {
faceLeft: Int, faceLeft: Int,
faceTop: Int faceTop: Int
): Bitmap { ): Bitmap {
val leftEye = face.leftEye ?: return faceBitmap val leftEye = face.getLandmark(FaceLandmark.LEFT_EYE) ?: return faceBitmap
val rightEye = face.rightEye ?: return faceBitmap val rightEye = face.getLandmark(FaceLandmark.RIGHT_EYE) ?: return faceBitmap
if (!leftEye.visible || !rightEye.visible) return faceBitmap
return aligner.align( return aligner.align(
bitmap = faceBitmap, bitmap = faceBitmap,

View File

@@ -10,7 +10,9 @@ import com.google.mlkit.vision.common.InputImage
import com.google.mlkit.vision.face.Face import com.google.mlkit.vision.face.Face
import com.google.mlkit.vision.face.FaceDetection import com.google.mlkit.vision.face.FaceDetection
import com.google.mlkit.vision.face.FaceDetectorOptions import com.google.mlkit.vision.face.FaceDetectorOptions
import com.google.mlkit.vision.face.FaceLandmark
import kotlinx.coroutines.tasks.await import kotlinx.coroutines.tasks.await
import kotlin.math.PI
import kotlin.math.atan2 import kotlin.math.atan2
import kotlin.math.sqrt import kotlin.math.sqrt
@@ -23,7 +25,7 @@ import kotlin.math.sqrt
* 2. 自动检测并选择最大的人脸 * 2. 自动检测并选择最大的人脸
* 3. 视频注册时自动提取多角度帧并融合特征 * 3. 视频注册时自动提取多角度帧并融合特征
*/ */
class SmartEnrollment(context: Context) { class SmartEnrollment(context: Context) : AutoCloseable {
// 高精度人脸检测器(用于注册) // 高精度人脸检测器(用于注册)
private val detector = FaceDetection.getClient( private val detector = FaceDetection.getClient(
@@ -207,28 +209,26 @@ class SmartEnrollment(context: Context) {
* 计算偏航角(左右转头) * 计算偏航角(左右转头)
*/ */
private fun yawAngle(face: Face): Float { private fun yawAngle(face: Face): Float {
val leftCheek = face.leftCheek ?: return 0f val leftCheek = face.getLandmark(FaceLandmark.LEFT_CHEEK) ?: return 0f
val rightCheek = face.rightCheek ?: return 0f val rightCheek = face.getLandmark(FaceLandmark.RIGHT_CHEEK) ?: return 0f
if (!leftCheek.visible || !rightCheek.visible) return 0f
val dx = rightCheek.position.x - leftCheek.position.x val dx = rightCheek.position.x - leftCheek.position.x
val dy = rightCheek.position.y - leftCheek.position.y val dy = rightCheek.position.y - leftCheek.position.y
return atan2(dy.toDouble(), dx.toDouble()).toFloat() * 180f / 3.14159 return atan2(dy.toDouble(), dx.toDouble()).toFloat() * 180f / PI.toFloat()
} }
/** /**
* 计算俯仰角(上下点头) * 计算俯仰角(上下点头)
*/ */
private fun pitchAngle(face: Face): Float { private fun pitchAngle(face: Face): Float {
val nose = face.nose ?: return 0f val nose = face.getLandmark(FaceLandmark.NOSE_BASE) ?: return 0f
val leftEye = face.leftEye ?: return 0f val leftEye = face.getLandmark(FaceLandmark.LEFT_EYE) ?: return 0f
val rightEye = face.rightEye ?: return 0f val rightEye = face.getLandmark(FaceLandmark.RIGHT_EYE) ?: return 0f
if (!nose.visible || !leftEye.visible || !rightEye.visible) return 0f
val eyeCenterY = (leftEye.position.y + rightEye.position.y) / 2f val eyeCenterY = (leftEye.position.y + rightEye.position.y) / 2f
val dx = nose.position.x - (leftEye.position.x + rightEye.position.x) / 2f val dx = nose.position.x - (leftEye.position.x + rightEye.position.x) / 2f
val dy = nose.position.y - eyeCenterY val dy = nose.position.y - eyeCenterY
return atan2(dy.toDouble(), dx.toDouble()).toFloat() * 180f / 3.14159 return atan2(dy.toDouble(), dx.toDouble()).toFloat() * 180f / PI.toFloat()
} }
/** /**
@@ -360,9 +360,8 @@ class SmartEnrollment(context: Context) {
* 对齐人脸 * 对齐人脸
*/ */
private fun alignFace(bitmap: Bitmap, face: Face, cropRect: Rect): Bitmap { private fun alignFace(bitmap: Bitmap, face: Face, cropRect: Rect): Bitmap {
val leftEye = face.leftEye ?: return bitmap val leftEye = face.getLandmark(FaceLandmark.LEFT_EYE) ?: return bitmap
val rightEye = face.rightEye ?: return bitmap val rightEye = face.getLandmark(FaceLandmark.RIGHT_EYE) ?: return bitmap
if (!leftEye.visible || !rightEye.visible) return bitmap
return aligner.align( return aligner.align(
bitmap = bitmap, bitmap = bitmap,

View File

@@ -6,7 +6,9 @@ import com.google.mlkit.vision.common.InputImage
import com.google.mlkit.vision.face.Face import com.google.mlkit.vision.face.Face
import com.google.mlkit.vision.face.FaceDetection import com.google.mlkit.vision.face.FaceDetection
import com.google.mlkit.vision.face.FaceDetectorOptions import com.google.mlkit.vision.face.FaceDetectorOptions
import com.google.mlkit.vision.face.FaceLandmark
import kotlinx.coroutines.tasks.await import kotlinx.coroutines.tasks.await
import kotlin.math.PI
import kotlin.math.abs import kotlin.math.abs
import kotlin.math.atan2 import kotlin.math.atan2
import kotlin.math.cos import kotlin.math.cos
@@ -22,7 +24,7 @@ import kotlin.math.sqrt
* - 自动选择质量好的帧 * - 自动选择质量好的帧
* - 融合多帧特征,提升识别准确率 * - 融合多帧特征,提升识别准确率
*/ */
class VideoEnrollment(private val context: android.content.Context) { class VideoEnrollment(private val context: android.content.Context) : AutoCloseable {
private val detector = FaceDetection.getClient( private val detector = FaceDetection.getClient(
FaceDetectorOptions.Builder() FaceDetectorOptions.Builder()
@@ -45,7 +47,7 @@ class VideoEnrollment(private val context: android.content.Context) {
frames: List<Bitmap>, frames: List<Bitmap>,
minFrames: Int = 5, minFrames: Int = 5,
maxFrames: Int = 20 maxFrames: Int = 20
): EnrollmentResult { ): VideoEnrollmentResult {
val faceFrames = mutableListOf<FaceFrame>() val faceFrames = mutableListOf<FaceFrame>()
// 检测所有帧中的人脸 // 检测所有帧中的人脸
@@ -71,7 +73,7 @@ class VideoEnrollment(private val context: android.content.Context) {
} }
if (faceFrames.size < minFrames) { if (faceFrames.size < minFrames) {
return EnrollmentResult.Failure( return VideoEnrollmentResult.Failure(
reason = "检测到 ${faceFrames.size} 帧有效人脸,需要至少 $minFrames 帧。请确保视频中人脸清晰且有多角度展示。" reason = "检测到 ${faceFrames.size} 帧有效人脸,需要至少 $minFrames 帧。请确保视频中人脸清晰且有多角度展示。"
) )
} }
@@ -90,7 +92,7 @@ class VideoEnrollment(private val context: android.content.Context) {
// 融合特征(平均 + L2 归一化) // 融合特征(平均 + L2 归一化)
val fusedEmbedding = fuseEmbeddings(embeddings) val fusedEmbedding = fuseEmbeddings(embeddings)
return EnrollmentResult.Success( return VideoEnrollmentResult.Success(
embedding = fusedEmbedding, embedding = fusedEmbedding,
frameCount = selectedFrames.size, frameCount = selectedFrames.size,
totalDetected = faceFrames.size, totalDetected = faceFrames.size,
@@ -105,8 +107,8 @@ class VideoEnrollment(private val context: android.content.Context) {
var score = 1.0f var score = 1.0f
// 可见度分数 // 可见度分数
val leftEyeVisible = face.leftEye?.visible ?: false val leftEyeVisible = face.getLandmark(FaceLandmark.LEFT_EYE) != null
val rightEyeVisible = face.rightEye?.visible ?: false val rightEyeVisible = face.getLandmark(FaceLandmark.RIGHT_EYE) != null
if (!leftEyeVisible || !rightEyeVisible) score *= 0.7f if (!leftEyeVisible || !rightEyeVisible) score *= 0.7f
// 置信度分数 // 置信度分数
@@ -125,16 +127,14 @@ class VideoEnrollment(private val context: android.content.Context) {
* 计算人脸偏航角(左右转头,-90 到 90 度) * 计算人脸偏航角(左右转头,-90 到 90 度)
*/ */
private fun calculateYawAngle(face: Face): Float { private fun calculateYawAngle(face: Face): Float {
val leftCheek = face.leftCheek ?: return 0f val leftCheek = face.getLandmark(FaceLandmark.LEFT_CHEEK) ?: return 0f
val rightCheek = face.rightCheek ?: return 0f val rightCheek = face.getLandmark(FaceLandmark.RIGHT_CHEEK) ?: return 0f
if (!leftCheek.visible || !rightCheek.visible) return 0f
val dx = rightCheek.position.x - leftCheek.position.x val dx = rightCheek.position.x - leftCheek.position.x
val dy = rightCheek.position.y - leftCheek.position.y val dy = rightCheek.position.y - leftCheek.position.y
// 计算角度 // 计算角度
val angle = atan2(dy.toDouble(), dx.toDouble()).toFloat() * 180f / 3.14159 val angle = atan2(dy.toDouble(), dx.toDouble()).toFloat() * 180f / PI.toFloat()
return angle.coerceIn(-90f, 90f) return angle.coerceIn(-90f, 90f)
} }
@@ -143,11 +143,9 @@ class VideoEnrollment(private val context: android.content.Context) {
* 计算人脸俯仰角(上下点头,-90 到 90 度) * 计算人脸俯仰角(上下点头,-90 到 90 度)
*/ */
private fun calculatePitchAngle(face: Face): Float { private fun calculatePitchAngle(face: Face): Float {
val nose = face.nose ?: return 0f val nose = face.getLandmark(FaceLandmark.NOSE_BASE) ?: return 0f
val leftEye = face.leftEye ?: return 0f val leftEye = face.getLandmark(FaceLandmark.LEFT_EYE) ?: return 0f
val rightEye = face.rightEye ?: return 0f val rightEye = face.getLandmark(FaceLandmark.RIGHT_EYE) ?: return 0f
if (!nose.visible || !leftEye.visible || !rightEye.visible) return 0f
// 眼睛中心点 // 眼睛中心点
val eyeCenterY = (leftEye.position.y + rightEye.position.y) / 2f val eyeCenterY = (leftEye.position.y + rightEye.position.y) / 2f
@@ -155,7 +153,7 @@ class VideoEnrollment(private val context: android.content.Context) {
val dx = nose.position.x - (leftEye.position.x + rightEye.position.x) / 2f val dx = nose.position.x - (leftEye.position.x + rightEye.position.x) / 2f
val dy = nose.position.y - eyeCenterY val dy = nose.position.y - eyeCenterY
val angle = atan2(dy.toDouble(), dx.toDouble()).toFloat() * 180f / 3.14159 val angle = atan2(dy.toDouble(), dx.toDouble()).toFloat() * 180f / PI.toFloat()
return angle.coerceIn(-90f, 90f) return angle.coerceIn(-90f, 90f)
} }
@@ -163,8 +161,8 @@ class VideoEnrollment(private val context: android.content.Context) {
/** /**
* 按角度分组人脸帧 * 按角度分组人脸帧
*/ */
private fun groupFramesByAngle(frames: List<FaceFrame>): Map<AngleBucket, List<FaceFrame>> { private fun groupFramesByAngle(frames: List<FaceFrame>): Map<VideoAngleBucket, List<FaceFrame>> {
val buckets = mutableMapOf<AngleBucket, MutableList<FaceFrame>>() val buckets = mutableMapOf<VideoAngleBucket, MutableList<FaceFrame>>()
for (frame in frames) { for (frame in frames) {
val bucket = getAngleBucket(frame.yaw, frame.pitch) val bucket = getAngleBucket(frame.yaw, frame.pitch)
@@ -180,7 +178,7 @@ class VideoEnrollment(private val context: android.content.Context) {
/** /**
* 获取角度分组 * 获取角度分组
*/ */
private fun getAngleBucket(yaw: Float, pitch: Float): AngleBucket { private fun getAngleBucket(yaw: Float, pitch: Float): VideoAngleBucket {
val yawBucket = when { val yawBucket = when {
yaw < -30 -> -2 // 左 yaw < -30 -> -2 // 左
yaw < -10 -> -1 // 左前 yaw < -10 -> -1 // 左前
@@ -195,14 +193,14 @@ class VideoEnrollment(private val context: android.content.Context) {
else -> 1 // 下 else -> 1 // 下
} }
return AngleBucket(yawBucket, pitchBucket) return VideoAngleBucket(yawBucket, pitchBucket)
} }
/** /**
* 从每组中选择质量最好的帧 * 从每组中选择质量最好的帧
*/ */
private fun selectBestFramesFromGroups( private fun selectBestFramesFromGroups(
groups: Map<AngleBucket, List<FaceFrame>>, groups: Map<VideoAngleBucket, List<FaceFrame>>,
maxFrames: Int maxFrames: Int
): List<FaceFrame> { ): List<FaceFrame> {
val selected = mutableListOf<FaceFrame>() val selected = mutableListOf<FaceFrame>()
@@ -314,10 +312,8 @@ class VideoEnrollment(private val context: android.content.Context) {
* 对齐人脸 * 对齐人脸
*/ */
private fun alignFace(bitmap: Bitmap, face: Face): Bitmap { private fun alignFace(bitmap: Bitmap, face: Face): Bitmap {
val leftEye = face.leftEye ?: return bitmap val leftEye = face.getLandmark(FaceLandmark.LEFT_EYE) ?: return bitmap
val rightEye = face.rightEye ?: return bitmap val rightEye = face.getLandmark(FaceLandmark.RIGHT_EYE) ?: return bitmap
if (!leftEye.visible || !rightEye.visible) return bitmap
val aligner = FaceAligner() val aligner = FaceAligner()
return aligner.align( return aligner.align(
@@ -363,7 +359,7 @@ data class FaceFrame(
/** /**
* 角度分组 * 角度分组
*/ */
data class AngleBucket(val yaw: Int, val pitch: Int) data class VideoAngleBucket(val yaw: Int, val pitch: Int)
/** /**
* 角度覆盖范围 * 角度覆盖范围
@@ -380,13 +376,13 @@ data class AngleCoverage(
/** /**
* 注册结果 * 注册结果
*/ */
sealed class EnrollmentResult { sealed class VideoEnrollmentResult {
data class Success( data class Success(
val embedding: FloatArray, val embedding: FloatArray,
val frameCount: Int, val frameCount: Int,
val totalDetected: Int, val totalDetected: Int,
val angleCoverage: AngleCoverage val angleCoverage: AngleCoverage
) : EnrollmentResult() ) : VideoEnrollmentResult()
data class Failure(val reason: String) : EnrollmentResult() data class Failure(val reason: String) : VideoEnrollmentResult()
} }

View File

@@ -5,10 +5,7 @@ import android.graphics.Canvas
import android.graphics.Color import android.graphics.Color
import android.graphics.Paint import android.graphics.Paint
import android.graphics.Rect import android.graphics.Rect
import android.graphics.RippleDrawable
import android.graphics.Typeface import android.graphics.Typeface
import android.graphics.drawable.ShapeDrawable
import android.graphics.drawable.shapes.RoundRectShape
import android.util.AttributeSet import android.util.AttributeSet
import android.view.View import android.view.View
import com.example.studentfaceregistry.face.MatchType import com.example.studentfaceregistry.face.MatchType
@@ -96,7 +93,7 @@ class OverlayView @JvmOverloads constructor(
// 标签背景颜色与框颜色一致 // 标签背景颜色与框颜色一致
labelBackgroundPaint.color = boxColor labelBackgroundPaint.color = boxColor
canvas.drawRoundRect(labelLeft, labelTop, labelRight, labelBottom, 8f, labelBackgroundPaint) canvas.drawRoundRect(labelLeft, labelTop, labelRight, labelBottom, 8f, 8f, labelBackgroundPaint)
// 绘制标签文字 // 绘制标签文字
canvas.drawText(label, labelLeft + padding, labelTop + 38f, labelPaint) canvas.drawText(label, labelLeft + padding, labelTop + 38f, labelPaint)
@@ -112,7 +109,7 @@ class OverlayView @JvmOverloads constructor(
// 置信度背景(半透明黑色) // 置信度背景(半透明黑色)
labelBackgroundPaint.color = Color.argb(180, 0, 0, 0) labelBackgroundPaint.color = Color.argb(180, 0, 0, 0)
canvas.drawRoundRect(confLeft, confTop, confRight, confBottom, 6f, labelBackgroundPaint) canvas.drawRoundRect(confLeft, confTop, confRight, confBottom, 6f, 6f, labelBackgroundPaint)
// 置信度文字 // 置信度文字
canvas.drawText(confidenceText, confLeft + 8f, confTop + 30f, confidencePaint) canvas.drawText(confidenceText, confLeft + 8f, confTop + 30f, confidencePaint)