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.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()
|
||||||
|
|||||||
@@ -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()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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">当前参数:欧氏 <= %1$s,余弦 >= %2$s</string>
|
||||||
|
<string name="suggested_matcher_config_summary">建议参数(基于 %1$d 名学生):欧氏 <= %2$s,余弦 >= %3$s,点击“保存识别参数”生效</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
Reference in New Issue
Block a user