fixed a bug
This commit is contained in:
@@ -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<DetectionUi>) {
|
||||
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<RawDetection>): List<DetectionUi> {
|
||||
val previewWidth = previewView.width.toFloat()
|
||||
val previewHeight = previewView.height.toFloat()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,4 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<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">当前参数:欧氏 <= %1$s,余弦 >= %2$s</string>
|
||||
<string name="suggested_matcher_config_summary">建议参数(基于 %1$d 名学生):欧氏 <= %2$s,余弦 >= %3$s,点击“保存识别参数”生效</string>
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user