如何借助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
相关产品推荐
相关产品推荐

