fixed a bug

This commit is contained in:
2026-08-07 11:02:08 +08:00
parent 0dd6ef9978
commit ddfb57c02c
3 changed files with 76 additions and 44 deletions

View File

@@ -27,8 +27,11 @@ import androidx.camera.core.ExperimentalGetImage
import androidx.camera.core.ImageAnalysis import androidx.camera.core.ImageAnalysis
import androidx.camera.core.ImageProxy import androidx.camera.core.ImageProxy
import androidx.camera.core.Preview import androidx.camera.core.Preview
import androidx.camera.core.resolutionselector.ResolutionSelector
import androidx.camera.core.resolutionselector.ResolutionStrategy
import androidx.camera.lifecycle.ProcessCameraProvider import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.view.PreviewView import androidx.camera.view.PreviewView
import androidx.core.content.edit
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.example.studentfaceregistry.data.Student import com.example.studentfaceregistry.data.Student
@@ -98,8 +101,8 @@ class MainActivity : AppCompatActivity() {
startRecognitionSession() startRecognitionSession()
} }
} else if (currentMode == AppMode.RECOGNITION) { } else if (currentMode == AppMode.RECOGNITION) {
recognitionStatusText.text = "需要相机权限才能识别学生身份。" recognitionStatusText.setText(R.string.camera_permission_required)
toast("需要相机权限才能识别学生身份。") toast(getString(R.string.camera_permission_required))
} }
} }
@@ -111,7 +114,7 @@ class MainActivity : AppCompatActivity() {
lifecycleScope.launch { lifecycleScope.launch {
repository.students.collectLatest { list -> repository.students.collectLatest { list ->
students = list students = list
countText.text = "已入库 ${list.size} 名学生" countText.text = getString(R.string.registered_student_count, list.size)
} }
} }
@@ -354,7 +357,7 @@ class MainActivity : AppCompatActivity() {
addView(uploadStatusText) addView(uploadStatusText)
addView(TextView(this@MainActivity).apply { addView(TextView(this@MainActivity).apply {
text = "在同一 Wi-Fi 下,用电脑浏览器打开下面的地址,就可以上传学生图片或视频。" setText(R.string.upload_instructions)
textSize = 16f textSize = 16f
setTextColor(0xFF475569.toInt()) setTextColor(0xFF475569.toInt())
gravity = Gravity.CENTER gravity = Gravity.CENTER
@@ -460,8 +463,8 @@ class MainActivity : AppCompatActivity() {
if (faceProcessor == null) { if (faceProcessor == null) {
val error = created.exceptionOrNull() val error = created.exceptionOrNull()
val message = error?.message ?: error?.javaClass?.simpleName ?: "unknown error" val message = error?.message ?: error?.javaClass?.simpleName ?: "unknown error"
recognitionStatusText.text = "模型初始化失败:$message" recognitionStatusText.text = getString(R.string.model_init_failed, message)
toast("模型初始化失败:$message") toast(getString(R.string.model_init_failed, message))
return@withContext return@withContext
} }
processor = faceProcessor processor = faceProcessor
@@ -482,11 +485,11 @@ class MainActivity : AppCompatActivity() {
} }
val preview = Preview.Builder() val preview = Preview.Builder()
.setTargetResolution(ANALYSIS_SIZE) .setResolutionSelector(analysisResolutionSelector())
.build() .build()
.also { it.setSurfaceProvider(previewView.surfaceProvider) } .also { it.surfaceProvider = previewView.surfaceProvider }
val analysis = ImageAnalysis.Builder() val analysis = ImageAnalysis.Builder()
.setTargetResolution(ANALYSIS_SIZE) .setResolutionSelector(analysisResolutionSelector())
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.build() .build()
@@ -630,7 +633,10 @@ class MainActivity : AppCompatActivity() {
Log.e("MainActivity", "Recognition failed", e) Log.e("MainActivity", "Recognition failed", e)
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
if (currentMode == AppMode.RECOGNITION) { if (currentMode == AppMode.RECOGNITION) {
recognitionStatusText.text = "识别失败:${e.message ?: e.javaClass.simpleName}" recognitionStatusText.text = getString(
R.string.recognition_failed,
e.message ?: e.javaClass.simpleName
)
} }
} }
} finally { } finally {
@@ -642,7 +648,7 @@ class MainActivity : AppCompatActivity() {
private fun updateRecognitionStatus(detections: List<DetectionUi>) { private fun updateRecognitionStatus(detections: List<DetectionUi>) {
if (detections.isEmpty()) { if (detections.isEmpty()) {
recognitionStatusText.text = "未检测到人脸" recognitionStatusText.setText(R.string.no_face_detected)
return return
} }
@@ -656,30 +662,35 @@ class MainActivity : AppCompatActivity() {
if (matched.size == 1 && total == 1) { if (matched.size == 1 && total == 1) {
val student = matched[0].result.student!! val student = matched[0].result.student!!
val confidence = (matched[0].confidence!! * 100).toInt() val confidence = (matched[0].confidence!! * 100).toInt()
recognitionStatusText.text = "识别到:${student.name}${student.studentNo})置信度${confidence}%" recognitionStatusText.text = getString(
R.string.single_student_recognized,
student.name,
student.studentNo,
confidence
)
} else { } else {
val names = matched.joinToString(", ") { val names = matched.joinToString(", ") {
val student = it.result.student!! val student = it.result.student!!
val confidence = (it.confidence!! * 100).toInt() val confidence = (it.confidence!! * 100).toInt()
"${student.name}(${confidence}%)" getString(R.string.student_confidence_item, student.name, confidence)
} }
val remaining = total - matched.size val remaining = total - matched.size
recognitionStatusText.text = if (remaining > 0) { recognitionStatusText.text = if (remaining > 0) {
"识别到 ${matched.size}/$total 人:$names,另有 $remaining 人跟踪中" getString(R.string.multi_student_recognized_with_tracking, matched.size, total, names, remaining)
} else { } else {
"识别到 ${matched.size}/$total 人:$names" getString(R.string.multi_student_recognized, matched.size, total, names)
} }
} }
} else if (unsure.isNotEmpty()) { } else if (unsure.isNotEmpty()) {
recognitionStatusText.text = "疑似检测到 ${unsure.size} 人(不确定)" recognitionStatusText.text = getString(R.string.unsure_faces_detected, unsure.size)
} else if (tracking > 0) { } else if (tracking > 0) {
recognitionStatusText.text = if (noMatch > 0) { recognitionStatusText.text = if (noMatch > 0) {
"跟踪到 $total 人,识别中 $tracking 人,$noMatch 人未匹配" getString(R.string.tracking_with_no_match, total, tracking, noMatch)
} else { } else {
"跟踪到 $total 人,识别中 $tracking" getString(R.string.tracking_faces, total, tracking)
} }
} else { } else {
recognitionStatusText.text = "检测到 $total 张未入库人脸" recognitionStatusText.text = getString(R.string.unregistered_faces_detected, total)
} }
} }
@@ -700,7 +711,7 @@ class MainActivity : AppCompatActivity() {
uploadUrlText.text = server.accessUrl() uploadUrlText.text = server.accessUrl()
}.onFailure { }.onFailure {
val message = it.message ?: "unknown error" val message = it.message ?: "unknown error"
uploadStatusText.text = "上传服务启动失败:$message" uploadStatusText.text = getString(R.string.upload_service_start_failed, message)
uploadUrlText.text = "-" uploadUrlText.text = "-"
} }
} }
@@ -748,10 +759,10 @@ class MainActivity : AppCompatActivity() {
euclideanThreshold = parsedEuclidean euclideanThreshold = parsedEuclidean
cosineThreshold = parsedCosine cosineThreshold = parsedCosine
matcher = FaceMatcher(euclideanThreshold, cosineThreshold) matcher = FaceMatcher(euclideanThreshold, cosineThreshold)
matcherPrefs.edit() matcherPrefs.edit {
.putFloat(KEY_EUCLIDEAN_THRESHOLD, euclideanThreshold) putFloat(KEY_EUCLIDEAN_THRESHOLD, euclideanThreshold)
.putFloat(KEY_COSINE_THRESHOLD, cosineThreshold) putFloat(KEY_COSINE_THRESHOLD, cosineThreshold)
.apply() }
matcherConfigSummaryText.text = matcherConfigSummary() matcherConfigSummaryText.text = matcherConfigSummary()
recognitionStatusText.text = "识别参数已更新" recognitionStatusText.text = "识别参数已更新"
@@ -759,7 +770,11 @@ class MainActivity : AppCompatActivity() {
} }
private fun matcherConfigSummary(): String { private fun matcherConfigSummary(): String {
return "当前参数:欧氏 <= ${formatThreshold(euclideanThreshold)},余弦 >= ${formatThreshold(cosineThreshold)}" return getString(
R.string.matcher_config_summary,
formatThreshold(euclideanThreshold),
formatThreshold(cosineThreshold)
)
} }
private fun applySuggestedThresholds() { private fun applySuggestedThresholds() {
@@ -770,9 +785,12 @@ class MainActivity : AppCompatActivity() {
val suggested = matcher.suggestThresholds(students) val suggested = matcher.suggestThresholds(students)
euclideanThresholdInput.setText(formatThreshold(suggested.euclidean)) euclideanThresholdInput.setText(formatThreshold(suggested.euclidean))
cosineThresholdInput.setText(formatThreshold(suggested.cosine)) cosineThresholdInput.setText(formatThreshold(suggested.cosine))
matcherConfigSummaryText.text = matcherConfigSummaryText.text = getString(
"建议参数(基于 ${students.size} 名学生):欧氏 <= ${formatThreshold(suggested.euclidean)}" + R.string.suggested_matcher_config_summary,
"余弦 >= ${formatThreshold(suggested.cosine)},点击“保存识别参数”生效" students.size,
formatThreshold(suggested.euclidean),
formatThreshold(suggested.cosine)
)
toast("已填入建议阈值,确认后点击保存") toast("已填入建议阈值,确认后点击保存")
} }
@@ -780,6 +798,17 @@ class MainActivity : AppCompatActivity() {
return String.format(java.util.Locale.US, "%.2f", value) return String.format(java.util.Locale.US, "%.2f", value)
} }
private fun analysisResolutionSelector(): ResolutionSelector {
return ResolutionSelector.Builder()
.setResolutionStrategy(
ResolutionStrategy(
ANALYSIS_SIZE,
ResolutionStrategy.FALLBACK_RULE_CLOSEST_HIGHER_THEN_LOWER
)
)
.build()
}
private fun mapDetectionsToPreview(image: ImageProxy, rawDetections: List<RawDetection>): List<DetectionUi> { private fun mapDetectionsToPreview(image: ImageProxy, rawDetections: List<RawDetection>): List<DetectionUi> {
val previewWidth = previewView.width.toFloat() val previewWidth = previewView.width.toFloat()
val previewHeight = previewView.height.toFloat() val previewHeight = previewView.height.toFloat()

View File

@@ -13,8 +13,6 @@ import com.google.mlkit.vision.face.FaceDetectorOptions
import com.google.mlkit.vision.face.FaceLandmark import com.google.mlkit.vision.face.FaceLandmark
import kotlinx.coroutines.tasks.await import kotlinx.coroutines.tasks.await
import kotlin.math.abs import kotlin.math.abs
import kotlin.math.PI
import kotlin.math.atan2
import kotlin.math.roundToInt import kotlin.math.roundToInt
import kotlin.math.sqrt import kotlin.math.sqrt
@@ -240,26 +238,14 @@ class SmartEnrollment(context: Context) : AutoCloseable {
* 计算偏航角(左右转头) * 计算偏航角(左右转头)
*/ */
private fun yawAngle(face: Face): Float { private fun yawAngle(face: Face): Float {
val leftCheek = face.getLandmark(FaceLandmark.LEFT_CHEEK) ?: return 0f return face.headEulerAngleY
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 / PI.toFloat()
} }
/** /**
* 计算俯仰角(上下点头) * 计算俯仰角(上下点头)
*/ */
private fun pitchAngle(face: Face): Float { private fun pitchAngle(face: Face): Float {
val nose = face.getLandmark(FaceLandmark.NOSE_BASE) ?: return 0f return face.headEulerAngleX
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 / PI.toFloat()
} }
/** /**

View File

@@ -1,4 +1,21 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="app_name">学生人脸识别</string> <string name="app_name">学生人脸识别</string>
<string name="camera_permission_required">需要相机权限才能识别学生身份。</string>
<string name="registered_student_count">已入库 %1$d 名学生</string>
<string name="upload_instructions">在同一 Wi-Fi 下,用电脑浏览器打开下面的地址,就可以上传学生图片或视频。</string>
<string name="model_init_failed">模型初始化失败:%1$s</string>
<string name="recognition_failed">识别失败:%1$s</string>
<string name="no_face_detected">未检测到人脸</string>
<string name="single_student_recognized">识别到:%1$s%2$s置信度%3$d%%</string>
<string name="student_confidence_item">%1$s(%2$d%%)</string>
<string name="multi_student_recognized_with_tracking">识别到 %1$d/%2$d 人:%3$s另有 %4$d 人跟踪中</string>
<string name="multi_student_recognized">识别到 %1$d/%2$d 人:%3$s</string>
<string name="unsure_faces_detected">疑似检测到 %1$d 人(不确定)</string>
<string name="tracking_with_no_match">跟踪到 %1$d 人,识别中 %2$d 人,%3$d 人未匹配</string>
<string name="tracking_faces">跟踪到 %1$d 人,识别中 %2$d 人</string>
<string name="unregistered_faces_detected">检测到 %1$d 张未入库人脸</string>
<string name="upload_service_start_failed">上传服务启动失败:%1$s</string>
<string name="matcher_config_summary">当前参数:欧氏 &lt;= %1$s余弦 &gt;= %2$s</string>
<string name="suggested_matcher_config_summary">建议参数(基于 %1$d 名学生):欧氏 &lt;= %2$s余弦 &gt;= %3$s点击“保存识别参数”生效</string>
</resources> </resources>