remove debug info
This commit is contained in:
@@ -1,10 +1,7 @@
|
||||
package com.example.studentfaceregistry
|
||||
|
||||
import android.Manifest
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.content.SharedPreferences
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.content.pm.PackageManager
|
||||
import android.graphics.Rect
|
||||
import android.graphics.RectF
|
||||
@@ -19,7 +16,6 @@ import android.view.View
|
||||
import android.widget.EditText
|
||||
import android.widget.Button
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
@@ -39,7 +35,6 @@ 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.example.studentfaceregistry.ui.FaceDebugRenderer
|
||||
import com.google.mlkit.vision.common.InputImage
|
||||
import com.google.mlkit.vision.face.Face
|
||||
import com.example.studentfaceregistry.ui.DetectionUi
|
||||
@@ -49,8 +44,6 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
class MainActivity : AppCompatActivity() {
|
||||
@@ -64,8 +57,6 @@ class MainActivity : AppCompatActivity() {
|
||||
private lateinit var uploadStatusText: TextView
|
||||
private lateinit var uploadUrlText: TextView
|
||||
private lateinit var pauseRecognitionButton: Button
|
||||
private lateinit var debugTraceStatusText: TextView
|
||||
private lateinit var debugTraceImageView: ImageView
|
||||
private lateinit var homePanel: View
|
||||
private lateinit var recognitionPanel: View
|
||||
private lateinit var uploadPanel: View
|
||||
@@ -88,7 +79,6 @@ class MainActivity : AppCompatActivity() {
|
||||
private var euclideanThreshold = DEFAULT_EUCLIDEAN_THRESHOLD
|
||||
private var cosineThreshold = DEFAULT_COSINE_THRESHOLD
|
||||
@Volatile private var matcher = FaceMatcher(DEFAULT_EUCLIDEAN_THRESHOLD, DEFAULT_COSINE_THRESHOLD)
|
||||
@Volatile private var captureNextLiveDebugFrame = false
|
||||
private val recognitionHistory = mutableMapOf<Int, ArrayDeque<RecognitionResult>>()
|
||||
private val recognitionCache = mutableMapOf<Int, CachedRecognition>()
|
||||
|
||||
@@ -292,9 +282,6 @@ class MainActivity : AppCompatActivity() {
|
||||
})
|
||||
|
||||
addView(matcherConfigPanel)
|
||||
if (isDebugBuild()) {
|
||||
addView(createDebugTracePanel())
|
||||
}
|
||||
|
||||
val cameraFrame = FrameLayout(this@MainActivity).apply {
|
||||
layoutParams = LinearLayout.LayoutParams(
|
||||
@@ -339,56 +326,6 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun createDebugTracePanel(): View {
|
||||
return LinearLayout(this).apply {
|
||||
orientation = LinearLayout.VERTICAL
|
||||
setPadding(16, 12, 16, 12)
|
||||
setBackgroundColor(0xFFFFFFFF.toInt())
|
||||
|
||||
val row = LinearLayout(this@MainActivity).apply {
|
||||
orientation = LinearLayout.HORIZONTAL
|
||||
gravity = Gravity.CENTER_VERTICAL
|
||||
}
|
||||
|
||||
row.addView(Button(this@MainActivity).apply {
|
||||
text = "调试 debug.jpg"
|
||||
setOnClickListener { runDebugTraceFromAsset() }
|
||||
})
|
||||
row.addView(Button(this@MainActivity).apply {
|
||||
text = "捕获实时帧"
|
||||
setOnClickListener {
|
||||
captureNextLiveDebugFrame = true
|
||||
debugTraceStatusText.text = "等待下一帧人脸画面..."
|
||||
}
|
||||
})
|
||||
|
||||
debugTraceStatusText = TextView(this@MainActivity).apply {
|
||||
text = "将 assets/debug.jpg 生成管线调试图"
|
||||
textSize = 13f
|
||||
setTextColor(0xFF475569.toInt())
|
||||
setPadding(16, 0, 0, 0)
|
||||
}
|
||||
row.addView(debugTraceStatusText, LinearLayout.LayoutParams(0, -2, 1f))
|
||||
addView(row)
|
||||
|
||||
debugTraceImageView = ImageView(this@MainActivity).apply {
|
||||
visibility = View.GONE
|
||||
adjustViewBounds = true
|
||||
scaleType = ImageView.ScaleType.FIT_CENTER
|
||||
setBackgroundColor(0xFFF8FAFC.toInt())
|
||||
}
|
||||
addView(
|
||||
debugTraceImageView,
|
||||
LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||
dp(320)
|
||||
).apply {
|
||||
topMargin = dp(10)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createUploadPanel(): View {
|
||||
return LinearLayout(this).apply {
|
||||
orientation = LinearLayout.VERTICAL
|
||||
@@ -588,14 +525,9 @@ class MainActivity : AppCompatActivity() {
|
||||
if (shouldRefreshRecognition(face.trackingId, now)) index to face else null
|
||||
}
|
||||
val refreshedResults = mutableMapOf<Int, RecognitionResult>()
|
||||
val debugBitmap = if (faces.isNotEmpty() && (facesNeedingRefresh.isNotEmpty() || isDebugBuild())) {
|
||||
ImageUtils.imageProxyToBitmap(image)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
if (facesNeedingRefresh.isNotEmpty()) {
|
||||
val bitmap = requireNotNull(debugBitmap)
|
||||
val bitmap = ImageUtils.imageProxyToBitmap(image)
|
||||
facesNeedingRefresh.forEach { (index, face) ->
|
||||
val embedding = faceProcessor.embedFace(bitmap, face)
|
||||
val refreshed = matcher.findNearest(embedding, students)
|
||||
@@ -620,8 +552,6 @@ class MainActivity : AppCompatActivity() {
|
||||
withContext(Dispatchers.Main) {
|
||||
if (currentMode == AppMode.RECOGNITION) {
|
||||
val detections = mapDetectionsToPreview(image, rawDetections)
|
||||
traceFacePipeline(image, faces, debugBitmap, rawDetections, detections)
|
||||
maybeSaveLiveDebugFrame(image, debugBitmap, rawDetections, detections)
|
||||
overlayView.update(detections.filter { it.result.isMatched })
|
||||
updateRecognitionStatus(detections)
|
||||
}
|
||||
@@ -699,97 +629,6 @@ class MainActivity : AppCompatActivity() {
|
||||
uploadUrlText.text = "-"
|
||||
}
|
||||
|
||||
private fun runDebugTraceFromAsset() {
|
||||
if (!isDebugBuild()) return
|
||||
|
||||
val faceProcessor = ensureProcessor() ?: return
|
||||
val previewWidth = previewView.width.takeIf { it > 0 } ?: 0
|
||||
val previewHeight = previewView.height.takeIf { it > 0 } ?: 0
|
||||
debugTraceStatusText.text = "正在读取 debug.jpg ..."
|
||||
|
||||
lifecycleScope.launch(Dispatchers.Default) {
|
||||
try {
|
||||
val sourceBitmap = loadDebugBitmapFromAssets()
|
||||
val steps = faceProcessor.debugTrace(sourceBitmap)
|
||||
val results = steps.map { step ->
|
||||
val embedding = faceProcessor.embedFace(sourceBitmap, step.face)
|
||||
matcher.findNearest(embedding, students)
|
||||
}
|
||||
val rendered = FaceDebugRenderer.render(
|
||||
source = sourceBitmap,
|
||||
steps = steps,
|
||||
results = results,
|
||||
previewWidth = previewWidth.takeIf { it > 0 } ?: sourceBitmap.width,
|
||||
previewHeight = previewHeight.takeIf { it > 0 } ?: sourceBitmap.height
|
||||
)
|
||||
val outFile = File(cacheDir, "face_trace_debug.png")
|
||||
FileOutputStream(outFile).use { stream ->
|
||||
rendered.compress(Bitmap.CompressFormat.PNG, 100, stream)
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
debugTraceImageView.setImageBitmap(rendered)
|
||||
debugTraceImageView.visibility = View.VISIBLE
|
||||
debugTraceStatusText.text = "已生成 ${outFile.absolutePath}"
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("MainActivity", "Debug trace failed", e)
|
||||
withContext(Dispatchers.Main) {
|
||||
debugTraceStatusText.text = "调试图生成失败:${e.message ?: e.javaClass.simpleName}"
|
||||
toast("调试图生成失败:${e.message ?: e.javaClass.simpleName}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadDebugBitmapFromAssets(): Bitmap {
|
||||
assets.open(DEBUG_ASSET_NAME).use { stream ->
|
||||
return BitmapFactory.decodeStream(stream)
|
||||
?: error("无法解码 assets/$DEBUG_ASSET_NAME")
|
||||
}
|
||||
}
|
||||
|
||||
private fun maybeSaveLiveDebugFrame(
|
||||
image: ImageProxy,
|
||||
bitmap: Bitmap?,
|
||||
rawDetections: List<RawDetection>,
|
||||
mappedDetections: List<DetectionUi>
|
||||
) {
|
||||
if (!isDebugBuild() || !captureNextLiveDebugFrame) return
|
||||
val sourceBitmap = bitmap ?: return
|
||||
captureNextLiveDebugFrame = false
|
||||
|
||||
val previewWidth = previewView.width.takeIf { it > 0 } ?: sourceBitmap.width
|
||||
val previewHeight = previewView.height.takeIf { it > 0 } ?: sourceBitmap.height
|
||||
val boxes = rawDetections.mapIndexed { index, raw ->
|
||||
FaceDebugRenderer.LiveBox(
|
||||
raw = Rect(raw.bounds),
|
||||
mapped = RectF(mappedDetections.getOrNull(index)?.bounds ?: RectF()),
|
||||
label = mappedDetections.getOrNull(index)?.label ?: raw.result.matchType.name
|
||||
)
|
||||
}
|
||||
val metadata = buildString {
|
||||
append("proxy=").append(image.width).append('x').append(image.height)
|
||||
append(" rotation=").append(image.imageInfo.rotationDegrees)
|
||||
append(" cropRect=").append(image.cropRect)
|
||||
append(" preview=").append(previewWidth).append('x').append(previewHeight)
|
||||
append(" bitmap=").append(sourceBitmap.width).append('x').append(sourceBitmap.height)
|
||||
}
|
||||
val rendered = FaceDebugRenderer.renderLiveFrame(
|
||||
source = sourceBitmap,
|
||||
boxes = boxes,
|
||||
previewWidth = previewWidth,
|
||||
previewHeight = previewHeight,
|
||||
metadata = metadata
|
||||
)
|
||||
val outFile = File(cacheDir, "face_trace_live.png")
|
||||
FileOutputStream(outFile).use { stream ->
|
||||
rendered.compress(Bitmap.CompressFormat.PNG, 100, stream)
|
||||
}
|
||||
debugTraceImageView.setImageBitmap(rendered)
|
||||
debugTraceImageView.visibility = View.VISIBLE
|
||||
debugTraceStatusText.text = "已生成 ${outFile.absolutePath}"
|
||||
}
|
||||
|
||||
private fun ensureProcessor(): FaceProcessor? {
|
||||
val current = processor
|
||||
if (current != null) return current
|
||||
@@ -805,14 +644,6 @@ class MainActivity : AppCompatActivity() {
|
||||
return created
|
||||
}
|
||||
|
||||
private fun dp(value: Int): Int {
|
||||
return (value * resources.displayMetrics.density).toInt()
|
||||
}
|
||||
|
||||
private fun isDebugBuild(): Boolean {
|
||||
return applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE != 0
|
||||
}
|
||||
|
||||
private fun hasCameraPermission(): Boolean {
|
||||
return ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED
|
||||
}
|
||||
@@ -928,49 +759,6 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun traceFacePipeline(
|
||||
image: ImageProxy,
|
||||
faces: List<Face>,
|
||||
bitmap: android.graphics.Bitmap?,
|
||||
rawDetections: List<RawDetection>,
|
||||
mappedDetections: List<DetectionUi>
|
||||
) {
|
||||
if (!isDebugBuild()) return
|
||||
|
||||
val mediaImage = image.image
|
||||
Log.d(
|
||||
TRACE_TAG,
|
||||
buildString {
|
||||
append("frame proxy=").append(image.width).append('x').append(image.height)
|
||||
append(", media=").append(mediaImage?.width).append('x').append(mediaImage?.height)
|
||||
append(", rotation=").append(image.imageInfo.rotationDegrees)
|
||||
append(", cropRect=").append(image.cropRect)
|
||||
append(", preview=").append(previewView.width).append('x').append(previewView.height)
|
||||
append(", previewTransform=").append(previewView.outputTransform != null)
|
||||
append(", bitmap=").append(bitmap?.width).append('x').append(bitmap?.height)
|
||||
append(", faces=").append(faces.size)
|
||||
}
|
||||
)
|
||||
|
||||
faces.forEachIndexed { index, face ->
|
||||
val raw = rawDetections.getOrNull(index)
|
||||
val mapped = mappedDetections.getOrNull(index)
|
||||
Log.d(
|
||||
TRACE_TAG,
|
||||
buildString {
|
||||
append("face#").append(index)
|
||||
append(" track=").append(face.trackingId)
|
||||
append(" box=").append(face.boundingBox)
|
||||
append(" mapped=").append(mapped?.bounds)
|
||||
append(" result=").append(raw?.result?.matchType)
|
||||
append(" dist=").append(raw?.result?.distance)
|
||||
append(" cosine=").append(raw?.result?.cosineSimilarity)
|
||||
append(" conf=").append(raw?.confidence)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun stabilizeRecognition(face: Face, result: RecognitionResult): RecognitionResult {
|
||||
val trackId = face.trackingId ?: return result
|
||||
val history = recognitionHistory.getOrPut(trackId) { ArrayDeque() }
|
||||
@@ -1018,8 +806,6 @@ class MainActivity : AppCompatActivity() {
|
||||
private const val DEFAULT_COSINE_THRESHOLD = 0.6f
|
||||
private const val RECOGNITION_REFRESH_MS = 400L
|
||||
private const val ANALYSIS_INTERVAL_MS = 120L
|
||||
private const val TRACE_TAG = "FaceTrace"
|
||||
private const val DEBUG_ASSET_NAME = "debug.jpg"
|
||||
private val ANALYSIS_SIZE = Size(640, 480)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
package com.example.studentfaceregistry.face
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Rect
|
||||
import com.google.mlkit.vision.face.Face
|
||||
|
||||
data class FaceDebugStep(
|
||||
val face: Face,
|
||||
val cropRect: Rect,
|
||||
val croppedBitmap: Bitmap,
|
||||
val alignedBitmap: Bitmap
|
||||
)
|
||||
@@ -1,10 +1,8 @@
|
||||
package com.example.studentfaceregistry.face
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Rect
|
||||
import android.util.Log
|
||||
import com.google.mlkit.vision.common.InputImage
|
||||
import com.google.mlkit.vision.face.Face
|
||||
import com.google.mlkit.vision.face.FaceDetection
|
||||
@@ -20,7 +18,6 @@ class FaceProcessor(context: Context) : AutoCloseable {
|
||||
private val embedder = FaceEmbedder(context)
|
||||
private val aligner = FaceAligner()
|
||||
private val preprocessor = ImagePreprocessor()
|
||||
private val debugEnabled = context.applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE != 0
|
||||
|
||||
// 实时检测器:快速模式,用于摄像头预览
|
||||
private val realtimeDetector = FaceDetection.getClient(
|
||||
@@ -91,30 +88,6 @@ class FaceProcessor(context: Context) : AutoCloseable {
|
||||
val leftEye = face.getLandmark(FaceLandmark.LEFT_EYE) ?: return faceBitmap
|
||||
val rightEye = face.getLandmark(FaceLandmark.RIGHT_EYE) ?: return faceBitmap
|
||||
|
||||
if (debugEnabled) {
|
||||
val leftEyeRelX = leftEye.position.x - faceLeft
|
||||
val leftEyeRelY = leftEye.position.y - faceTop
|
||||
val rightEyeRelX = rightEye.position.x - faceLeft
|
||||
val rightEyeRelY = rightEye.position.y - faceTop
|
||||
val angle = Math.atan2(
|
||||
(rightEyeRelY - leftEyeRelY).toDouble(),
|
||||
(rightEyeRelX - leftEyeRelX).toDouble()
|
||||
).toFloat() * 180f / Math.PI.toFloat()
|
||||
Log.d(
|
||||
TRACE_TAG,
|
||||
buildString {
|
||||
append("alignFace bitmap=").append(faceBitmap.width).append('x').append(faceBitmap.height)
|
||||
append(" faceLeft=").append(faceLeft)
|
||||
append(" faceTop=").append(faceTop)
|
||||
append(" leftEye=(").append(leftEye.position.x).append(',').append(leftEye.position.y).append(')')
|
||||
append(" rightEye=(").append(rightEye.position.x).append(',').append(rightEye.position.y).append(')')
|
||||
append(" relLeft=(").append(leftEyeRelX).append(',').append(leftEyeRelY).append(')')
|
||||
append(" relRight=(").append(rightEyeRelX).append(',').append(rightEyeRelY).append(')')
|
||||
append(" angle=").append(angle)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
return aligner.align(
|
||||
bitmap = faceBitmap,
|
||||
leftEyeX = leftEye.position.x,
|
||||
@@ -140,18 +113,6 @@ class FaceProcessor(context: Context) : AutoCloseable {
|
||||
return detectFaces(InputImage.fromBitmap(bitmap, 0))
|
||||
}
|
||||
|
||||
/**
|
||||
* 调试入口:保留检测、裁剪、对齐三段中间结果,便于生成可视化报告。
|
||||
*/
|
||||
suspend fun debugTrace(bitmap: Bitmap): List<FaceDebugStep> {
|
||||
return detectFaces(bitmap).mapNotNull { face ->
|
||||
val crop = safeCropRect(bitmap, face.boundingBox) ?: return@mapNotNull null
|
||||
val cropped = Bitmap.createBitmap(bitmap, crop.left, crop.top, crop.width(), crop.height())
|
||||
val aligned = alignFaceWithLandmarks(cropped, face, crop.left, crop.top)
|
||||
FaceDebugStep(face, Rect(crop), cropped, aligned)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册用的人脸检测(高精度)
|
||||
*/
|
||||
@@ -167,12 +128,6 @@ class FaceProcessor(context: Context) : AutoCloseable {
|
||||
fun embedFace(bitmap: Bitmap, face: Face): FloatArray {
|
||||
val crop = requireNotNull(safeCropRect(bitmap, face.boundingBox)) { "人脸区域超出图片边界。" }
|
||||
val cropped = Bitmap.createBitmap(bitmap, crop.left, crop.top, crop.width(), crop.height())
|
||||
if (debugEnabled) {
|
||||
Log.d(
|
||||
TRACE_TAG,
|
||||
"embedFace src=${bitmap.width}x${bitmap.height} box=${face.boundingBox} crop=$crop"
|
||||
)
|
||||
}
|
||||
val aligned = alignFaceWithLandmarks(cropped, face, crop.left, crop.top)
|
||||
val preprocessed = preprocessor.preprocessRealtime(aligned)
|
||||
return embedder.embed(preprocessed)
|
||||
@@ -205,8 +160,4 @@ class FaceProcessor(context: Context) : AutoCloseable {
|
||||
|
||||
return Rect(left, top, right, bottom)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TRACE_TAG = "FaceTrace"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,446 +0,0 @@
|
||||
package com.example.studentfaceregistry.ui
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import android.graphics.Paint
|
||||
import android.graphics.RectF
|
||||
import android.graphics.Typeface
|
||||
import com.example.studentfaceregistry.face.FaceDebugStep
|
||||
import com.example.studentfaceregistry.face.RecognitionResult
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
object FaceDebugRenderer {
|
||||
data class LiveBox(
|
||||
val raw: android.graphics.Rect,
|
||||
val mapped: RectF,
|
||||
val label: String
|
||||
)
|
||||
|
||||
fun render(
|
||||
source: Bitmap,
|
||||
steps: List<FaceDebugStep>,
|
||||
results: List<RecognitionResult>,
|
||||
previewWidth: Int,
|
||||
previewHeight: Int
|
||||
): Bitmap {
|
||||
val width = 1600
|
||||
val outerPadding = 32
|
||||
val panelGap = 24
|
||||
val titleHeight = 52
|
||||
val panelWidth = width - outerPadding * 2
|
||||
val previewAspect = if (previewWidth > 0 && previewHeight > 0) {
|
||||
previewWidth.toFloat() / previewHeight.toFloat()
|
||||
} else {
|
||||
source.width.toFloat() / source.height.toFloat()
|
||||
}
|
||||
val panelHeight = ((panelWidth / previewAspect).toInt()).coerceIn(360, 640)
|
||||
val chosenIndex = chooseHighlightIndex(steps, results)
|
||||
val cropStep = chosenIndex?.let { steps.getOrNull(it) }
|
||||
val cropBitmap = cropStep?.croppedBitmap
|
||||
val alignedBitmap = cropStep?.alignedBitmap
|
||||
val rawPanelHeight = panelHeight
|
||||
val previewPanelHeight = panelHeight
|
||||
val thumbnailHeight = 320
|
||||
val totalHeight =
|
||||
outerPadding +
|
||||
titleHeight + rawPanelHeight +
|
||||
panelGap +
|
||||
titleHeight + previewPanelHeight +
|
||||
panelGap +
|
||||
titleHeight + thumbnailHeight +
|
||||
panelGap +
|
||||
titleHeight + thumbnailHeight +
|
||||
panelGap +
|
||||
180 +
|
||||
outerPadding
|
||||
|
||||
val output = Bitmap.createBitmap(width, totalHeight, Bitmap.Config.ARGB_8888)
|
||||
val canvas = Canvas(output)
|
||||
canvas.drawColor(Color.WHITE)
|
||||
|
||||
val titlePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = Color.rgb(15, 23, 42)
|
||||
textSize = 38f
|
||||
typeface = Typeface.DEFAULT_BOLD
|
||||
}
|
||||
val metaPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = Color.rgb(71, 85, 105)
|
||||
textSize = 24f
|
||||
}
|
||||
val borderPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
style = Paint.Style.STROKE
|
||||
strokeWidth = 4f
|
||||
color = Color.rgb(203, 213, 225)
|
||||
}
|
||||
val rawPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
style = Paint.Style.STROKE
|
||||
strokeWidth = 6f
|
||||
}
|
||||
val previewPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
style = Paint.Style.STROKE
|
||||
strokeWidth = 6f
|
||||
color = Color.rgb(59, 130, 246)
|
||||
}
|
||||
val labelBgPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
style = Paint.Style.FILL
|
||||
}
|
||||
val labelPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = Color.WHITE
|
||||
textSize = 28f
|
||||
typeface = Typeface.DEFAULT_BOLD
|
||||
}
|
||||
|
||||
var top = outerPadding.toFloat()
|
||||
drawTitle(canvas, "1. 原图 + 原始检测框", top, titlePaint, metaPaint, source, steps.size)
|
||||
top += titleHeight.toFloat()
|
||||
val rawRect = RectF(
|
||||
outerPadding.toFloat(),
|
||||
top,
|
||||
(outerPadding + panelWidth).toFloat(),
|
||||
(top + rawPanelHeight)
|
||||
)
|
||||
drawPanelBorder(canvas, rawRect, borderPaint)
|
||||
drawBitmapFitCenter(canvas, source, rawRect)
|
||||
steps.forEachIndexed { index, step ->
|
||||
val color = if (index == chosenIndex) Color.rgb(34, 197, 94) else Color.rgb(239, 68, 68)
|
||||
rawPaint.color = color
|
||||
val mapped = mapRectFitCenter(step.face.boundingBox, source.width, source.height, rawRect)
|
||||
canvas.drawRect(mapped, rawPaint)
|
||||
drawLabel(canvas, mapped.left, max(rawRect.top, mapped.top - 38f), "face#$index", labelBgPaint, labelPaint, color)
|
||||
}
|
||||
|
||||
top = rawRect.bottom + panelGap
|
||||
drawTitle(canvas, "2. 映射到预览后的框", top, titlePaint, metaPaint, source, previewWidth, previewHeight)
|
||||
top += titleHeight.toFloat()
|
||||
val previewRect = RectF(
|
||||
outerPadding.toFloat(),
|
||||
top,
|
||||
(outerPadding + panelWidth).toFloat(),
|
||||
(top + previewPanelHeight)
|
||||
)
|
||||
drawPanelBorder(canvas, previewRect, borderPaint)
|
||||
drawBitmapCenterCrop(canvas, source, previewRect)
|
||||
steps.forEachIndexed { index, step ->
|
||||
val color = if (index == chosenIndex) Color.rgb(34, 197, 94) else Color.rgb(59, 130, 246)
|
||||
previewPaint.color = color
|
||||
val mapped = mapRectCenterCrop(step.face.boundingBox, source.width, source.height, previewRect)
|
||||
canvas.drawRect(mapped, previewPaint)
|
||||
drawLabel(canvas, mapped.left, max(previewRect.top, mapped.top - 38f), "mapped#$index", labelBgPaint, labelPaint, color)
|
||||
}
|
||||
|
||||
top = previewRect.bottom + panelGap
|
||||
drawTitle(canvas, "3. 裁剪后的人脸", top, titlePaint, metaPaint, source, cropStep?.cropRect)
|
||||
top += titleHeight.toFloat()
|
||||
val cropRect = RectF(
|
||||
outerPadding.toFloat(),
|
||||
top,
|
||||
(outerPadding + panelWidth / 2f - panelGap / 2f),
|
||||
(top + thumbnailHeight)
|
||||
)
|
||||
val alignRect = RectF(
|
||||
cropRect.right + panelGap,
|
||||
top,
|
||||
(outerPadding + panelWidth).toFloat(),
|
||||
(top + thumbnailHeight)
|
||||
)
|
||||
drawPanelBorder(canvas, cropRect, borderPaint)
|
||||
drawPanelBorder(canvas, alignRect, borderPaint)
|
||||
if (cropBitmap != null) {
|
||||
drawBitmapFitCenter(canvas, cropBitmap, cropRect)
|
||||
} else {
|
||||
drawEmpty(canvas, cropRect, "无裁剪结果")
|
||||
}
|
||||
if (alignedBitmap != null) {
|
||||
drawBitmapFitCenter(canvas, alignedBitmap, alignRect)
|
||||
} else {
|
||||
drawEmpty(canvas, alignRect, "无对齐结果")
|
||||
}
|
||||
|
||||
top = cropRect.bottom + panelGap
|
||||
val infoRect = RectF(
|
||||
outerPadding.toFloat(),
|
||||
top,
|
||||
(outerPadding + panelWidth).toFloat(),
|
||||
(top + 180)
|
||||
)
|
||||
drawPanelBorder(canvas, infoRect, borderPaint)
|
||||
val infoText = buildString {
|
||||
append("输出目录:cache/face_trace_debug.png")
|
||||
append(" 选中人脸:")
|
||||
append(chosenIndex?.let { "face#$it" } ?: "无")
|
||||
append(" 预览尺寸:")
|
||||
append(previewWidth).append('x').append(previewHeight)
|
||||
}
|
||||
canvas.drawText(infoText, infoRect.left + 24f, infoRect.top + 60f, metaPaint)
|
||||
canvas.drawText("图例:红=原始框 蓝=映射框 绿=选中人脸", infoRect.left + 24f, infoRect.top + 104f, metaPaint)
|
||||
|
||||
cropStep?.let {
|
||||
val cropText = "cropRect=${it.cropRect}"
|
||||
canvas.drawText(cropText, infoRect.left + 24f, infoRect.top + 148f, metaPaint)
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
fun renderLiveFrame(
|
||||
source: Bitmap,
|
||||
boxes: List<LiveBox>,
|
||||
previewWidth: Int,
|
||||
previewHeight: Int,
|
||||
metadata: String
|
||||
): Bitmap {
|
||||
val width = 1600
|
||||
val outerPadding = 32
|
||||
val panelGap = 24
|
||||
val titleHeight = 52
|
||||
val panelWidth = width - outerPadding * 2
|
||||
val previewAspect = if (previewWidth > 0 && previewHeight > 0) {
|
||||
previewWidth.toFloat() / previewHeight.toFloat()
|
||||
} else {
|
||||
source.width.toFloat() / source.height.toFloat()
|
||||
}
|
||||
val panelHeight = ((panelWidth / previewAspect).toInt()).coerceIn(360, 640)
|
||||
val totalHeight =
|
||||
outerPadding +
|
||||
titleHeight + panelHeight +
|
||||
panelGap +
|
||||
titleHeight + panelHeight +
|
||||
panelGap +
|
||||
220 +
|
||||
outerPadding
|
||||
|
||||
val output = Bitmap.createBitmap(width, totalHeight, Bitmap.Config.ARGB_8888)
|
||||
val canvas = Canvas(output)
|
||||
canvas.drawColor(Color.WHITE)
|
||||
|
||||
val titlePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = Color.rgb(15, 23, 42)
|
||||
textSize = 38f
|
||||
typeface = Typeface.DEFAULT_BOLD
|
||||
}
|
||||
val metaPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = Color.rgb(71, 85, 105)
|
||||
textSize = 24f
|
||||
}
|
||||
val borderPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
style = Paint.Style.STROKE
|
||||
strokeWidth = 4f
|
||||
color = Color.rgb(203, 213, 225)
|
||||
}
|
||||
val rawPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
style = Paint.Style.STROKE
|
||||
strokeWidth = 6f
|
||||
color = Color.rgb(239, 68, 68)
|
||||
}
|
||||
val mappedPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
style = Paint.Style.STROKE
|
||||
strokeWidth = 6f
|
||||
color = Color.rgb(59, 130, 246)
|
||||
}
|
||||
val labelBgPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
style = Paint.Style.FILL
|
||||
}
|
||||
val labelPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = Color.WHITE
|
||||
textSize = 28f
|
||||
typeface = Typeface.DEFAULT_BOLD
|
||||
}
|
||||
|
||||
var top = outerPadding.toFloat()
|
||||
drawTitle(canvas, "1. 实时帧 + 原始检测框", top, titlePaint, metaPaint, source, boxes.size)
|
||||
top += titleHeight.toFloat()
|
||||
val rawPanel = RectF(outerPadding.toFloat(), top, (outerPadding + panelWidth).toFloat(), top + panelHeight)
|
||||
drawPanelBorder(canvas, rawPanel, borderPaint)
|
||||
drawBitmapFitCenter(canvas, source, rawPanel)
|
||||
boxes.forEachIndexed { index, box ->
|
||||
val raw = mapRectFitCenter(box.raw, source.width, source.height, rawPanel)
|
||||
canvas.drawRect(raw, rawPaint)
|
||||
drawLabel(canvas, raw.left, max(rawPanel.top, raw.top - 38f), "raw#$index", labelBgPaint, labelPaint, rawPaint.color)
|
||||
}
|
||||
|
||||
top = rawPanel.bottom + panelGap
|
||||
drawTitle(canvas, "2. 实际映射到预览后的框", top, titlePaint, metaPaint, source, previewWidth, previewHeight)
|
||||
top += titleHeight.toFloat()
|
||||
val mappedPanel = RectF(outerPadding.toFloat(), top, (outerPadding + panelWidth).toFloat(), top + panelHeight)
|
||||
drawPanelBorder(canvas, mappedPanel, borderPaint)
|
||||
drawBitmapCenterCrop(canvas, source, mappedPanel)
|
||||
boxes.forEachIndexed { index, box ->
|
||||
val mapped = mapRectFromPreview(box.mapped, previewWidth, previewHeight, mappedPanel)
|
||||
canvas.drawRect(mapped, mappedPaint)
|
||||
drawLabel(canvas, mapped.left, max(mappedPanel.top, mapped.top - 38f), "mapped#$index", labelBgPaint, labelPaint, mappedPaint.color)
|
||||
}
|
||||
|
||||
top = mappedPanel.bottom + panelGap
|
||||
val infoRect = RectF(outerPadding.toFloat(), top, (outerPadding + panelWidth).toFloat(), top + 220)
|
||||
drawPanelBorder(canvas, infoRect, borderPaint)
|
||||
canvas.drawText("输出目录:cache/face_trace_live.png", infoRect.left + 24f, infoRect.top + 52f, metaPaint)
|
||||
canvas.drawText("图例:红=ML Kit 原始框 蓝=CameraX 实际映射框", infoRect.left + 24f, infoRect.top + 92f, metaPaint)
|
||||
canvas.drawText(metadata, infoRect.left + 24f, infoRect.top + 132f, metaPaint)
|
||||
boxes.firstOrNull()?.let { first ->
|
||||
canvas.drawText("first raw=${first.raw} mapped=${first.mapped}", infoRect.left + 24f, infoRect.top + 172f, metaPaint)
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
private fun chooseHighlightIndex(
|
||||
steps: List<FaceDebugStep>,
|
||||
results: List<RecognitionResult>
|
||||
): Int? {
|
||||
val matchedIndex = results.indexOfFirst { it.isMatched }
|
||||
if (matchedIndex >= 0) return matchedIndex
|
||||
if (steps.isEmpty()) return null
|
||||
return steps.indices.maxByOrNull { steps[it].cropRect.width() * steps[it].cropRect.height() }
|
||||
}
|
||||
|
||||
private fun drawTitle(
|
||||
canvas: Canvas,
|
||||
title: String,
|
||||
top: Float,
|
||||
titlePaint: Paint,
|
||||
metaPaint: Paint,
|
||||
source: Bitmap,
|
||||
extra: Any? = null
|
||||
) {
|
||||
canvas.drawText(title, 32f, top + 36f, titlePaint)
|
||||
val subtitle = when (extra) {
|
||||
is Int -> "source=${source.width}x${source.height} faces=$extra"
|
||||
is String -> extra
|
||||
else -> "source=${source.width}x${source.height}"
|
||||
}
|
||||
canvas.drawText(subtitle, 32f, top + 48f + 26f, metaPaint)
|
||||
}
|
||||
|
||||
private fun drawTitle(
|
||||
canvas: Canvas,
|
||||
title: String,
|
||||
top: Float,
|
||||
titlePaint: Paint,
|
||||
metaPaint: Paint,
|
||||
source: Bitmap,
|
||||
previewWidth: Int,
|
||||
previewHeight: Int
|
||||
) {
|
||||
canvas.drawText(title, 32f, top + 36f, titlePaint)
|
||||
val subtitle = "source=${source.width}x${source.height} preview=${previewWidth}x${previewHeight}"
|
||||
canvas.drawText(subtitle, 32f, top + 48f + 26f, metaPaint)
|
||||
}
|
||||
|
||||
private fun drawTitle(
|
||||
canvas: Canvas,
|
||||
title: String,
|
||||
top: Float,
|
||||
titlePaint: Paint,
|
||||
metaPaint: Paint,
|
||||
source: Bitmap,
|
||||
cropRect: android.graphics.Rect?
|
||||
) {
|
||||
canvas.drawText(title, 32f, top + 36f, titlePaint)
|
||||
val subtitle = "source=${source.width}x${source.height} crop=${cropRect ?: "-"}"
|
||||
canvas.drawText(subtitle, 32f, top + 48f + 26f, metaPaint)
|
||||
}
|
||||
|
||||
private fun drawPanelBorder(canvas: Canvas, rect: RectF, paint: Paint) {
|
||||
canvas.drawRoundRect(rect, 16f, 16f, paint)
|
||||
}
|
||||
|
||||
private fun drawBitmapFitCenter(canvas: Canvas, bitmap: Bitmap, dest: RectF) {
|
||||
val scale = min(dest.width() / bitmap.width, dest.height() / bitmap.height)
|
||||
val drawWidth = bitmap.width * scale
|
||||
val drawHeight = bitmap.height * scale
|
||||
val left = dest.left + (dest.width() - drawWidth) / 2f
|
||||
val top = dest.top + (dest.height() - drawHeight) / 2f
|
||||
canvas.drawBitmap(
|
||||
bitmap,
|
||||
null,
|
||||
RectF(left, top, left + drawWidth, top + drawHeight),
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
private fun drawBitmapCenterCrop(canvas: Canvas, bitmap: Bitmap, dest: RectF) {
|
||||
val scale = max(dest.width() / bitmap.width, dest.height() / bitmap.height)
|
||||
val drawWidth = bitmap.width * scale
|
||||
val drawHeight = bitmap.height * scale
|
||||
val left = dest.left + (dest.width() - drawWidth) / 2f
|
||||
val top = dest.top + (dest.height() - drawHeight) / 2f
|
||||
val saveCount = canvas.save()
|
||||
canvas.clipRect(dest)
|
||||
canvas.drawBitmap(
|
||||
bitmap,
|
||||
null,
|
||||
RectF(left, top, left + drawWidth, top + drawHeight),
|
||||
null
|
||||
)
|
||||
canvas.restoreToCount(saveCount)
|
||||
}
|
||||
|
||||
private fun mapRectFitCenter(rect: android.graphics.Rect, srcWidth: Int, srcHeight: Int, dest: RectF): RectF {
|
||||
val scale = min(dest.width() / srcWidth, dest.height() / srcHeight)
|
||||
val drawWidth = srcWidth * scale
|
||||
val drawHeight = srcHeight * scale
|
||||
val left = dest.left + (dest.width() - drawWidth) / 2f
|
||||
val top = dest.top + (dest.height() - drawHeight) / 2f
|
||||
return RectF(
|
||||
rect.left * scale + left,
|
||||
rect.top * scale + top,
|
||||
rect.right * scale + left,
|
||||
rect.bottom * scale + top
|
||||
)
|
||||
}
|
||||
|
||||
private fun mapRectCenterCrop(rect: android.graphics.Rect, srcWidth: Int, srcHeight: Int, dest: RectF): RectF {
|
||||
val scale = max(dest.width() / srcWidth, dest.height() / srcHeight)
|
||||
val drawWidth = srcWidth * scale
|
||||
val drawHeight = srcHeight * scale
|
||||
val left = dest.left + (dest.width() - drawWidth) / 2f
|
||||
val top = dest.top + (dest.height() - drawHeight) / 2f
|
||||
return RectF(
|
||||
rect.left * scale + left,
|
||||
rect.top * scale + top,
|
||||
rect.right * scale + left,
|
||||
rect.bottom * scale + top
|
||||
)
|
||||
}
|
||||
|
||||
private fun mapRectFromPreview(rect: RectF, previewWidth: Int, previewHeight: Int, dest: RectF): RectF {
|
||||
val safeWidth = previewWidth.takeIf { it > 0 } ?: 1
|
||||
val safeHeight = previewHeight.takeIf { it > 0 } ?: 1
|
||||
val scaleX = dest.width() / safeWidth
|
||||
val scaleY = dest.height() / safeHeight
|
||||
return RectF(
|
||||
dest.left + rect.left * scaleX,
|
||||
dest.top + rect.top * scaleY,
|
||||
dest.left + rect.right * scaleX,
|
||||
dest.top + rect.bottom * scaleY
|
||||
)
|
||||
}
|
||||
|
||||
private fun drawLabel(
|
||||
canvas: Canvas,
|
||||
left: Float,
|
||||
top: Float,
|
||||
text: String,
|
||||
bgPaint: Paint,
|
||||
textPaint: Paint,
|
||||
color: Int
|
||||
) {
|
||||
val paddingX = 12f
|
||||
val paddingY = 10f
|
||||
val textWidth = textPaint.measureText(text)
|
||||
val rect = RectF(left, top, left + textWidth + paddingX * 2, top + 38f)
|
||||
bgPaint.color = color
|
||||
canvas.drawRoundRect(rect, 8f, 8f, bgPaint)
|
||||
canvas.drawText(text, rect.left + paddingX, rect.bottom - paddingY, textPaint)
|
||||
}
|
||||
|
||||
private fun drawEmpty(canvas: Canvas, rect: RectF, text: String) {
|
||||
val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = Color.rgb(148, 163, 184)
|
||||
textSize = 28f
|
||||
}
|
||||
canvas.drawText(text, rect.left + 24f, rect.centerY(), paint)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user