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 package com.example.studentfaceregistry
import android.Manifest import android.Manifest
import android.content.SharedPreferences
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.graphics.Rect
import android.graphics.RectF
import android.graphics.Typeface import android.graphics.Typeface
import android.os.Bundle import android.os.Bundle
import android.os.SystemClock import android.os.SystemClock
import android.text.InputType
import android.util.Size import android.util.Size
import android.util.Log import android.util.Log
import android.view.Gravity import android.view.Gravity
import android.view.View import android.view.View
import android.widget.EditText
import android.widget.Button import android.widget.Button
import android.widget.FrameLayout import android.widget.FrameLayout
import android.widget.LinearLayout import android.widget.LinearLayout
@@ -22,6 +27,8 @@ import androidx.camera.core.ImageProxy
import androidx.camera.core.Preview import androidx.camera.core.Preview
import androidx.camera.lifecycle.ProcessCameraProvider import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.view.PreviewView import androidx.camera.view.PreviewView
import androidx.camera.view.transform.CoordinateTransform
import androidx.camera.view.transform.ImageProxyTransformFactory
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
@@ -29,7 +36,9 @@ import com.example.studentfaceregistry.data.StudentRepository
import com.example.studentfaceregistry.face.FaceMatcher import com.example.studentfaceregistry.face.FaceMatcher
import com.example.studentfaceregistry.face.FaceProcessor import com.example.studentfaceregistry.face.FaceProcessor
import com.example.studentfaceregistry.face.MatchType import com.example.studentfaceregistry.face.MatchType
import com.example.studentfaceregistry.face.RecognitionResult
import com.google.mlkit.vision.common.InputImage 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.DetectionUi
import com.example.studentfaceregistry.ui.OverlayView import com.example.studentfaceregistry.ui.OverlayView
import com.example.studentfaceregistry.upload.UploadServer import com.example.studentfaceregistry.upload.UploadServer
@@ -44,6 +53,9 @@ class MainActivity : AppCompatActivity() {
private lateinit var previewView: PreviewView private lateinit var previewView: PreviewView
private lateinit var overlayView: OverlayView private lateinit var overlayView: OverlayView
private lateinit var recognitionStatusText: TextView 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 uploadStatusText: TextView
private lateinit var uploadUrlText: TextView private lateinit var uploadUrlText: TextView
private lateinit var pauseRecognitionButton: Button private lateinit var pauseRecognitionButton: Button
@@ -58,12 +70,22 @@ class MainActivity : AppCompatActivity() {
private var cameraProvider: ProcessCameraProvider? = null private var cameraProvider: ProcessCameraProvider? = null
private val repository by lazy { StudentRepository(this) } 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 val cameraExecutor = Executors.newSingleThreadExecutor()
private var students: List<Student> = emptyList() private var students: List<Student> = emptyList()
private var recognitionEnabled = true private var recognitionEnabled = true
private var analyzing = false private var analyzing = false
private var lastAnalysisAt = 0L 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( private val permissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestMultiplePermissions() ActivityResultContracts.RequestMultiplePermissions()
@@ -80,6 +102,7 @@ class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
loadMatcherConfig()
setContentView(createContentView()) setContentView(createContentView())
lifecycleScope.launch { lifecycleScope.launch {
@@ -197,6 +220,74 @@ class MainActivity : AppCompatActivity() {
} }
addView(recognitionStatusText) 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 { val cameraFrame = FrameLayout(this@MainActivity).apply {
layoutParams = LinearLayout.LayoutParams( layoutParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.MATCH_PARENT,
@@ -206,6 +297,7 @@ class MainActivity : AppCompatActivity() {
} }
previewView = PreviewView(this@MainActivity).apply { previewView = PreviewView(this@MainActivity).apply {
scaleType = PreviewView.ScaleType.FILL_CENTER scaleType = PreviewView.ScaleType.FILL_CENTER
implementationMode = PreviewView.ImplementationMode.COMPATIBLE
} }
overlayView = OverlayView(this@MainActivity) overlayView = OverlayView(this@MainActivity)
cameraFrame.addView(previewView, FrameLayout.LayoutParams(-1, -1)) cameraFrame.addView(previewView, FrameLayout.LayoutParams(-1, -1))
@@ -229,7 +321,7 @@ class MainActivity : AppCompatActivity() {
text = if (recognitionEnabled) "暂停识别" else "继续识别" text = if (recognitionEnabled) "暂停识别" else "继续识别"
recognitionStatusText.text = if (recognitionEnabled) "正在识别" else "识别已暂停" recognitionStatusText.text = if (recognitionEnabled) "正在识别" else "识别已暂停"
if (!recognitionEnabled) { if (!recognitionEnabled) {
overlayView.update(emptyList(), 1, 1) overlayView.update(emptyList())
} }
updateAnalyzerState() updateAnalyzerState()
} }
@@ -320,7 +412,10 @@ class MainActivity : AppCompatActivity() {
lastAnalysisAt = 0L lastAnalysisAt = 0L
pauseRecognitionButton.text = "暂停识别" pauseRecognitionButton.text = "暂停识别"
recognitionStatusText.text = "正在准备识别服务..." recognitionStatusText.text = "正在准备识别服务..."
overlayView.update(emptyList(), 1, 1) matcherConfigSummaryText.text = matcherConfigSummary()
euclideanThresholdInput.setText(formatThreshold(euclideanThreshold))
cosineThresholdInput.setText(formatThreshold(cosineThreshold))
overlayView.update(emptyList())
if (hasCameraPermission()) { if (hasCameraPermission()) {
startRecognitionSession() startRecognitionSession()
@@ -333,6 +428,7 @@ class MainActivity : AppCompatActivity() {
if (currentMode != AppMode.RECOGNITION) return if (currentMode != AppMode.RECOGNITION) return
val faceProcessor = ensureProcessor() ?: return val faceProcessor = ensureProcessor() ?: return
previewView.post {
val providerFuture = ProcessCameraProvider.getInstance(this) val providerFuture = ProcessCameraProvider.getInstance(this)
providerFuture.addListener({ providerFuture.addListener({
val provider = providerFuture.get() val provider = providerFuture.get()
@@ -342,21 +438,33 @@ class MainActivity : AppCompatActivity() {
return@addListener return@addListener
} }
val preview = Preview.Builder().build().also { val preview = Preview.Builder()
it.setSurfaceProvider(previewView.surfaceProvider) .setTargetResolution(ANALYSIS_SIZE)
} .build()
.also { it.setSurfaceProvider(previewView.surfaceProvider) }
val analysis = ImageAnalysis.Builder() val analysis = ImageAnalysis.Builder()
.setTargetResolution(ANALYSIS_SIZE) .setTargetResolution(ANALYSIS_SIZE)
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.build() .build()
val viewPort = previewView.viewPort
provider.unbindAll() 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) provider.bindToLifecycle(this, CameraSelector.DEFAULT_BACK_CAMERA, preview, analysis)
}
analysisUseCase = analysis analysisUseCase = analysis
recognitionStatusText.text = if (faceProcessor === processor) "正在识别" else "识别服务已启动" recognitionStatusText.text = if (faceProcessor === processor) "正在识别" else "识别服务已启动"
updateAnalyzerState() updateAnalyzerState()
}, ContextCompat.getMainExecutor(this)) }, ContextCompat.getMainExecutor(this))
} }
}
private fun stopRecognitionSession() { private fun stopRecognitionSession() {
recognitionEnabled = false recognitionEnabled = false
@@ -365,7 +473,7 @@ class MainActivity : AppCompatActivity() {
analysisUseCase = null analysisUseCase = null
cameraProvider?.unbindAll() cameraProvider?.unbindAll()
cameraProvider = null cameraProvider = null
overlayView.update(emptyList(), 1, 1) overlayView.update(emptyList())
processor?.close() processor?.close()
processor = null processor = null
} }
@@ -404,7 +512,8 @@ class MainActivity : AppCompatActivity() {
if (faces.isEmpty()) { if (faces.isEmpty()) {
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
if (currentMode == AppMode.RECOGNITION) { if (currentMode == AppMode.RECOGNITION) {
overlayView.update(emptyList(), 1, 1) recognitionHistory.clear()
overlayView.update(emptyList())
updateRecognitionStatus(emptyList()) updateRecognitionStatus(emptyList())
} }
} }
@@ -412,19 +521,22 @@ class MainActivity : AppCompatActivity() {
} }
val bitmap = ImageUtils.imageProxyToBitmap(image) 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 embedding = faceProcessor.embedFace(bitmap, face)
val result = matcher.findNearest(embedding, students) val result = stabilizeRecognition(face, matcher.findNearest(embedding, students))
DetectionUi( RawDetection(
bounds = face.boundingBox, bounds = face.boundingBox,
result = result, result = result,
confidence = result.confidence confidence = result.confidence
) )
} }
recognitionHistory.keys.retainAll(activeTrackIds)
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
if (currentMode == AppMode.RECOGNITION) { if (currentMode == AppMode.RECOGNITION) {
overlayView.update(detections, bitmap.width, bitmap.height) val detections = mapDetectionsToPreview(image, rawDetections)
overlayView.update(detections)
updateRecognitionStatus(detections) updateRecognitionStatus(detections)
} }
} }
@@ -524,8 +636,87 @@ class MainActivity : AppCompatActivity() {
Toast.makeText(this, message, Toast.LENGTH_SHORT).show() 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 { 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) private val ANALYSIS_SIZE = Size(640, 480)
} }
} }
@@ -535,3 +726,9 @@ private enum class AppMode {
RECOGNITION, RECOGNITION,
UPLOAD 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.Canvas
import android.graphics.Color import android.graphics.Color
import android.graphics.Paint import android.graphics.Paint
import android.graphics.Rect import android.graphics.RectF
import android.graphics.Typeface import android.graphics.Typeface
import android.util.AttributeSet import android.util.AttributeSet
import android.view.View import android.view.View
@@ -51,13 +51,9 @@ class OverlayView @JvmOverloads constructor(
private val colorUnknown = Color.rgb(59, 130, 246) // 蓝色 - 未入库 private val colorUnknown = Color.rgb(59, 130, 246) // 蓝色 - 未入库
private var detections: List<DetectionUi> = emptyList() 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 detections = items
imageWidth = width.coerceAtLeast(1)
imageHeight = height.coerceAtLeast(1)
invalidate() invalidate()
} }
@@ -70,7 +66,7 @@ class OverlayView @JvmOverloads constructor(
} }
private fun drawDetection(canvas: Canvas, item: DetectionUi, index: Int) { private fun drawDetection(canvas: Canvas, item: DetectionUi, index: Int) {
val rect = mapRect(item.bounds) val rect = item.bounds
// 根据识别结果设置颜色 // 根据识别结果设置颜色
val boxColor = getBoxColor(item.result) val boxColor = getBoxColor(item.result)
@@ -124,24 +120,13 @@ class OverlayView @JvmOverloads constructor(
else -> colorUnknown 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 数据 * 识别 UI 数据
*/ */
data class DetectionUi( data class DetectionUi(
val bounds: Rect, val bounds: RectF,
val result: RecognitionResult, val result: RecognitionResult,
val confidence: Float? = null val confidence: Float? = null
) { ) {