diff --git a/app/src/main/java/com/example/studentfaceregistry/MainActivity.kt b/app/src/main/java/com/example/studentfaceregistry/MainActivity.kt index 9ad9db1..5d39e34 100644 --- a/app/src/main/java/com/example/studentfaceregistry/MainActivity.kt +++ b/app/src/main/java/com/example/studentfaceregistry/MainActivity.kt @@ -27,8 +27,11 @@ import androidx.camera.core.ExperimentalGetImage import androidx.camera.core.ImageAnalysis import androidx.camera.core.ImageProxy 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.view.PreviewView +import androidx.core.content.edit import androidx.core.content.ContextCompat import androidx.lifecycle.lifecycleScope import com.example.studentfaceregistry.data.Student @@ -98,8 +101,8 @@ class MainActivity : AppCompatActivity() { startRecognitionSession() } } else if (currentMode == AppMode.RECOGNITION) { - recognitionStatusText.text = "需要相机权限才能识别学生身份。" - toast("需要相机权限才能识别学生身份。") + recognitionStatusText.setText(R.string.camera_permission_required) + toast(getString(R.string.camera_permission_required)) } } @@ -111,7 +114,7 @@ class MainActivity : AppCompatActivity() { lifecycleScope.launch { repository.students.collectLatest { 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(TextView(this@MainActivity).apply { - text = "在同一 Wi-Fi 下,用电脑浏览器打开下面的地址,就可以上传学生图片或视频。" + setText(R.string.upload_instructions) textSize = 16f setTextColor(0xFF475569.toInt()) gravity = Gravity.CENTER @@ -460,8 +463,8 @@ class MainActivity : AppCompatActivity() { if (faceProcessor == null) { val error = created.exceptionOrNull() val message = error?.message ?: error?.javaClass?.simpleName ?: "unknown error" - recognitionStatusText.text = "模型初始化失败:$message" - toast("模型初始化失败:$message") + recognitionStatusText.text = getString(R.string.model_init_failed, message) + toast(getString(R.string.model_init_failed, message)) return@withContext } processor = faceProcessor @@ -482,11 +485,11 @@ class MainActivity : AppCompatActivity() { } val preview = Preview.Builder() - .setTargetResolution(ANALYSIS_SIZE) + .setResolutionSelector(analysisResolutionSelector()) .build() - .also { it.setSurfaceProvider(previewView.surfaceProvider) } + .also { it.surfaceProvider = previewView.surfaceProvider } val analysis = ImageAnalysis.Builder() - .setTargetResolution(ANALYSIS_SIZE) + .setResolutionSelector(analysisResolutionSelector()) .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) .build() @@ -630,7 +633,10 @@ class MainActivity : AppCompatActivity() { Log.e("MainActivity", "Recognition failed", e) withContext(Dispatchers.Main) { if (currentMode == AppMode.RECOGNITION) { - recognitionStatusText.text = "识别失败:${e.message ?: e.javaClass.simpleName}" + recognitionStatusText.text = getString( + R.string.recognition_failed, + e.message ?: e.javaClass.simpleName + ) } } } finally { @@ -642,7 +648,7 @@ class MainActivity : AppCompatActivity() { private fun updateRecognitionStatus(detections: List) { if (detections.isEmpty()) { - recognitionStatusText.text = "未检测到人脸" + recognitionStatusText.setText(R.string.no_face_detected) return } @@ -656,30 +662,35 @@ class MainActivity : AppCompatActivity() { if (matched.size == 1 && total == 1) { val student = matched[0].result.student!! 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 { val names = matched.joinToString(", ") { val student = it.result.student!! val confidence = (it.confidence!! * 100).toInt() - "${student.name}(${confidence}%)" + getString(R.string.student_confidence_item, student.name, confidence) } val remaining = total - matched.size recognitionStatusText.text = if (remaining > 0) { - "识别到 ${matched.size}/$total 人:$names,另有 $remaining 人跟踪中" + getString(R.string.multi_student_recognized_with_tracking, matched.size, total, names, remaining) } else { - "识别到 ${matched.size}/$total 人:$names" + getString(R.string.multi_student_recognized, matched.size, total, names) } } } else if (unsure.isNotEmpty()) { - recognitionStatusText.text = "疑似检测到 ${unsure.size} 人(不确定)" + recognitionStatusText.text = getString(R.string.unsure_faces_detected, unsure.size) } else if (tracking > 0) { recognitionStatusText.text = if (noMatch > 0) { - "跟踪到 $total 人,识别中 $tracking 人,$noMatch 人未匹配" + getString(R.string.tracking_with_no_match, total, tracking, noMatch) } else { - "跟踪到 $total 人,识别中 $tracking 人" + getString(R.string.tracking_faces, total, tracking) } } else { - recognitionStatusText.text = "检测到 $total 张未入库人脸" + recognitionStatusText.text = getString(R.string.unregistered_faces_detected, total) } } @@ -700,7 +711,7 @@ class MainActivity : AppCompatActivity() { uploadUrlText.text = server.accessUrl() }.onFailure { val message = it.message ?: "unknown error" - uploadStatusText.text = "上传服务启动失败:$message" + uploadStatusText.text = getString(R.string.upload_service_start_failed, message) uploadUrlText.text = "-" } } @@ -748,10 +759,10 @@ class MainActivity : AppCompatActivity() { euclideanThreshold = parsedEuclidean cosineThreshold = parsedCosine matcher = FaceMatcher(euclideanThreshold, cosineThreshold) - matcherPrefs.edit() - .putFloat(KEY_EUCLIDEAN_THRESHOLD, euclideanThreshold) - .putFloat(KEY_COSINE_THRESHOLD, cosineThreshold) - .apply() + matcherPrefs.edit { + putFloat(KEY_EUCLIDEAN_THRESHOLD, euclideanThreshold) + putFloat(KEY_COSINE_THRESHOLD, cosineThreshold) + } matcherConfigSummaryText.text = matcherConfigSummary() recognitionStatusText.text = "识别参数已更新" @@ -759,7 +770,11 @@ class MainActivity : AppCompatActivity() { } private fun matcherConfigSummary(): String { - return "当前参数:欧氏 <= ${formatThreshold(euclideanThreshold)},余弦 >= ${formatThreshold(cosineThreshold)}" + return getString( + R.string.matcher_config_summary, + formatThreshold(euclideanThreshold), + formatThreshold(cosineThreshold) + ) } private fun applySuggestedThresholds() { @@ -770,9 +785,12 @@ class MainActivity : AppCompatActivity() { val suggested = matcher.suggestThresholds(students) euclideanThresholdInput.setText(formatThreshold(suggested.euclidean)) cosineThresholdInput.setText(formatThreshold(suggested.cosine)) - matcherConfigSummaryText.text = - "建议参数(基于 ${students.size} 名学生):欧氏 <= ${formatThreshold(suggested.euclidean)}," + - "余弦 >= ${formatThreshold(suggested.cosine)},点击“保存识别参数”生效" + matcherConfigSummaryText.text = getString( + R.string.suggested_matcher_config_summary, + students.size, + formatThreshold(suggested.euclidean), + formatThreshold(suggested.cosine) + ) toast("已填入建议阈值,确认后点击保存") } @@ -780,6 +798,17 @@ class MainActivity : AppCompatActivity() { 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): List { val previewWidth = previewView.width.toFloat() val previewHeight = previewView.height.toFloat() diff --git a/app/src/main/java/com/example/studentfaceregistry/face/SmartEnrollment.kt b/app/src/main/java/com/example/studentfaceregistry/face/SmartEnrollment.kt index 419e9e6..88e29ed 100644 --- a/app/src/main/java/com/example/studentfaceregistry/face/SmartEnrollment.kt +++ b/app/src/main/java/com/example/studentfaceregistry/face/SmartEnrollment.kt @@ -13,8 +13,6 @@ import com.google.mlkit.vision.face.FaceDetectorOptions import com.google.mlkit.vision.face.FaceLandmark import kotlinx.coroutines.tasks.await import kotlin.math.abs -import kotlin.math.PI -import kotlin.math.atan2 import kotlin.math.roundToInt import kotlin.math.sqrt @@ -240,26 +238,14 @@ class SmartEnrollment(context: Context) : AutoCloseable { * 计算偏航角(左右转头) */ private fun yawAngle(face: Face): Float { - 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 / PI.toFloat() + return face.headEulerAngleY } /** * 计算俯仰角(上下点头) */ private fun pitchAngle(face: Face): Float { - 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 / PI.toFloat() + return face.headEulerAngleX } /** diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2692cc1..993e040 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,4 +1,21 @@ 学生人脸识别 + 需要相机权限才能识别学生身份。 + 已入库 %1$d 名学生 + 在同一 Wi-Fi 下,用电脑浏览器打开下面的地址,就可以上传学生图片或视频。 + 模型初始化失败:%1$s + 识别失败:%1$s + 未检测到人脸 + 识别到:%1$s(%2$s)置信度%3$d%% + %1$s(%2$d%%) + 识别到 %1$d/%2$d 人:%3$s,另有 %4$d 人跟踪中 + 识别到 %1$d/%2$d 人:%3$s + 疑似检测到 %1$d 人(不确定) + 跟踪到 %1$d 人,识别中 %2$d 人,%3$d 人未匹配 + 跟踪到 %1$d 人,识别中 %2$d 人 + 检测到 %1$d 张未入库人脸 + 上传服务启动失败:%1$s + 当前参数:欧氏 <= %1$s,余弦 >= %2$s + 建议参数(基于 %1$d 名学生):欧氏 <= %2$s,余弦 >= %3$s,点击“保存识别参数”生效