You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何借助FaceNet从ML Kit检测的人脸中提取人脸特征向量?

基于ML Kit+FaceNet实现人脸特征提取与认证

核心流程概述

你已经用ML Kit完成了人脸检测,接下来需要裁剪人脸区域→预处理成FaceNet要求的输入格式→模型推理得到特征向量→存储/比对,以下是具体实现步骤:


1. 准备FaceNet模型

使用TensorFlow Lite版的FaceNet预训练模型,将模型文件(比如facenet.tflite)放入项目的assets目录。

2. 封装FaceNet工具类

创建FaceNetHelper类,负责模型加载、图像预处理和特征提取:

class FaceNetHelper(context: Context) {
    private val interpreter: Interpreter

    init {
        // 加载TFLite模型
        val modelFile = context.assets.openFd("facenet.tflite")
        val interpreterOptions = InterpreterOptions()
        interpreter = Interpreter(
            FileDescriptorInputStream(modelFile.fileDescriptor),
            interpreterOptions
        )
        modelFile.close()
    }

    // 预处理图像:转成160x160 RGB,归一化到[-1,1]
    private fun preprocessBitmap(bitmap: Bitmap): Array<Array<Array<FloatArray>>> {
        val resizedBitmap = Bitmap.createScaledBitmap(bitmap, 160, 160, true)
        val input = Array(1) { Array(160) { Array(160) { FloatArray(3) } } }

        for (y in 0 until 160) {
            for (x in 0 until 160) {
                val pixel = resizedBitmap.getPixel(x, y)
                // 归一化:(像素值/255 - 0.5) * 2 → 范围[-1,1]
                input[0][y][x][0] = ((Color.red(pixel) / 255f) - 0.5f) * 2
                input[0][y][x][1] = ((Color.green(pixel) / 255f) - 0.5f) * 2
                input[0][y][x][2] = ((Color.blue(pixel) / 255f) - 0.5f) * 2
            }
        }
        return input
    }

    // 提取人脸特征向量
    fun extractEmbedding(bitmap: Bitmap): FloatArray {
        val input = preprocessBitmap(bitmap)
        // FaceNet输出是1x512的特征向量
        val output = Array(1) { FloatArray(512) }
        interpreter.run(input, output)
        return output[0]
    }

    // 计算两个特征向量的余弦相似度
    fun calculateSimilarity(embedding1: FloatArray, embedding2: FloatArray): Float {
        var dotProduct = 0f
        var norm1 = 0f
        var norm2 = 0f
        for (i in embedding1.indices) {
            dotProduct += embedding1[i] * embedding2[i]
            norm1 += embedding1[i] * embedding1[i]
            norm2 += embedding2[i] * embedding2[i]
        }
        return dotProduct / (sqrt(norm1) * sqrt(norm2))
    }

    fun close() {
        interpreter.close()
    }
}

3. 改造你的人脸检测代码

在processImageProxy中,从ML Kit的Face对象裁剪人脸区域,调用FaceNet提取特征:

// 全局或类内初始化FaceNetHelper
private lateinit var faceNetHelper: FaceNetHelper

// 在onCreate中初始化
faceNetHelper = FaceNetHelper(this)

private fun processImageProxy(detector: FaceDetector, imageProxy: ImageProxy){
    val inputImage = InputImage.fromMediaImage(imageProxy.image!!, imageProxy.imageInfo.rotationDegrees)

    detector.process(inputImage)
        .addOnSuccessListener { faces ->
            binding.faceboxOverlay.clear()

            faces.forEach{ face ->
                // 1. 从ImageProxy中裁剪人脸区域
                val faceBitmap = cropFaceFromImageProxy(imageProxy, face.boundingBox)
                faceBitmap?.let {
                    // 2. 提取特征向量
                    val embedding = faceNetHelper.extractEmbedding(it)
                    Log.i("FaceEmbedding", "特征向量长度:${embedding.size}")

                    // 3. 存储特征向量(示例用SharedPreferences,实际可改用Room)
                    saveEmbeddingToStorage("user1", embedding)

                    // 4. 身份验证示例:和存储的特征比对
                    val storedEmbedding = loadEmbeddingFromStorage("user1")
                    storedEmbedding?.let { stored ->
                        val similarity = faceNetHelper.calculateSimilarity(embedding, stored)
                        Log.i("AuthResult", "相似度:${similarity},验证${if(similarity > 0.7) "通过" else "失败"}")
                    }
                }
            }
        }
        .addOnFailureListener {
            Log.e(TAG, it.message ?: it.toString())
        }
        .addOnCompleteListener {
            imageProxy.close()
        }
}

// 从ImageProxy中裁剪人脸区域的工具方法
private fun cropFaceFromImageProxy(imageProxy: ImageProxy, boundingBox: Rect): Bitmap? {
    val image = imageProxy.image ?: return null
    // 将Image转成Bitmap
    val bitmap = image.toBitmap()
    // 处理图像旋转:ML Kit的boundingBox是基于旋转后的InputImage,所以需要调整Bitmap的方向
    val rotatedBitmap = when(imageProxy.imageInfo.rotationDegrees) {
        90 -> rotateBitmap(bitmap, 90f)
        180 -> rotateBitmap(bitmap, 180f)
        270 -> rotateBitmap(bitmap, 270f)
        else -> bitmap
    }
    // 裁剪人脸区域,注意边界检查
    val cropRect = Rect(boundingBox)
    cropRect.left = max(0, cropRect.left)
    cropRect.top = max(0, cropRect.top)
    cropRect.right = min(rotatedBitmap.width, cropRect.right)
    cropRect.bottom = min(rotatedBitmap.height, cropRect.bottom)
    return Bitmap.createBitmap(rotatedBitmap, cropRect.left, cropRect.top, cropRect.width(), cropRect.height())
}

// 旋转Bitmap的工具方法
private fun rotateBitmap(bitmap: Bitmap, degrees: Float): Bitmap {
    val matrix = Matrix().apply { postRotate(degrees) }
    return Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true)
}

// 存储特征向量到SharedPreferences
private fun saveEmbeddingToStorage(key: String, embedding: FloatArray) {
    val sharedPrefs = getSharedPreferences("FaceAuthPrefs", MODE_PRIVATE)
    val editor = sharedPrefs.edit()
    val floatArray = embedding.joinToString(",")
    editor.putString(key, floatArray)
    editor.apply()
}

// 从SharedPreferences加载特征向量
private fun loadEmbeddingFromStorage(key: String): FloatArray? {
    val sharedPrefs = getSharedPreferences("FaceAuthPrefs", MODE_PRIVATE)
    val floatString = sharedPrefs.getString(key, null) ?: return null
    return floatString.split(",").map { it.toFloat() }.toFloatArray()
}

4. 关键注意事项

  • 模型输入格式:不同FaceNet模型的输入要求可能不同(比如有些是224x224,归一化范围是[0,1]),需要对应调整预处理逻辑。
  • 阈值设置:余弦相似度阈值建议在0.6~0.8之间,根据实际测试调整,越高越严格。
  • 性能优化:可以开启TFLite的硬件加速(比如NNAPI、GPU),在InterpreterOptions中配置。
  • 内存管理:使用完FaceNetHelper后记得调用close()释放资源。

内容的提问来源于stack exchange,提问作者Leoh Tek

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.06 11:31:13