fixed some errors
This commit is contained in:
@@ -2,6 +2,7 @@ package com.example.studentfaceregistry.face
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Matrix
|
||||
import kotlin.math.absoluteValue
|
||||
import kotlin.math.cos
|
||||
import kotlin.math.sin
|
||||
import kotlin.math.sqrt
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package com.example.studentfaceregistry.face
|
||||
|
||||
import com.example.studentfaceregistry.data.Student
|
||||
import kotlin.math acos
|
||||
import kotlin.math.sqrt
|
||||
|
||||
/**
|
||||
|
||||
@@ -7,6 +7,7 @@ import com.google.mlkit.vision.common.InputImage
|
||||
import com.google.mlkit.vision.face.Face
|
||||
import com.google.mlkit.vision.face.FaceDetection
|
||||
import com.google.mlkit.vision.face.FaceDetectorOptions
|
||||
import com.google.mlkit.vision.face.FaceLandmark
|
||||
import kotlinx.coroutines.tasks.await
|
||||
|
||||
/**
|
||||
@@ -84,10 +85,8 @@ class FaceProcessor(context: Context) : AutoCloseable {
|
||||
faceLeft: Int,
|
||||
faceTop: Int
|
||||
): Bitmap {
|
||||
val leftEye = face.leftEye ?: return faceBitmap
|
||||
val rightEye = face.rightEye ?: return faceBitmap
|
||||
|
||||
if (!leftEye.visible || !rightEye.visible) return faceBitmap
|
||||
val leftEye = face.getLandmark(FaceLandmark.LEFT_EYE) ?: return faceBitmap
|
||||
val rightEye = face.getLandmark(FaceLandmark.RIGHT_EYE) ?: return faceBitmap
|
||||
|
||||
return aligner.align(
|
||||
bitmap = faceBitmap,
|
||||
|
||||
@@ -10,7 +10,9 @@ import com.google.mlkit.vision.common.InputImage
|
||||
import com.google.mlkit.vision.face.Face
|
||||
import com.google.mlkit.vision.face.FaceDetection
|
||||
import com.google.mlkit.vision.face.FaceDetectorOptions
|
||||
import com.google.mlkit.vision.face.FaceLandmark
|
||||
import kotlinx.coroutines.tasks.await
|
||||
import kotlin.math.PI
|
||||
import kotlin.math.atan2
|
||||
import kotlin.math.sqrt
|
||||
|
||||
@@ -23,7 +25,7 @@ import kotlin.math.sqrt
|
||||
* 2. 自动检测并选择最大的人脸
|
||||
* 3. 视频注册时自动提取多角度帧并融合特征
|
||||
*/
|
||||
class SmartEnrollment(context: Context) {
|
||||
class SmartEnrollment(context: Context) : AutoCloseable {
|
||||
|
||||
// 高精度人脸检测器(用于注册)
|
||||
private val detector = FaceDetection.getClient(
|
||||
@@ -207,28 +209,26 @@ class SmartEnrollment(context: Context) {
|
||||
* 计算偏航角(左右转头)
|
||||
*/
|
||||
private fun yawAngle(face: Face): Float {
|
||||
val leftCheek = face.leftCheek ?: return 0f
|
||||
val rightCheek = face.rightCheek ?: return 0f
|
||||
if (!leftCheek.visible || !rightCheek.visible) return 0f
|
||||
val leftCheek = face.getLandmark(FaceLandmark.LEFT_CHEEK) ?: return 0f
|
||||
val rightCheek = face.getLandmark(FaceLandmark.RIGHT_CHEEK) ?: return 0f
|
||||
|
||||
val dx = rightCheek.position.x - leftCheek.position.x
|
||||
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 {
|
||||
val nose = face.nose ?: return 0f
|
||||
val leftEye = face.leftEye ?: return 0f
|
||||
val rightEye = face.rightEye ?: return 0f
|
||||
if (!nose.visible || !leftEye.visible || !rightEye.visible) return 0f
|
||||
val nose = face.getLandmark(FaceLandmark.NOSE_BASE) ?: return 0f
|
||||
val leftEye = face.getLandmark(FaceLandmark.LEFT_EYE) ?: return 0f
|
||||
val rightEye = face.getLandmark(FaceLandmark.RIGHT_EYE) ?: return 0f
|
||||
|
||||
val eyeCenterY = (leftEye.position.y + rightEye.position.y) / 2f
|
||||
val dx = nose.position.x - (leftEye.position.x + rightEye.position.x) / 2f
|
||||
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 {
|
||||
val leftEye = face.leftEye ?: return bitmap
|
||||
val rightEye = face.rightEye ?: return bitmap
|
||||
if (!leftEye.visible || !rightEye.visible) return bitmap
|
||||
val leftEye = face.getLandmark(FaceLandmark.LEFT_EYE) ?: return bitmap
|
||||
val rightEye = face.getLandmark(FaceLandmark.RIGHT_EYE) ?: return bitmap
|
||||
|
||||
return aligner.align(
|
||||
bitmap = bitmap,
|
||||
|
||||
@@ -6,7 +6,9 @@ import com.google.mlkit.vision.common.InputImage
|
||||
import com.google.mlkit.vision.face.Face
|
||||
import com.google.mlkit.vision.face.FaceDetection
|
||||
import com.google.mlkit.vision.face.FaceDetectorOptions
|
||||
import com.google.mlkit.vision.face.FaceLandmark
|
||||
import kotlinx.coroutines.tasks.await
|
||||
import kotlin.math.PI
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.atan2
|
||||
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(
|
||||
FaceDetectorOptions.Builder()
|
||||
@@ -45,7 +47,7 @@ class VideoEnrollment(private val context: android.content.Context) {
|
||||
frames: List<Bitmap>,
|
||||
minFrames: Int = 5,
|
||||
maxFrames: Int = 20
|
||||
): EnrollmentResult {
|
||||
): VideoEnrollmentResult {
|
||||
val faceFrames = mutableListOf<FaceFrame>()
|
||||
|
||||
// 检测所有帧中的人脸
|
||||
@@ -71,7 +73,7 @@ class VideoEnrollment(private val context: android.content.Context) {
|
||||
}
|
||||
|
||||
if (faceFrames.size < minFrames) {
|
||||
return EnrollmentResult.Failure(
|
||||
return VideoEnrollmentResult.Failure(
|
||||
reason = "检测到 ${faceFrames.size} 帧有效人脸,需要至少 $minFrames 帧。请确保视频中人脸清晰且有多角度展示。"
|
||||
)
|
||||
}
|
||||
@@ -90,7 +92,7 @@ class VideoEnrollment(private val context: android.content.Context) {
|
||||
// 融合特征(平均 + L2 归一化)
|
||||
val fusedEmbedding = fuseEmbeddings(embeddings)
|
||||
|
||||
return EnrollmentResult.Success(
|
||||
return VideoEnrollmentResult.Success(
|
||||
embedding = fusedEmbedding,
|
||||
frameCount = selectedFrames.size,
|
||||
totalDetected = faceFrames.size,
|
||||
@@ -105,8 +107,8 @@ class VideoEnrollment(private val context: android.content.Context) {
|
||||
var score = 1.0f
|
||||
|
||||
// 可见度分数
|
||||
val leftEyeVisible = face.leftEye?.visible ?: false
|
||||
val rightEyeVisible = face.rightEye?.visible ?: false
|
||||
val leftEyeVisible = face.getLandmark(FaceLandmark.LEFT_EYE) != null
|
||||
val rightEyeVisible = face.getLandmark(FaceLandmark.RIGHT_EYE) != null
|
||||
if (!leftEyeVisible || !rightEyeVisible) score *= 0.7f
|
||||
|
||||
// 置信度分数
|
||||
@@ -125,16 +127,14 @@ class VideoEnrollment(private val context: android.content.Context) {
|
||||
* 计算人脸偏航角(左右转头,-90 到 90 度)
|
||||
*/
|
||||
private fun calculateYawAngle(face: Face): Float {
|
||||
val leftCheek = face.leftCheek ?: return 0f
|
||||
val rightCheek = face.rightCheek ?: return 0f
|
||||
|
||||
if (!leftCheek.visible || !rightCheek.visible) return 0f
|
||||
val leftCheek = face.getLandmark(FaceLandmark.LEFT_CHEEK) ?: return 0f
|
||||
val rightCheek = face.getLandmark(FaceLandmark.RIGHT_CHEEK) ?: return 0f
|
||||
|
||||
val dx = rightCheek.position.x - leftCheek.position.x
|
||||
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)
|
||||
}
|
||||
@@ -143,11 +143,9 @@ class VideoEnrollment(private val context: android.content.Context) {
|
||||
* 计算人脸俯仰角(上下点头,-90 到 90 度)
|
||||
*/
|
||||
private fun calculatePitchAngle(face: Face): Float {
|
||||
val nose = face.nose ?: return 0f
|
||||
val leftEye = face.leftEye ?: return 0f
|
||||
val rightEye = face.rightEye ?: return 0f
|
||||
|
||||
if (!nose.visible || !leftEye.visible || !rightEye.visible) return 0f
|
||||
val nose = face.getLandmark(FaceLandmark.NOSE_BASE) ?: return 0f
|
||||
val leftEye = face.getLandmark(FaceLandmark.LEFT_EYE) ?: return 0f
|
||||
val rightEye = face.getLandmark(FaceLandmark.RIGHT_EYE) ?: return 0f
|
||||
|
||||
// 眼睛中心点
|
||||
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 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)
|
||||
}
|
||||
@@ -163,8 +161,8 @@ class VideoEnrollment(private val context: android.content.Context) {
|
||||
/**
|
||||
* 按角度分组人脸帧
|
||||
*/
|
||||
private fun groupFramesByAngle(frames: List<FaceFrame>): Map<AngleBucket, List<FaceFrame>> {
|
||||
val buckets = mutableMapOf<AngleBucket, MutableList<FaceFrame>>()
|
||||
private fun groupFramesByAngle(frames: List<FaceFrame>): Map<VideoAngleBucket, List<FaceFrame>> {
|
||||
val buckets = mutableMapOf<VideoAngleBucket, MutableList<FaceFrame>>()
|
||||
|
||||
for (frame in frames) {
|
||||
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 {
|
||||
yaw < -30 -> -2 // 左
|
||||
yaw < -10 -> -1 // 左前
|
||||
@@ -195,14 +193,14 @@ class VideoEnrollment(private val context: android.content.Context) {
|
||||
else -> 1 // 下
|
||||
}
|
||||
|
||||
return AngleBucket(yawBucket, pitchBucket)
|
||||
return VideoAngleBucket(yawBucket, pitchBucket)
|
||||
}
|
||||
|
||||
/**
|
||||
* 从每组中选择质量最好的帧
|
||||
*/
|
||||
private fun selectBestFramesFromGroups(
|
||||
groups: Map<AngleBucket, List<FaceFrame>>,
|
||||
groups: Map<VideoAngleBucket, List<FaceFrame>>,
|
||||
maxFrames: Int
|
||||
): List<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 {
|
||||
val leftEye = face.leftEye ?: return bitmap
|
||||
val rightEye = face.rightEye ?: return bitmap
|
||||
|
||||
if (!leftEye.visible || !rightEye.visible) return bitmap
|
||||
val leftEye = face.getLandmark(FaceLandmark.LEFT_EYE) ?: return bitmap
|
||||
val rightEye = face.getLandmark(FaceLandmark.RIGHT_EYE) ?: return bitmap
|
||||
|
||||
val aligner = FaceAligner()
|
||||
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(
|
||||
val embedding: FloatArray,
|
||||
val frameCount: Int,
|
||||
val totalDetected: Int,
|
||||
val angleCoverage: AngleCoverage
|
||||
) : EnrollmentResult()
|
||||
) : VideoEnrollmentResult()
|
||||
|
||||
data class Failure(val reason: String) : EnrollmentResult()
|
||||
data class Failure(val reason: String) : VideoEnrollmentResult()
|
||||
}
|
||||
|
||||
@@ -5,10 +5,7 @@ import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import android.graphics.Paint
|
||||
import android.graphics.Rect
|
||||
import android.graphics.RippleDrawable
|
||||
import android.graphics.Typeface
|
||||
import android.graphics.drawable.ShapeDrawable
|
||||
import android.graphics.drawable.shapes.RoundRectShape
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import com.example.studentfaceregistry.face.MatchType
|
||||
@@ -96,7 +93,7 @@ class OverlayView @JvmOverloads constructor(
|
||||
|
||||
// 标签背景颜色与框颜色一致
|
||||
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)
|
||||
@@ -112,7 +109,7 @@ class OverlayView @JvmOverloads constructor(
|
||||
|
||||
// 置信度背景(半透明黑色)
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user