OpenCV 这套东西我从第三弹一路写到第六弹,前面几篇分别折腾过图像基础操作、颜色空间、轮廓提取和视频流处理。到了人脸检测和识别这一块,问的人明显变多,原因也很直接:它是少数几个"跑通十分钟、调好要半年"的方向。我这篇不打算把 OpenCV 官方文档翻译一遍,而是把OpenCV 人脸检测和人脸识别从选型、参数、代码到踩坑的完整链条摊开讲,中间会穿插我自己在项目里踩过的具体问题,比如为什么同一张脸在 A 电脑上识别对了、B 电脑上就认错,为什么 Haar 检测框老是抖,为什么 LBPH 训练完信心值全是 20 出头。这篇适合三类人:刚接触计算机视觉、想快速做出一个能跑 Demo 的新手;做过 Haar 但被误检率折磨、想换 DNN 方案的中级开发者;以及准备把识别功能塞进实际产品、需要知道阈值怎么标定、帧率怎么保住的老手。代码全部基于 Python 和 OpenCV,能直接复制运行,涉及参数选择的地方我都会把计算过程写出来,而不是甩一句"自己调"。
1. 先搞清楚"检测"和"识别"到底差在哪
1.1 两个词经常被混着用,但技术栈完全不同
很多刚入门的朋友会把"人脸检测"和"人脸识别"当成一件事,写代码时直接找一个recognize()函数,结果发现 OpenCV 根本没有这个 API。这两个任务在工程上是严格分开的两个阶段:人脸检测回答的是"这张图里有没有脸、脸在哪",输出是一组矩形坐标;人脸识别回答的是"这个框里的脸是谁",输出是一个身份标签加一个相似度或距离值。前者是目标检测问题,后者本质上是特征提取加分类或度量学习问题。
这个区分不是学术洁癖,它直接决定了你的代码结构。我见过不少人把识别模型直接怼到整张原图上,模型当然认不出来,因为训练时喂进去的是裁剪对齐后的人脸小图,推理时给的是带背景、带肩膀、带天花板的全景图,分布完全对不上。正确的流水线一定是:检测拿到人脸框 → 按框裁剪 → 尺寸归一化 → 灰度或颜色归一化 → 送进识别器。中间任何一步偷懒,最后的表现就是"识别不准",然后你会在错误的地方找半天原因。
还有一种情况是把"人脸关键点检测"也算进来,那是第三个任务,输出的是眼睛、鼻尖、嘴角这些坐标,主要用于对齐和活体判断。这一弹我们先不展开,但你要知道它存在,因为后面讲对齐的时候会提到它。
1.2 三条主流路线的选型对比,别一上来就上深度学习
OpenCV 里能直接调起来的人脸方案大概分三档,我按上手难度和实际表现列个表,这张表基本能覆盖 80% 的选型场景:
| 方案 | 核心接口 | 检测速度 | 遮挡/侧脸表现 | 依赖 | 适用场景 |
|---|---|---|---|---|---|
| Haar 级联 | CascadeClassifier | 快(CPU 单帧几毫秒到十几毫秒) | 差,正脸限定 | 无额外依赖,模型自带 | 教学、嵌入式、快速原型 |
| LBP 级联 | CascadeClassifier | 比 Haar 更快 | 略好于 Haar | 无额外依赖 | 低算力设备、实时性优先 |
| DNN(SSD/ResNet) | cv.dnn.readNetFromCaffe | 中(CPU 单帧 30~80ms) | 明显更好,能扛一定角度 | 需要额外下载模型文件 | 工程落地、准确率优先 |
识别侧同样有三档:Eigenfaces(PCA)、Fisherfaces(LDA)、LBPH(局部二值模式直方图)。这三个里我只推荐 LBPH,原因后面细说,简单讲就是它对光照变化的鲁棒性明显好于前两个,而且不需要把训练集整体重训一遍才能加新人。
选型的核心逻辑是:你的瓶颈在速度还是在准确率。如果跑在树莓派或者老笔记本上,帧率要求 30fps,那 Haar 或 LBP 级联是唯一选择,DNN 跑不动。如果跑在带独立显卡的机器上,或者可以接受 10fps,那 DNN 检测加 LBPH 识别是性价比最高的组合。我自己的默认配置就是这个组合,实测在普通办公本上能做到 12~15fps,足够交互使用。
2. 环境搭建与第一个能跑通的检测器
2.1 依赖安装这一步就有坑,contrib 包必须装对
OpenCV 的 Python 包有两个:opencv-python和opencv-contrib-python。前者只有主模块,后者包含cv2.face这个子模块,也就是 LBPH、Eigenfaces、Fisherfaces 这三个识别器的所在地。很多人装完opencv-python之后写cv2.face.LBPHFaceRecognizer_create(),直接报AttributeError: module 'cv2' has no attribute 'face',然后开始怀疑人生。
正确的安装命令是:
pip install opencv-contrib-python注意不要同时装opencv-python和opencv-contrib-python,这两个包共用同一个cv2命名空间,同时存在会出现模块互相覆盖、部分函数莫名其妙消失的情况。如果你之前装过,先卸干净:
pip uninstall opencv-python opencv-contrib-python opencv-python-headless -y pip install opencv-contrib-python==4.9.0.80版本我建议锁在 4.x 的某个具体小版本上,不要用pip install默认拉最新。原因是 OpenCV 4.x 各版本之间cv2.face的接口有过细微变动,比如LBPHFaceRecognizer_create的参数默认值改过,团队协作时不锁版本很容易出现"我这儿能跑你那儿不能跑"。
验证装好没有,跑这两行:
import cv2 as cv print(cv.__version__) print(hasattr(cv, "face"))第二个输出必须是True。如果是False,说明 contrib 没装上,回去检查是不是装了 headless 版本,headless 包是不带 contrib 模块的。
Haar 和 LBP 的级联模型文件随 OpenCV 一起发布,路径可以通过cv.data.haarcascades拿到,不用自己下载。常用的几个:
haarcascade_frontalface_default.xml:正脸,最通用haarcascade_frontalface_alt2.xml:误检略少,速度略慢haarcascade_profileface.xml:侧脸,但只认一个朝向lbpcascade_frontalface.xml:LBP 版本,速度最快
DNN 的模型要单独下,Caffe 版的人脸检测模型由两个文件组成:一个.prototxt描述网络结构,一个.caffemodel存权重。这两个文件加起来十几兆,放到项目目录里,路径写死或者走配置都行。
2.2 Haar 检测的完整代码与三个核心参数的含义
先上能直接跑的版本:
import cv2 as cv def detect_haar(frame): gray = cv.cvtColor(frame, cv.COLOR_BGR2GRAY) gray = cv.equalizeHist(gray) detector = cv.CascadeClassifier( cv.data.haarcascades + "haarcascade_frontalface_default.xml" ) faces = detector.detectMultiScale( gray, scaleFactor=1.1, minNeighbors=5, minSize=(60, 60), maxSize=(400, 400), ) return faces这段代码不到十行,但里面有四个地方值得单独说。
第一是cvtColor转灰度。Haar 特征本身就是基于灰度像素差计算的,喂彩色图进去它内部也会转,你提前转还能省一次转换开销。
第二是equalizeHist直方图均衡化。这一步不是可选项,是强烈建议。人脸检测在逆光、阴天、室内顶灯这些光照不均匀的场景下,对比度低,Haar 的矩形特征差值接近零,分类器就分不出来。均衡化把灰度分布拉开,等于给分类器喂了对比度更高的输入。我实测过,同一个室内场景,不加均衡化漏检率大概 25%,加上之后降到 10% 以内。
第三是scaleFactor。这个参数控制图像金字塔的缩放步长。检测器不是一次性在整张图上扫,而是把图按比例一层层缩小,每层扫一遍。scaleFactor=1.1意味着每层缩小到上一层的 90.9%。层数可以算出来:假设原图宽 640,minSize宽 60,那么需要缩到 60/640 ≈ 0.094 倍,层数 n 满足1.1^(-n) ≈ 0.094,两边取对数,n = ln(0.094) / ln(1/1.1) ≈ 24.8,也就是大约 25 层。这个数字直接影响耗时,25 层意味着同一块区域被扫了 25 次。把scaleFactor调到 1.3,层数降到ln(0.094)/ln(1/1.3) ≈ 8.3,速度能快三倍左右,但代价是可能跳过某些尺寸的脸,因为相邻两层的缩放比例差太大,脸刚好落在两层之间就漏了。
我的经验值:实时视频用 1.2,静态图或者对召回率要求高的场景用 1.1。1.05 太慢,收益不明显,不建议。
第四是minNeighbors。检测器在每个位置会得到很多重叠的候选框,minNeighbors是"一个候选框周围至少要有多少个同样被判定为正的邻居才保留"。设为 0 会把所有候选都吐出来,满屏都是框;设为 5 是比较稳的默认值;设到 10 以上,误检基本没了,但一些侧脸、戴眼镜的脸会被一起干掉。我通常从 5 开始,如果误检多就往上加,如果漏检多就往下减。
minSize和maxSize这两个很多人不设,结果就是远处一个小点也被当成脸,或者画面里一个巨大的物体被反复扫描。设minSize等于"你能接受的最小脸的像素宽度",这个值和你摄像头分辨率、识别距离有关。720p 摄像头、两米距离,人脸宽度大概 100 像素左右,minSize设 60 是合理的;设 30 会引入大量噪声误检,设 150 又会漏掉稍远的人。
3. 换成 DNN 检测之后,收益到底在哪
3.1 模型加载与 blobFromImage 的四个参数怎么定
Haar 最大的问题是它对"正脸"这件事太执着了。人脸偏转超过 15 度、戴个口罩遮住下半张、侧光把半边脸打进阴影,它就开始漏。DNN 方案用的是深度学习检测器,基于 SSD 架构,训练数据里包含了大量角度和遮挡样本,表现明显更稳。
加载和推理的代码:
import cv2 as cv import numpy as np net = cv.dnn.readNetFromCaffe( "deploy.prototxt", "res10_300x300_ssd_iter_140000.caffemodel", ) def detect_dnn(frame, conf_threshold=0.5): h, w = frame.shape[:2] blob = cv.dnn.blobFromImage( frame, scalefactor=1.0, size=(300, 300), mean=(104.0, 177.0, 123.0), swapRB=False, crop=False, ) net.setInput(blob) detections = net.forward() boxes = [] for i in range(detections.shape[2]): confidence = detections[0, 0, i, 2] if confidence < conf_threshold: continue box = detections[0, 0, i, 3:7] * np.array([w, h, w, h]) x1, y1, x2, y2 = box.astype("int") boxes.append((x1, y1, x2 - x1, y2 - y1, float(confidence))) return boxesblobFromImage这四个参数每一个都有讲究。
scalefactor=1.0表示不额外缩放像素值。这个模型的训练输入就是 0~255 范围,不要再除以 255,除错了会导致输出全是低置信度,你会以为是模型坏了。
size=(300, 300)必须和模型训练时的输入尺寸一致。ResNet-10 SSD 这个模型就是 300×300,改成 512 会直接报维度不匹配。
mean=(104.0, 177.0, 123.0)是 BGR 三个通道的均值,模型训练时做了均值减除,推理时必须减同样的值。这三个数看起来奇怪,其实是训练数据集统计出来的,照抄就行,不要自己算。
swapRB=False是因为 OpenCV 读图默认就是 BGR 顺序,而 Caffe 模型训练时用的也是 BGR,所以不用交换。如果你从 PIL 或者别的库拿到的图是 RGB,这里就要设成True,否则检测会明显变差。这个错误特别隐蔽,因为代码不会报错,只是精度下降,你会一直以为是自己阈值没调好。
crop=False表示按比例缩放后不裁剪,保持完整画面。设成True会做中心裁剪,适合输入本身就是正方形的情况,一般用不上。
3.2 置信度阈值和重叠框处理
detections的输出形状是(1, 1, N, 7),前两个维度是批次和通道,第三维是检测到的候选框数量(一般是 200 个),第四维的 7 个值分别是:[batch_id, class_id, confidence, x1, y1, x2, y2],其中坐标是 0~1 的归一化值,所以要乘上原图的宽高还原。
置信度阈值conf_threshold怎么定,这个不能拍脑袋。我的做法是拿一批真实场景的图跑一遍,统计每个误检框和正确框的置信度分布。实际测下来,这个模型正确人脸的置信度普遍在 0.7 以上,误检集中在 0.3~0.5 区间。所以 0.5 是一个比较安全的默认值,如果你的场景背景特别复杂(比如墙上挂了很多圆形物体),可以提到 0.6;如果要求召回率优先,比如做人数统计,可以降到 0.4。
SSD 输出的框之间会有大量重叠,虽然模型本身已经做过一部分 NMS,但阈值比较宽松。工程上通常还要再过一道非极大值抑制:
indices = cv.dnn.NMSBoxes( bboxes=[(x, y, w, h) for x, y, w, h, _ in boxes], scores=[c for *_, c in boxes], score_threshold=0.5, nms_threshold=0.4, )nms_threshold=0.4的意思是:两个框的交并比(IoU)超过 0.4 就认为是同一个目标,保留置信度高的那个。这个值设太低会把挨得近的两张脸合并成一个,设太高又会残留重复框。0.4 是通用默认,如果画面里人多且脸挨得近,建议调到 0.5。
还有一个细节:DNN 检测出来的框,有时候会比真实人脸大一圈,因为训练标注的框本身就包含了部分头发和下巴。用它去裁剪人脸做识别时,可以适当往里收一点,比如左右各收 10%、上下各收 5%,这样裁出来的区域更聚焦在五官上,识别器的输入质量会更好。这个操作看起来粗糙,实际效果比不收缩要稳定。
4. 人脸识别:从数据采集到推理的完整链路
4.1 采集阶段偷的懒,训练阶段会全部还回来
LBPH 的训练数据格式很简单:一堆灰度人脸小图,加上对应的整数标签。但采集环节的规范程度,直接决定后面识别率能达到什么水平。我见过最典型的错误是:同一个人对着摄像头录了 100 张,姿势、表情、光照几乎一样,训练出来信心值全是个位数,看着特别漂亮,换个角度立刻失效。这叫过拟合,只是它过拟合的是姿态和光照,不是身份。
我的采集规范是:
| 维度 | 要求 | 原因 |
|---|---|---|
| 数量 | 每人 40~80 张 | 少于 30 张 LBPH 直方图统计不稳 |
| 角度 | 左右各转 30 度内,上仰下俯各 15 度内 | 覆盖实际使用中的姿态范围 |
| 光照 | 至少两种光源(自然光、室内灯) | LBP 对光照有一定鲁棒性,但不是免疫 |
| 表情 | 中性为主,配合少量微笑、说话 | 表情变化会改变五官相对位置 |
| 距离 | 覆盖实际使用距离的上下浮动 50% | 影响人脸在画面中的像素尺寸 |
采集代码要注意一个容易被忽略的点:必须先检测再裁剪,不能把整帧存下来。我见过有人直接把摄像头画面整张存成训练样本,然后训练时再统一缩放,结果背景占了 70%,模型学的全是背景。
import cv2 as cv import os def collect(uid, save_dir="dataset", max_count=60): os.makedirs(f"{save_dir}/{uid}", exist_ok=True) cap = cv.VideoCapture(0) detector = cv.CascadeClassifier( cv.data.haarcascades + "haarcascade_frontalface_default.xml" ) count = 0 while count < max_count: ok, frame = cap.read() if not ok: break gray = cv.equalizeHist(cv.cvtColor(frame, cv.COLOR_BGR2GRAY)) faces = detector.detectMultiScale(gray, 1.2, 5, minSize=(80, 80)) for (x, y, w, h) in faces: roi = gray[y:y+h, x:x+w] roi = cv.resize(roi, (128, 128)) cv.imwrite(f"{save_dir}/{uid}/{count}.png", roi) count += 1 cv.rectangle(frame, (x, y), (x+w, y+h), (0, 255, 0), 2) cv.imshow("collect", frame) if cv.waitKey(1) == 27: break cap.release() cv.destroyAllWindows()几个实操细节:resize到 128×128 不是为了好看,是让所有样本的 LBP 直方图维度一致,LBPH 内部会把图分成 8×8 个网格,每个网格独立统计直方图,尺寸不一致会导致网格覆盖的物理区域不同,特征对不上。采集时加一个小延迟,比如每 100 毫秒存一张,避免连续帧高度相似导致样本冗余。
4.2 LBPH 训练与阈值标定,别信默认值
LBPH 的全称是局部二值模式直方图,原理不复杂:对每个像素,拿它和周围 8 个邻居比大小,比它大的记 1、小的记 0,得到一个 8 位二进制数,也就是 0~255 的一个值。这个值反映的是局部纹理,而不是绝对灰度,所以光照整体变亮变暗,只要相对关系不变,LBP 值就不变。这是它比 Eigenfaces 稳的根本原因。
训练代码:
import cv2 as cv import numpy as np from PIL import Image import os def train(dataset_dir="dataset", model_path="model.yml"): recognizer = cv.face.LBPHFaceRecognizer_create( radius=1, neighbors=8, grid_x=8, grid_y=8 ) faces, labels = [], [] for uid in os.listdir(dataset_dir): person_dir = os.path.join(dataset_dir, uid) if not os.path.isdir(person_dir): continue for fname in os.listdir(person_dir): img = cv.imread(os.path.join(person_dir, fname), cv.IMREAD_GRAYSCALE) if img is None: continue faces.append(cv.resize(img, (128, 128))) labels.append(int(uid)) recognizer.train(faces, np.array(labels)) recognizer.save(model_path) return recognizer三个参数的含义:radius是 LBP 算子的半径,1 表示取紧邻的 8 个像素,2 表示取隔一圈的像素,半径越大捕捉的纹理尺度越大,但细节丢失也越多;neighbors是采样点数量,8 是标准配置,改成 16 计算量翻倍,精度提升有限;grid_x和grid_y是把人脸图切成 8×8 网格,每个网格单独算直方图,最后拼起来。这个网格的作用是保留空间信息——如果整张脸只算一个直方图,那眼睛和嘴巴的纹理会被混在一起,换个人只要纹理总量接近就可能误判。
网格数怎么选:128×128 的输入,8×8 网格意味着每个格子 16×16 像素,这个粒度能大致对应到眼睛、鼻子、嘴巴这些区域。如果输入是 64×64,网格还设 8×8,每个格子只有 8×8 像素,统计量太少,直方图噪声大。所以输入尺寸和网格数是绑定的,改一个就要重新考虑另一个。
训练完最重要的是标定阈值。LBPH 的predict()返回两个值:标签和confidence。这个名字有严重的误导性,它其实是一个距离值,越小表示越像,取值范围大概 0 到 200 多。很多人看到confidence=20以为是 20% 的置信度,觉得很低,其实这个意思是"非常像"。反过来confidence=120才表示"很不像"。
阈值标定方法:拿一批同一个人的正样本和不同人的负样本,分别记录预测出的距离值,找到两条分布曲线的交叉点附近。实际测下来,同一个人普遍落在 30~70,不同人普遍落在 80~120,所以阈值设为 70~75 是一个比较稳的分界。但这个值高度依赖你的采集质量和人数,人数越多,不同人之间的区分度越难,阈值可能要往下压。
label, distance = recognizer.predict(gray_face) if distance < 70: name = id_to_name.get(label, "unknown") else: name = "unknown"这里的判断逻辑一定要保留unknown分支。我见过不少 Demo 直接取距离最小的标签就返回,结果来一个陌生人,系统也能给个名字,只是距离大一点,这种在实际使用中是很尴尬的。
5. 常见问题与排查技巧实录
5.1 检测框抖动、漏检和误检的三类根因
框抖动:视频里人脸没动,框却在几个像素范围内跳。这是 Haar 逐帧独立检测的必然结果,相邻两帧的候选框位置有细微差异。解决办法有两种,简单的是做框的平滑,用指数移动平均:new_box = 0.7 * old_box + 0.3 * current_box,延迟大概两三帧,肉眼基本无感。另一种是加跟踪器,检测只做一次,后面用cv.TrackerKCF这类跟踪算法跟住,定期重新检测校正。后者的代码复杂度高一些,但帧率能提升不少,因为跟踪比检测快一个数量级。
漏检:先看光照,把equalizeHist加上;再看minNeighbors,从 5 降到 3 试试;再看minSize,是不是设太大了;最后考虑换haarcascade_frontalface_alt2.xml,这个模型对正面轻度偏转的容忍度更好。如果这些都试过还是漏,那大概率是脸太偏或者遮挡太重,只能换 DNN。
误检:墙上挂的钟表、灯泡、圆形图案被认成脸,这是 Haar 的老毛病,因为它的特征本质上是矩形区域的明暗对比,而人脸的"亮额头、暗眼窝、亮鼻梁、暗嘴缝"这个模式在别的物体上也可能出现。处理办法是把minNeighbors提到 6~8,同时给minSize设一个下限。如果误检还是顽固,可以考虑加一道验证:把检测出的区域送进识别器,看距离值是否小于阈值,小于才认为是真脸。这就是"检测加识别联合判定",代价是每帧多一次推理。
5.2 识别总是返回同一个标签,问题多半在数据分布
这个现象我遇到过两次,两次原因完全不同。
第一次是训练数据的标签编码错了。我用文件夹名当标签,但某个文件夹名叫01,int("01")得到 1,而另一个文件夹叫1,也是 1,两个不同的人被系统认为是同一个人,训练时标签冲突,模型就崩了。解决办法是标签统一用不重复的整数,并且维护一张id → name的映射表,别指望从文件夹名反推。
第二次是样本数量严重不均衡。A 有 80 张,B 只有 12 张,LBPH 在统计时会被 A 的样本主导,导致 B 的预测总是偏向 A。这个不是算法 bug,是数据问题,只能补齐样本。一般要求每个人样本数量差距不超过 2 倍。
还有一种情况是输入预处理不一致。训练时用的是equalizeHist后的灰度图,推理时忘了做均衡化,特征分布对不上,表现就是所有人距离值都偏高,系统被迫在"都很差"的选项里挑一个最好的,结果就是乱认。训练和推理的预处理必须严格一致,这一条要写进代码注释里。
5.3 一份可以贴在显示器边上的速查表
| 现象 | 最可能的原因 | 优先尝试的修复 |
|---|---|---|
| 检测不到脸 | 光照太暗/太亮 | 加equalizeHist |
| 检测不到侧脸 | Haar 正脸模型限制 | 换 DNN 方案 |
| 框太多 | minNeighbors太小 | 提到 6~8 |
| 远处小脸检测不到 | minSize太大 | 降到 40~50 |
| 检测很慢 | scaleFactor太小 | 从 1.1 调到 1.2 |
| 识别全返回同一人 | 标签冲突或样本不均 | 检查标签唯一性、补齐样本 |
| 所有人都是 unknown | 阈值定太低 | 阈值从 70 调到 90 试 |
| 陌生人被认成熟人 | 没有 unknown 分支 | 加距离判断 |
| 换台机器结果不同 | OpenCV 版本不一致 | 锁定同一小版本 |
| 识别速度慢 | 每帧都全图检测 | 检测+跟踪交替 |
6. 工程化落地时值得提前想清楚的事
6.1 帧率优化:检测和跟踪交替跑
一个很朴素但效果明显的优化思路:不是每一帧都需要做检测。人脸在连续帧之间的位移很小,完全可以第 1 帧做检测,第 2 到第 5 帧用跟踪器跟住,第 6 帧再重新检测校正。这样检测的调用频率降到原来的五分之一,整体帧率能提升两三倍。
tracker = None frame_idx = 0 while True: ok, frame = cap.read() if not ok: break if frame_idx % 5 == 0 or tracker is None: boxes = detect_dnn(frame) if boxes: x, y, w, h, _ = boxes[0] tracker = cv.TrackerKCF_create() tracker.init(frame, (x, y, w, h)) else: ok_track, box = tracker.update(frame) if not ok_track: tracker = None frame_idx += 1注意跟踪器会漂移,所以定期重检测是必须的,五帧一次是我实测比较平衡的值。如果人脸移动很快,改成三帧一次;如果基本静止,十帧一次也行。另外TrackerKCF_create在 OpenCV 4.5 之后的版本里接口有调整,有的版本要用cv.legacy.TrackerKCF_create,这个要按你装的版本试一下。
6.2 一套能长期维护的目录结构
Demo 阶段可以所有代码塞一个文件,但只要这个功能要活下去,目录结构就得从第一天开始理清。我用的结构大概是这样:
face_project/ ├── models/ # 检测模型、识别模型 ├── dataset/ # 按 id 分文件夹的原始样本 ├── config.yaml # 阈值、路径、摄像头参数 ├── detect.py # 检测封装 ├── recognize.py # 识别封装 ├── collect.py # 采集脚本 ├── train.py # 训练脚本 └── app.py # 主流程config.yaml这一步很多人省掉,结果阈值散落在各个文件里,改一个值要全局搜索。把scale_factor、min_neighbors、conf_threshold、distance_threshold这些全抽出来,调参的时候只改一个文件,效率完全不一样。而且换场景部署的时候,只要换一份配置文件,代码一行不用动。
还有一点是模型的版本管理。LBPH 训练出来的model.yml是二进制文件,每次重新训练都会变。建议在文件名里带上时间戳,比如model_20240612.yml,同时在配置里记录当前用的哪个。这样某天发现识别变差了,可以快速回滚到上一个版本对比,而不是一脸茫然地重新采数据重训。
6.3 关于隐私和数据的几句实话
人脸数据是生物特征数据,和密码不一样,密码泄露了能改,脸改不了。所以哪怕只是内部 Demo,也建议遵守几条基本纪律:采集前明确告知并取得同意;数据只存本地,不上传第三方;用完即删,不要留一堆不知道谁的样本在硬盘里;识别结果只用于当次业务判断,不做跨场景关联。这些不是形式主义,是这类功能能不能长期做下去的前提。
另外一个技术层面的建议:如果场景允许,优先做人脸验证而不是人脸识别。验证是一对一,判断"这张脸是不是张三",阈值好定、误判率低;识别是一对多,从 N 个人里挑一个,人数一多准确率就掉。很多实际需求其实用验证就能满足,没必要上识别。
我在实际项目里最深的一个体会是,人脸这块的坑几乎全在数据和预处理上,算法本身反倒是最省心的部分。同一套代码,采集规范、预处理一致,识别率能到 90% 以上;采集随便糊弄、训练和推理预处理不统一,掉到 50% 都不奇怪。所以每次有人问我"识别不准怎么办",我第一句总是先问:你训练用的图和推理时的图,是不是同一套流程处理出来的。这个问题排掉之后,剩下的才是调参的事。