remove debug info

This commit is contained in:
2026-05-11 18:00:52 +08:00
parent 2f83a1f610
commit 7998428587
4 changed files with 1 additions and 722 deletions

View File

@@ -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)
}
}

View File

@@ -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
)

View File

@@ -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"
}
}

View File

@@ -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)
}
}