fixed some errors
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user