From 799842858791deccdff077f5330e0dbcbd7d5db0 Mon Sep 17 00:00:00 2001 From: Shuming Liu Date: Mon, 11 May 2026 18:00:52 +0800 Subject: [PATCH] remove debug info --- .../studentfaceregistry/MainActivity.kt | 216 +-------- .../face/FaceDebugTrace.kt | 12 - .../studentfaceregistry/face/FaceProcessor.kt | 49 -- .../ui/FaceDebugRenderer.kt | 446 ------------------ 4 files changed, 1 insertion(+), 722 deletions(-) delete mode 100644 app/src/main/java/com/example/studentfaceregistry/face/FaceDebugTrace.kt delete mode 100644 app/src/main/java/com/example/studentfaceregistry/ui/FaceDebugRenderer.kt diff --git a/app/src/main/java/com/example/studentfaceregistry/MainActivity.kt b/app/src/main/java/com/example/studentfaceregistry/MainActivity.kt index 3f00b6e..5f4d3e6 100644 --- a/app/src/main/java/com/example/studentfaceregistry/MainActivity.kt +++ b/app/src/main/java/com/example/studentfaceregistry/MainActivity.kt @@ -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>() private val recognitionCache = mutableMapOf() @@ -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() - 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, - mappedDetections: List - ) { - 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, - bitmap: android.graphics.Bitmap?, - rawDetections: List, - mappedDetections: List - ) { - 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) } } diff --git a/app/src/main/java/com/example/studentfaceregistry/face/FaceDebugTrace.kt b/app/src/main/java/com/example/studentfaceregistry/face/FaceDebugTrace.kt deleted file mode 100644 index 4b68d64..0000000 --- a/app/src/main/java/com/example/studentfaceregistry/face/FaceDebugTrace.kt +++ /dev/null @@ -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 -) diff --git a/app/src/main/java/com/example/studentfaceregistry/face/FaceProcessor.kt b/app/src/main/java/com/example/studentfaceregistry/face/FaceProcessor.kt index b1f9980..49c8d1e 100644 --- a/app/src/main/java/com/example/studentfaceregistry/face/FaceProcessor.kt +++ b/app/src/main/java/com/example/studentfaceregistry/face/FaceProcessor.kt @@ -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 { - 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" - } } diff --git a/app/src/main/java/com/example/studentfaceregistry/ui/FaceDebugRenderer.kt b/app/src/main/java/com/example/studentfaceregistry/ui/FaceDebugRenderer.kt deleted file mode 100644 index e0eebde..0000000 --- a/app/src/main/java/com/example/studentfaceregistry/ui/FaceDebugRenderer.kt +++ /dev/null @@ -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, - results: List, - 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, - 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, - results: List - ): 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) - } -}