remove debug info
This commit is contained in:
@@ -1,10 +1,7 @@
|
|||||||
package com.example.studentfaceregistry
|
package com.example.studentfaceregistry
|
||||||
|
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
import android.graphics.Bitmap
|
|
||||||
import android.graphics.BitmapFactory
|
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import android.content.pm.ApplicationInfo
|
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.graphics.Rect
|
import android.graphics.Rect
|
||||||
import android.graphics.RectF
|
import android.graphics.RectF
|
||||||
@@ -19,7 +16,6 @@ import android.view.View
|
|||||||
import android.widget.EditText
|
import android.widget.EditText
|
||||||
import android.widget.Button
|
import android.widget.Button
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
import android.widget.ImageView
|
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import android.widget.Toast
|
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.FaceProcessor
|
||||||
import com.example.studentfaceregistry.face.MatchType
|
import com.example.studentfaceregistry.face.MatchType
|
||||||
import com.example.studentfaceregistry.face.RecognitionResult
|
import com.example.studentfaceregistry.face.RecognitionResult
|
||||||
import com.example.studentfaceregistry.ui.FaceDebugRenderer
|
|
||||||
import com.google.mlkit.vision.common.InputImage
|
import com.google.mlkit.vision.common.InputImage
|
||||||
import com.google.mlkit.vision.face.Face
|
import com.google.mlkit.vision.face.Face
|
||||||
import com.example.studentfaceregistry.ui.DetectionUi
|
import com.example.studentfaceregistry.ui.DetectionUi
|
||||||
@@ -49,8 +44,6 @@ import kotlinx.coroutines.Dispatchers
|
|||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.io.File
|
|
||||||
import java.io.FileOutputStream
|
|
||||||
import java.util.concurrent.Executors
|
import java.util.concurrent.Executors
|
||||||
|
|
||||||
class MainActivity : AppCompatActivity() {
|
class MainActivity : AppCompatActivity() {
|
||||||
@@ -64,8 +57,6 @@ class MainActivity : AppCompatActivity() {
|
|||||||
private lateinit var uploadStatusText: TextView
|
private lateinit var uploadStatusText: TextView
|
||||||
private lateinit var uploadUrlText: TextView
|
private lateinit var uploadUrlText: TextView
|
||||||
private lateinit var pauseRecognitionButton: Button
|
private lateinit var pauseRecognitionButton: Button
|
||||||
private lateinit var debugTraceStatusText: TextView
|
|
||||||
private lateinit var debugTraceImageView: ImageView
|
|
||||||
private lateinit var homePanel: View
|
private lateinit var homePanel: View
|
||||||
private lateinit var recognitionPanel: View
|
private lateinit var recognitionPanel: View
|
||||||
private lateinit var uploadPanel: View
|
private lateinit var uploadPanel: View
|
||||||
@@ -88,7 +79,6 @@ class MainActivity : AppCompatActivity() {
|
|||||||
private var euclideanThreshold = DEFAULT_EUCLIDEAN_THRESHOLD
|
private var euclideanThreshold = DEFAULT_EUCLIDEAN_THRESHOLD
|
||||||
private var cosineThreshold = DEFAULT_COSINE_THRESHOLD
|
private var cosineThreshold = DEFAULT_COSINE_THRESHOLD
|
||||||
@Volatile private var matcher = FaceMatcher(DEFAULT_EUCLIDEAN_THRESHOLD, 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 recognitionHistory = mutableMapOf<Int, ArrayDeque<RecognitionResult>>()
|
||||||
private val recognitionCache = mutableMapOf<Int, CachedRecognition>()
|
private val recognitionCache = mutableMapOf<Int, CachedRecognition>()
|
||||||
|
|
||||||
@@ -292,9 +282,6 @@ class MainActivity : AppCompatActivity() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
addView(matcherConfigPanel)
|
addView(matcherConfigPanel)
|
||||||
if (isDebugBuild()) {
|
|
||||||
addView(createDebugTracePanel())
|
|
||||||
}
|
|
||||||
|
|
||||||
val cameraFrame = FrameLayout(this@MainActivity).apply {
|
val cameraFrame = FrameLayout(this@MainActivity).apply {
|
||||||
layoutParams = LinearLayout.LayoutParams(
|
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 {
|
private fun createUploadPanel(): View {
|
||||||
return LinearLayout(this).apply {
|
return LinearLayout(this).apply {
|
||||||
orientation = LinearLayout.VERTICAL
|
orientation = LinearLayout.VERTICAL
|
||||||
@@ -588,14 +525,9 @@ class MainActivity : AppCompatActivity() {
|
|||||||
if (shouldRefreshRecognition(face.trackingId, now)) index to face else null
|
if (shouldRefreshRecognition(face.trackingId, now)) index to face else null
|
||||||
}
|
}
|
||||||
val refreshedResults = mutableMapOf<Int, RecognitionResult>()
|
val refreshedResults = mutableMapOf<Int, RecognitionResult>()
|
||||||
val debugBitmap = if (faces.isNotEmpty() && (facesNeedingRefresh.isNotEmpty() || isDebugBuild())) {
|
|
||||||
ImageUtils.imageProxyToBitmap(image)
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
if (facesNeedingRefresh.isNotEmpty()) {
|
if (facesNeedingRefresh.isNotEmpty()) {
|
||||||
val bitmap = requireNotNull(debugBitmap)
|
val bitmap = ImageUtils.imageProxyToBitmap(image)
|
||||||
facesNeedingRefresh.forEach { (index, face) ->
|
facesNeedingRefresh.forEach { (index, face) ->
|
||||||
val embedding = faceProcessor.embedFace(bitmap, face)
|
val embedding = faceProcessor.embedFace(bitmap, face)
|
||||||
val refreshed = matcher.findNearest(embedding, students)
|
val refreshed = matcher.findNearest(embedding, students)
|
||||||
@@ -620,8 +552,6 @@ class MainActivity : AppCompatActivity() {
|
|||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
if (currentMode == AppMode.RECOGNITION) {
|
if (currentMode == AppMode.RECOGNITION) {
|
||||||
val detections = mapDetectionsToPreview(image, rawDetections)
|
val detections = mapDetectionsToPreview(image, rawDetections)
|
||||||
traceFacePipeline(image, faces, debugBitmap, rawDetections, detections)
|
|
||||||
maybeSaveLiveDebugFrame(image, debugBitmap, rawDetections, detections)
|
|
||||||
overlayView.update(detections.filter { it.result.isMatched })
|
overlayView.update(detections.filter { it.result.isMatched })
|
||||||
updateRecognitionStatus(detections)
|
updateRecognitionStatus(detections)
|
||||||
}
|
}
|
||||||
@@ -699,97 +629,6 @@ class MainActivity : AppCompatActivity() {
|
|||||||
uploadUrlText.text = "-"
|
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? {
|
private fun ensureProcessor(): FaceProcessor? {
|
||||||
val current = processor
|
val current = processor
|
||||||
if (current != null) return current
|
if (current != null) return current
|
||||||
@@ -805,14 +644,6 @@ class MainActivity : AppCompatActivity() {
|
|||||||
return created
|
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 {
|
private fun hasCameraPermission(): Boolean {
|
||||||
return ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED
|
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 {
|
private fun stabilizeRecognition(face: Face, result: RecognitionResult): RecognitionResult {
|
||||||
val trackId = face.trackingId ?: return result
|
val trackId = face.trackingId ?: return result
|
||||||
val history = recognitionHistory.getOrPut(trackId) { ArrayDeque() }
|
val history = recognitionHistory.getOrPut(trackId) { ArrayDeque() }
|
||||||
@@ -1018,8 +806,6 @@ class MainActivity : AppCompatActivity() {
|
|||||||
private const val DEFAULT_COSINE_THRESHOLD = 0.6f
|
private const val DEFAULT_COSINE_THRESHOLD = 0.6f
|
||||||
private const val RECOGNITION_REFRESH_MS = 400L
|
private const val RECOGNITION_REFRESH_MS = 400L
|
||||||
private const val ANALYSIS_INTERVAL_MS = 120L
|
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)
|
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
|
package com.example.studentfaceregistry.face
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.pm.ApplicationInfo
|
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.graphics.Rect
|
import android.graphics.Rect
|
||||||
import android.util.Log
|
|
||||||
import com.google.mlkit.vision.common.InputImage
|
import com.google.mlkit.vision.common.InputImage
|
||||||
import com.google.mlkit.vision.face.Face
|
import com.google.mlkit.vision.face.Face
|
||||||
import com.google.mlkit.vision.face.FaceDetection
|
import com.google.mlkit.vision.face.FaceDetection
|
||||||
@@ -20,7 +18,6 @@ class FaceProcessor(context: Context) : AutoCloseable {
|
|||||||
private val embedder = FaceEmbedder(context)
|
private val embedder = FaceEmbedder(context)
|
||||||
private val aligner = FaceAligner()
|
private val aligner = FaceAligner()
|
||||||
private val preprocessor = ImagePreprocessor()
|
private val preprocessor = ImagePreprocessor()
|
||||||
private val debugEnabled = context.applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE != 0
|
|
||||||
|
|
||||||
// 实时检测器:快速模式,用于摄像头预览
|
// 实时检测器:快速模式,用于摄像头预览
|
||||||
private val realtimeDetector = FaceDetection.getClient(
|
private val realtimeDetector = FaceDetection.getClient(
|
||||||
@@ -91,30 +88,6 @@ class FaceProcessor(context: Context) : AutoCloseable {
|
|||||||
val leftEye = face.getLandmark(FaceLandmark.LEFT_EYE) ?: return faceBitmap
|
val leftEye = face.getLandmark(FaceLandmark.LEFT_EYE) ?: return faceBitmap
|
||||||
val rightEye = face.getLandmark(FaceLandmark.RIGHT_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(
|
return aligner.align(
|
||||||
bitmap = faceBitmap,
|
bitmap = faceBitmap,
|
||||||
leftEyeX = leftEye.position.x,
|
leftEyeX = leftEye.position.x,
|
||||||
@@ -140,18 +113,6 @@ class FaceProcessor(context: Context) : AutoCloseable {
|
|||||||
return detectFaces(InputImage.fromBitmap(bitmap, 0))
|
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 {
|
fun embedFace(bitmap: Bitmap, face: Face): FloatArray {
|
||||||
val crop = requireNotNull(safeCropRect(bitmap, face.boundingBox)) { "人脸区域超出图片边界。" }
|
val crop = requireNotNull(safeCropRect(bitmap, face.boundingBox)) { "人脸区域超出图片边界。" }
|
||||||
val cropped = Bitmap.createBitmap(bitmap, crop.left, crop.top, crop.width(), crop.height())
|
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 aligned = alignFaceWithLandmarks(cropped, face, crop.left, crop.top)
|
||||||
val preprocessed = preprocessor.preprocessRealtime(aligned)
|
val preprocessed = preprocessor.preprocessRealtime(aligned)
|
||||||
return embedder.embed(preprocessed)
|
return embedder.embed(preprocessed)
|
||||||
@@ -205,8 +160,4 @@ class FaceProcessor(context: Context) : AutoCloseable {
|
|||||||
|
|
||||||
return Rect(left, top, right, bottom)
|
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