fixed some bugs

This commit is contained in:
2026-05-10 18:38:05 +08:00
parent 9607c92154
commit 4e875819b6
2 changed files with 233 additions and 51 deletions

View File

@@ -1,14 +1,19 @@
package com.example.studentfaceregistry
import android.Manifest
import android.content.SharedPreferences
import android.content.pm.PackageManager
import android.graphics.Rect
import android.graphics.RectF
import android.graphics.Typeface
import android.os.Bundle
import android.os.SystemClock
import android.text.InputType
import android.util.Size
import android.util.Log
import android.view.Gravity
import android.view.View
import android.widget.EditText
import android.widget.Button
import android.widget.FrameLayout
import android.widget.LinearLayout
@@ -22,6 +27,8 @@ import androidx.camera.core.ImageProxy
import androidx.camera.core.Preview
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.view.PreviewView
import androidx.camera.view.transform.CoordinateTransform
import androidx.camera.view.transform.ImageProxyTransformFactory
import androidx.core.content.ContextCompat
import androidx.lifecycle.lifecycleScope
import com.example.studentfaceregistry.data.Student
@@ -29,7 +36,9 @@ import com.example.studentfaceregistry.data.StudentRepository
import com.example.studentfaceregistry.face.FaceMatcher
import com.example.studentfaceregistry.face.FaceProcessor
import com.example.studentfaceregistry.face.MatchType
import com.example.studentfaceregistry.face.RecognitionResult
import com.google.mlkit.vision.common.InputImage
import com.google.mlkit.vision.face.Face
import com.example.studentfaceregistry.ui.DetectionUi
import com.example.studentfaceregistry.ui.OverlayView
import com.example.studentfaceregistry.upload.UploadServer
@@ -44,6 +53,9 @@ class MainActivity : AppCompatActivity() {
private lateinit var previewView: PreviewView
private lateinit var overlayView: OverlayView
private lateinit var recognitionStatusText: TextView
private lateinit var matcherConfigSummaryText: TextView
private lateinit var euclideanThresholdInput: EditText
private lateinit var cosineThresholdInput: EditText
private lateinit var uploadStatusText: TextView
private lateinit var uploadUrlText: TextView
private lateinit var pauseRecognitionButton: Button
@@ -58,12 +70,22 @@ class MainActivity : AppCompatActivity() {
private var cameraProvider: ProcessCameraProvider? = null
private val repository by lazy { StudentRepository(this) }
private val matcher = FaceMatcher(euclideanThreshold = 1.0f, cosineThreshold = 0.6f)
private val matcherPrefs: SharedPreferences by lazy {
getSharedPreferences(MATCHER_PREFS_NAME, MODE_PRIVATE)
}
private val imageProxyTransformFactory = ImageProxyTransformFactory().apply {
setUsingCropRect(false)
setUsingRotationDegrees(false)
}
private val cameraExecutor = Executors.newSingleThreadExecutor()
private var students: List<Student> = emptyList()
private var recognitionEnabled = true
private var analyzing = false
private var lastAnalysisAt = 0L
private var euclideanThreshold = DEFAULT_EUCLIDEAN_THRESHOLD
private var cosineThreshold = DEFAULT_COSINE_THRESHOLD
@Volatile private var matcher = FaceMatcher(DEFAULT_EUCLIDEAN_THRESHOLD, DEFAULT_COSINE_THRESHOLD)
private val recognitionHistory = mutableMapOf<Int, ArrayDeque<RecognitionResult>>()
private val permissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestMultiplePermissions()
@@ -80,6 +102,7 @@ class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
loadMatcherConfig()
setContentView(createContentView())
lifecycleScope.launch {
@@ -197,6 +220,74 @@ class MainActivity : AppCompatActivity() {
}
addView(recognitionStatusText)
val matcherConfigPanel = LinearLayout(this@MainActivity).apply {
orientation = LinearLayout.VERTICAL
setPadding(16, 16, 16, 16)
setBackgroundColor(0xFFF1F5F9.toInt())
}
matcherConfigSummaryText = TextView(this@MainActivity).apply {
textSize = 14f
setTextColor(0xFF475569.toInt())
text = matcherConfigSummary()
setPadding(0, 0, 0, 12)
}
matcherConfigPanel.addView(matcherConfigSummaryText)
val thresholdRow = LinearLayout(this@MainActivity).apply {
orientation = LinearLayout.HORIZONTAL
}
val euclideanColumn = LinearLayout(this@MainActivity).apply {
orientation = LinearLayout.VERTICAL
layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)
setPadding(0, 0, 12, 0)
}
euclideanColumn.addView(TextView(this@MainActivity).apply {
text = "欧氏阈值"
textSize = 13f
setTextColor(0xFF334155.toInt())
})
euclideanThresholdInput = EditText(this@MainActivity).apply {
setText(formatThreshold(euclideanThreshold))
hint = "0.00"
inputType = InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_FLAG_DECIMAL
setTextColor(0xFF0F172A.toInt())
setBackgroundColor(0xFFFFFFFF.toInt())
setPadding(20, 18, 20, 18)
}
euclideanColumn.addView(euclideanThresholdInput)
val cosineColumn = LinearLayout(this@MainActivity).apply {
orientation = LinearLayout.VERTICAL
layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)
}
cosineColumn.addView(TextView(this@MainActivity).apply {
text = "余弦阈值"
textSize = 13f
setTextColor(0xFF334155.toInt())
})
cosineThresholdInput = EditText(this@MainActivity).apply {
setText(formatThreshold(cosineThreshold))
hint = "0.00"
inputType = InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_FLAG_DECIMAL or InputType.TYPE_NUMBER_FLAG_SIGNED
setTextColor(0xFF0F172A.toInt())
setBackgroundColor(0xFFFFFFFF.toInt())
setPadding(20, 18, 20, 18)
}
cosineColumn.addView(cosineThresholdInput)
thresholdRow.addView(euclideanColumn)
thresholdRow.addView(cosineColumn)
matcherConfigPanel.addView(thresholdRow)
matcherConfigPanel.addView(Button(this@MainActivity).apply {
text = "保存识别参数"
setOnClickListener { saveMatcherConfig() }
})
addView(matcherConfigPanel)
val cameraFrame = FrameLayout(this@MainActivity).apply {
layoutParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
@@ -206,6 +297,7 @@ class MainActivity : AppCompatActivity() {
}
previewView = PreviewView(this@MainActivity).apply {
scaleType = PreviewView.ScaleType.FILL_CENTER
implementationMode = PreviewView.ImplementationMode.COMPATIBLE
}
overlayView = OverlayView(this@MainActivity)
cameraFrame.addView(previewView, FrameLayout.LayoutParams(-1, -1))
@@ -229,7 +321,7 @@ class MainActivity : AppCompatActivity() {
text = if (recognitionEnabled) "暂停识别" else "继续识别"
recognitionStatusText.text = if (recognitionEnabled) "正在识别" else "识别已暂停"
if (!recognitionEnabled) {
overlayView.update(emptyList(), 1, 1)
overlayView.update(emptyList())
}
updateAnalyzerState()
}
@@ -320,7 +412,10 @@ class MainActivity : AppCompatActivity() {
lastAnalysisAt = 0L
pauseRecognitionButton.text = "暂停识别"
recognitionStatusText.text = "正在准备识别服务..."
overlayView.update(emptyList(), 1, 1)
matcherConfigSummaryText.text = matcherConfigSummary()
euclideanThresholdInput.setText(formatThreshold(euclideanThreshold))
cosineThresholdInput.setText(formatThreshold(cosineThreshold))
overlayView.update(emptyList())
if (hasCameraPermission()) {
startRecognitionSession()
@@ -333,6 +428,7 @@ class MainActivity : AppCompatActivity() {
if (currentMode != AppMode.RECOGNITION) return
val faceProcessor = ensureProcessor() ?: return
previewView.post {
val providerFuture = ProcessCameraProvider.getInstance(this)
providerFuture.addListener({
val provider = providerFuture.get()
@@ -342,21 +438,33 @@ class MainActivity : AppCompatActivity() {
return@addListener
}
val preview = Preview.Builder().build().also {
it.setSurfaceProvider(previewView.surfaceProvider)
}
val preview = Preview.Builder()
.setTargetResolution(ANALYSIS_SIZE)
.build()
.also { it.setSurfaceProvider(previewView.surfaceProvider) }
val analysis = ImageAnalysis.Builder()
.setTargetResolution(ANALYSIS_SIZE)
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.build()
val viewPort = previewView.viewPort
provider.unbindAll()
if (viewPort != null) {
val group = androidx.camera.core.UseCaseGroup.Builder()
.setViewPort(viewPort)
.addUseCase(preview)
.addUseCase(analysis)
.build()
provider.bindToLifecycle(this, CameraSelector.DEFAULT_BACK_CAMERA, group)
} else {
provider.bindToLifecycle(this, CameraSelector.DEFAULT_BACK_CAMERA, preview, analysis)
}
analysisUseCase = analysis
recognitionStatusText.text = if (faceProcessor === processor) "正在识别" else "识别服务已启动"
updateAnalyzerState()
}, ContextCompat.getMainExecutor(this))
}
}
private fun stopRecognitionSession() {
recognitionEnabled = false
@@ -365,7 +473,7 @@ class MainActivity : AppCompatActivity() {
analysisUseCase = null
cameraProvider?.unbindAll()
cameraProvider = null
overlayView.update(emptyList(), 1, 1)
overlayView.update(emptyList())
processor?.close()
processor = null
}
@@ -404,7 +512,8 @@ class MainActivity : AppCompatActivity() {
if (faces.isEmpty()) {
withContext(Dispatchers.Main) {
if (currentMode == AppMode.RECOGNITION) {
overlayView.update(emptyList(), 1, 1)
recognitionHistory.clear()
overlayView.update(emptyList())
updateRecognitionStatus(emptyList())
}
}
@@ -412,19 +521,22 @@ class MainActivity : AppCompatActivity() {
}
val bitmap = ImageUtils.imageProxyToBitmap(image)
val detections = faces.map { face ->
val activeTrackIds = faces.mapNotNull { it.trackingId }.toSet()
val rawDetections = faces.map { face ->
val embedding = faceProcessor.embedFace(bitmap, face)
val result = matcher.findNearest(embedding, students)
DetectionUi(
val result = stabilizeRecognition(face, matcher.findNearest(embedding, students))
RawDetection(
bounds = face.boundingBox,
result = result,
confidence = result.confidence
)
}
recognitionHistory.keys.retainAll(activeTrackIds)
withContext(Dispatchers.Main) {
if (currentMode == AppMode.RECOGNITION) {
overlayView.update(detections, bitmap.width, bitmap.height)
val detections = mapDetectionsToPreview(image, rawDetections)
overlayView.update(detections)
updateRecognitionStatus(detections)
}
}
@@ -524,8 +636,87 @@ class MainActivity : AppCompatActivity() {
Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
}
private fun loadMatcherConfig() {
euclideanThreshold = matcherPrefs.getFloat(KEY_EUCLIDEAN_THRESHOLD, DEFAULT_EUCLIDEAN_THRESHOLD)
cosineThreshold = matcherPrefs.getFloat(KEY_COSINE_THRESHOLD, DEFAULT_COSINE_THRESHOLD)
matcher = FaceMatcher(euclideanThreshold, cosineThreshold)
}
private fun saveMatcherConfig() {
val parsedEuclidean = euclideanThresholdInput.text?.toString()?.trim()?.toFloatOrNull()
val parsedCosine = cosineThresholdInput.text?.toString()?.trim()?.toFloatOrNull()
if (parsedEuclidean == null || parsedCosine == null) {
toast("请输入有效的数字")
return
}
if (parsedEuclidean !in 0f..2f) {
toast("欧氏阈值建议在 0 到 2 之间")
return
}
if (parsedCosine !in -1f..1f) {
toast("余弦阈值建议在 -1 到 1 之间")
return
}
euclideanThreshold = parsedEuclidean
cosineThreshold = parsedCosine
matcher = FaceMatcher(euclideanThreshold, cosineThreshold)
matcherPrefs.edit()
.putFloat(KEY_EUCLIDEAN_THRESHOLD, euclideanThreshold)
.putFloat(KEY_COSINE_THRESHOLD, cosineThreshold)
.apply()
matcherConfigSummaryText.text = matcherConfigSummary()
recognitionStatusText.text = "识别参数已更新"
toast("识别参数已保存")
}
private fun matcherConfigSummary(): String {
return "当前参数:欧氏 <= ${formatThreshold(euclideanThreshold)},余弦 >= ${formatThreshold(cosineThreshold)}"
}
private fun formatThreshold(value: Float): String {
return String.format(java.util.Locale.US, "%.2f", value)
}
private fun mapDetectionsToPreview(image: ImageProxy, rawDetections: List<RawDetection>): List<DetectionUi> {
val target = previewView.outputTransform ?: return rawDetections.map {
DetectionUi(android.graphics.RectF(it.bounds), it.result, it.confidence)
}
val source = imageProxyTransformFactory.getOutputTransform(image)
val transform = CoordinateTransform(source, target)
return rawDetections.map { detection ->
val rect = RectF(detection.bounds)
transform.mapRect(rect)
DetectionUi(rect, detection.result, detection.confidence)
}
}
private fun stabilizeRecognition(face: Face, result: RecognitionResult): RecognitionResult {
val trackId = face.trackingId ?: return result
val history = recognitionHistory.getOrPut(trackId) { ArrayDeque() }
history.addLast(result)
while (history.size > 5) {
history.removeFirst()
}
val matched = history.filter { it.isMatched }
if (matched.isEmpty()) return result
val best = matched.groupBy { it.student?.id }.maxByOrNull { it.value.size } ?: return result
return if (best.value.size >= 2) best.value.last() else result
}
companion object {
private const val ANALYSIS_INTERVAL_MS = 250L
private const val MATCHER_PREFS_NAME = "face_matcher_config"
private const val KEY_EUCLIDEAN_THRESHOLD = "euclidean_threshold"
private const val KEY_COSINE_THRESHOLD = "cosine_threshold"
private const val DEFAULT_EUCLIDEAN_THRESHOLD = 1.0f
private const val DEFAULT_COSINE_THRESHOLD = 0.6f
private const val ANALYSIS_INTERVAL_MS = 120L
private val ANALYSIS_SIZE = Size(640, 480)
}
}
@@ -535,3 +726,9 @@ private enum class AppMode {
RECOGNITION,
UPLOAD
}
private data class RawDetection(
val bounds: Rect,
val result: RecognitionResult,
val confidence: Float?
)

View File

@@ -4,7 +4,7 @@ import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Rect
import android.graphics.RectF
import android.graphics.Typeface
import android.util.AttributeSet
import android.view.View
@@ -51,13 +51,9 @@ class OverlayView @JvmOverloads constructor(
private val colorUnknown = Color.rgb(59, 130, 246) // 蓝色 - 未入库
private var detections: List<DetectionUi> = emptyList()
private var imageWidth = 1
private var imageHeight = 1
fun update(items: List<DetectionUi>, width: Int, height: Int) {
fun update(items: List<DetectionUi>) {
detections = items
imageWidth = width.coerceAtLeast(1)
imageHeight = height.coerceAtLeast(1)
invalidate()
}
@@ -70,7 +66,7 @@ class OverlayView @JvmOverloads constructor(
}
private fun drawDetection(canvas: Canvas, item: DetectionUi, index: Int) {
val rect = mapRect(item.bounds)
val rect = item.bounds
// 根据识别结果设置颜色
val boxColor = getBoxColor(item.result)
@@ -124,24 +120,13 @@ class OverlayView @JvmOverloads constructor(
else -> colorUnknown
}
}
private fun mapRect(rect: Rect): android.graphics.RectF {
val scaleX = width.toFloat() / imageWidth
val scaleY = height.toFloat() / imageHeight
return android.graphics.RectF(
width - rect.right * scaleX,
rect.top * scaleY,
width - rect.left * scaleX,
rect.bottom * scaleY
)
}
}
/**
* 识别 UI 数据
*/
data class DetectionUi(
val bounds: Rect,
val bounds: RectF,
val result: RecognitionResult,
val confidence: Float? = null
) {