解决face-api.js人脸匹配问题:多/单人脸检测与匹配
2025-03-11 11:43:38
如何识别上传图片中与本地图片匹配的人脸
最近做了一个 Angular 应用,用 face-api.js
来做人脸识别。这个应用处理一堆本地图片来识别人脸,并生成对应的面部符。当用户上传一张新图片,应用会检测并提取这张图片的面部符,然后和本地图片的面部描述符比较,找出并返回所有包含匹配人脸的图片。
但是,目前这段代码无法正确返回匹配的人脸图片。 下面我们来分析下问题,并给出解决办法。
一、 问题原因分析
从代码看, 主要问题可能出现在几个地方:
- 单张/多张人脸检测: 本地图片加载(
loadLocalImages
)时使用了detectSingleFace
, 假设每张本地图片中都只有一张人脸。如果本地图片包含多张人脸,detectSingleFace
只会检测并返回第一个人脸, 其余人脸被忽略。上传图片处理(onUpload
)部分同样使用了detectSingleFace
. - FaceMatcher 的使用:
FaceMatcher
构造函数接收一个LabeledFaceDescriptors
数组,或者一个已有的FaceMatcher
对象。当前代码中每次调用onUpload
时,用this.faceDescriptors
构建了一个新的FaceMatcher
,而之前的匹配状态和数据并没有保留。尽管这个点本身不是不返回图片直接原因,但会导致匹配效率低。 更主要的问题出现在下面一点: - 匹配逻辑错误: 使用
this.faceDescriptors
直接做filter
,然后对每一个descriptor
做faceMatcher.findBestMatch(detection.descriptor)
,是错误的. 因为this.faceDescriptors
的label是imagePath, 而descriptor是一个数组(即使其中只有一个元素),包含人脸特征数据。即使找到最佳匹配,也无法知道是本地多张照片中的哪张人脸.faceMatcher.findBestMatch
是对每个labeledFaceDescriptors做匹配. 换句话说,即使找到最接近的,也只能知道这张人脸最接近localImages中若干人脸数据(注意:这里“若干人脸”是指本地图片可能加载了多张人脸的特征)的哪一个特征值,并不知道来自哪张图。
二、 解决方案
针对上面分析的原因,提出下面几个解决步骤。
1. 改造人脸检测方法
需要同时支持单张人脸和多张人脸检测. 把 detectSingleFace
换成 detectAllFaces
.
原理: detectAllFaces
可以检测图片中的所有人脸,返回一个包含所有人脸信息的数组。
代码示例 (对 loadLocalImages
和 onUpload
进行修改):
// ... 其他代码 ...
async loadLocalImages() {
try {
for (const imagePath of this.localImages) {
const img = await faceapi.fetchImage(imagePath);
const detections = await faceapi.detectAllFaces(img).withFaceLandmarks().withFaceDescriptors(); // 改成 detectAllFaces
if (detections && detections.length > 0) {
for (const detection of detections) { // 遍历所有检测到的人脸
const label = imagePath; // Use image file path as label
this.faceDescriptors.push(new faceapi.LabeledFaceDescriptors(label, [detection.descriptor]));
}
console.log(`Loaded descriptors for ${imagePath}:`, detections);
} else {
console.log(`No face detected in ${imagePath}`);
}
}
console.log('Local images loaded and processed:', this.faceDescriptors);
} catch (error) {
console.error('Error loading local images:', error);
}
}
async onUpload() {
if (this.selectedFile) {
const reader = new FileReader();
reader.onload = async () => {
try {
const imageBlob = this.dataURLToBlob(reader.result as string);
if (imageBlob) {
const img = await faceapi.bufferToImage(imageBlob);
const detections = await faceapi.detectAllFaces(img).withFaceLandmarks().withFaceDescriptors();// 改成 detectAllFaces
if (detections && detections.length > 0) {
//后续匹配逻辑见第3点
} else {
this.images = [];
console.log('No face detected in the uploaded image.');
}
} else {
this.images = [];
console.error('Failed to convert Data URL to Blob.');
}
} catch (error) {
console.error('Error processing uploaded image:', error);
}
};
reader.readAsDataURL(this.selectedFile);
}
}
// ... 其他代码 ...
dataURLToBlob(dataURL: string) {
const arr = dataURL.split(',');
const mime = arr[0].match(/:(.*?);/)![1];
const bstr = atob(arr[1]);
let n = bstr.length;
const u8arr = new Uint8Array(n);
while (n--) {
u8arr[n] = bstr.charCodeAt(n);
}
return new Blob([u8arr], { type: mime });
}
2. 创建并维护 FaceMatcher
在 ngOnInit
中创建 FaceMatcher
,而不是在 onUpload
中。
原理: 只创建一个 FaceMatcher
对象,在多次上传图片时可以复用,提高效率,且确保了基准一致。
代码示例:
// ... 其他代码 ...
faceMatcher: faceapi.FaceMatcher | null = null; // 添加 FaceMatcher 属性
async ngOnInit() {
await this.loadModels();
await this.loadLocalImages();
this.faceMatcher = new faceapi.FaceMatcher(this.faceDescriptors); // 创建 FaceMatcher
}
// ... 其他代码 ...
3. 修改匹配逻辑
使用detectAllFaces
后,onUpload
中的匹配逻辑也要修改。 对上传图片检测到的每一个人脸,都在 FaceMatcher
中寻找最佳匹配。
原理: 对上传图片中的 每个 人脸,都和本地图片中 所有 人脸进行比对. 这样,即使上传的是一张集体照,也能正确匹配.
代码示例 (修改 onUpload
):
// ... 其他代码 ...
async onUpload() {
if (this.selectedFile) {
const reader = new FileReader();
reader.onload = async () => {
try {
const imageBlob = this.dataURLToBlob(reader.result as string);
if (imageBlob) {
const img = await faceapi.bufferToImage(imageBlob);
const detections = await faceapi.detectAllFaces(img).withFaceLandmarks().withFaceDescriptors();
if (detections && detections.length > 0) {
console.log('Uploaded image descriptors:', detections);
const threshold = 0.6; // 阈值,可以根据实际情况调整
const matchedImageURLs: string[] = [];
for (const detection of detections) { // 遍历上传图片中检测到的每个人脸
for (const labeledFaceDescriptors of this.faceDescriptors){
const match = this.faceMatcher!.findBestMatch(detection.descriptor);
if (match.distance < threshold && match.label != 'unknown') {
if (!matchedImageURLs.includes(labeledFaceDescriptors.label)) {
matchedImageURLs.push(labeledFaceDescriptors.label);
}
}
}
}
if (matchedImageURLs.length > 0) {
console.log('Matched image URLs:', matchedImageURLs);
this.images = matchedImageURLs;
} else {
this.images = [];
console.log('No exact matches found.');
}
} else {
this.images = [];
console.log('No face detected in the uploaded image.');
}
} else {
this.images = [];
console.error('Failed to convert Data URL to Blob.');
}
} catch (error) {
console.error('Error processing uploaded image:', error);
}
};
reader.readAsDataURL(this.selectedFile);
}
}
// ... 其他代码 ...
4. 处理 “unknown” 标签
如果 FaceMatcher
找不到匹配的人脸,findBestMatch
会返回一个标签为 "unknown" 的结果。 我们的场景中,需要排除这些结果。
原理: "unknown" 表示没有找到匹配的人脸,不应被包含在匹配结果中。
代码示例: 在上面的匹配逻辑里,已经通过 match.label != 'unknown'
进行了过滤.
5. (进阶)优化本地图片加载
如果本地图片数量非常多, 每次启动都重新加载、计算描述符会很慢。可以把描述符保存到本地存储(比如 localStorage
或 IndexedDB
), 启动时先检查是否有已保存的描述符,如果有,直接加载,不用重新计算。
原理: 空间换时间,避免重复计算。
代码示例 (仅展示思路,具体实现较复杂):
// ... 其他代码 ...
async loadLocalImages() {
// 1. 尝试从 localStorage 加载已保存的描述符
const savedDescriptors = localStorage.getItem('faceDescriptors');
if (savedDescriptors) {
this.faceDescriptors = JSON.parse(savedDescriptors);
console.log('Loaded descriptors from localStorage');
return;
}
// 2. 如果没有保存的描述符,正常加载图片、计算描述符
try {
for (const imagePath of this.localImages) {
const img = await faceapi.fetchImage(imagePath);
const detections = await faceapi.detectAllFaces(img).withFaceLandmarks().withFaceDescriptors(); // 改成 detectAllFaces
if (detections && detections.length > 0) {
for (const detection of detections) { // 遍历所有检测到的人脸
const label = imagePath; // Use image file path as label
this.faceDescriptors.push(new faceapi.LabeledFaceDescriptors(label, [detection.descriptor]));
}
console.log(`Loaded descriptors for ${imagePath}:`, detections);
} else {
console.log(`No face detected in ${imagePath}`);
}
}
console.log('Local images loaded and processed:', this.faceDescriptors);
} catch (error) {
console.error('Error loading local images:', error);
}
// 3. 把计算好的描述符保存到 localStorage
localStorage.setItem('faceDescriptors', JSON.stringify(this.faceDescriptors));
}
// ...其他代码,使用IndexedDB类似...
注意: 序列化/反序列化 LabeledFaceDescriptors
对象比较麻烦,直接用 JSON.stringify
不行。 上面的代码只是简单演示。 正确的做法是只保存描述符数组(Float32Array), 加载后,再重新创建 LabeledFaceDescriptors
对象。
6. 安全建议
- 限制上传图片大小和格式: 防止恶意用户上传超大图片导致服务器资源耗尽,或上传恶意文件。可以在前端和后端都进行校验。
- 对人脸识别结果进行二次确认(可选): 如果应用场景对准确性要求很高,可以在返回匹配结果后, 增加人工确认环节, 避免误判.
- 谨慎处理用户的人脸数据。 遵守相关隐私法规, 比如 GDPR。
总结
经过以上修改, 现在应用应该能正确识别上传图片中与本地图片匹配的人脸了。主要改进是:
- 使用
detectAllFaces
检测所有人脸, 包括单张和多张人脸图片。 - 在组件初始化时创建
FaceMatcher
, 而不是每次上传都创建,避免重复运算。 - 修改了匹配循环逻辑, 使上传图像中的每个人脸特征都和本地所有图片人脸进行对比.
- 过滤掉
FaceMatcher
返回的 "unknown" 结果. - (可选)增加预加载,缓存机制, 提升大量本地图片时的加载速度.
按照上面修改后, 多人脸,单人脸应该都能正确处理和匹配。 如果还有问题,请检查一下 threshold
阈值是否设置合理. 如果阈值过高,可能导致本来匹配的人脸被过滤掉。