侦测是「这张脸在哪」,识别是「这张脸是谁」。技术上是两套东西,别混着说。 Haar 级联用一堆黑白条纹模板层层筛,快,但对侧脸和遮挡很弱。 它能给你一个框,不能给你一个身份。 人脸识别不比像素,比特征:一张脸压成 128 个数,距离小于 0.6 就认作同一个人。
上一篇的模糊、边缘、轮廓是「通用视觉」。这一篇专门对付脸。
教材第 14 章最后三节是【安装 face_recognition 程序库与 dlib 工具程序】,
还有【人脸侦测】和【人脸识别程序】。
侦测部分全真跑:Haar 级联是本机 OpenCV 自带的,检测、调参、换猫脸模型都是实测。
识别那一层是复刻:face_recognition 要编译 dlib,本沙箱装不上,
所以「128 维特征」用像素距离演示原理,真机代码走平台原样标注。
一、侦测和识别是两件事
| 侦测(detection) | 识别(recognition) | |
|---|---|---|
| 回答什么 | 这张脸在哪 | 这张脸是谁 |
| 输出 | 一个方框的坐标 | 一个身份,或一串特征向量 |
| 难在哪 | 侧脸、遮挡、光线 | 同一个人换角度也要认成同一个 |
| 本课用什么 | Haar 级联 / OpenCV | 特征比对 / face_recognition |
这两件事经常被混着说。 「人脸识别门禁」其实是先侦测再识别,两步。 顺序不能反:先找到脸,才能谈「是谁」。 框都没框出来,比什么都没意义。 分开还有一个好处:侦测算力小、识别算力大。 可以先在所有帧上做侦测,只在有人脸的那几帧做识别,省很多。
二、真检测一张脸
演示第二段加载 haarcascade_frontalface_default.xml,检测到 1 张脸:左上角 (177, 66),
宽高 95×95,占全图面积 3.4%。
Haar 级联是老技术,思路是「用一堆简单的黑白条纹模板一层层筛」:先粗筛掉明显不是脸的区域,
再一层层细看。
快,但对侧脸和遮挡很弱。演示里 1.1, 5 这组参数只检出一张,就是它保守的表现。
现在主流是深度学习的人脸检测(YuNet、RetinaFace),精度高得多。 但 Haar 有两个好处:文件只有几百 KB,而且不需要显卡。 所以它几十年了还留着,当入门的第一个例子,也当嵌入式设备的兜底方案。
三、参数一动,结果就变
| scaleFactor | minNeighbors | 检出 | 框大小 |
|---|---|---|---|
| 1.10 | 5 | 1 张 | 95×95 |
| 1.05 | 3 | 3 张 | 127×127、97×97 |
| 1.30 | 5 | 1 张 | 95×95 |
| 1.10 | 8 | 1 张 | 95×95 |
scaleFactor 是每次把检测窗口放大多少:1.1 是每次大 10%。数越小搜得越细、越慢、越容易误检。
minNeighbors 是「要有几个邻居同意才算脸」:数越大越严,漏检的风险越高。
演示里 1.05, 3 那组检出 3 张,多出来的两张是误检。从 1 张到 3 张,只是把阈值放松了一点点。
这两个参数没有最优值,得拿你自己的图去试。 而且换个人、换个光线,结论可能就变了——这也是传统视觉方法最烦人的地方。
四、换个模型就换一种脸
演示第四段拿猫的图试两种模型:猫脸模型检出 1 张((270, 208),64×64),人脸模型检出 0 张。
模型是「专门认某一种东西」的。 猫脸模型认不出人脸,人脸模型也认不出猫脸。
所以「AI 识别」并不通用:换一种目标,就得换一份训练好的模型。
真实项目里第一件事是问「有没有现成的模型」,而不是自己从头训。
训练一个能用的检测模型,要几万张标注图、几十小时显卡时间,还要调参。 而现成的模型常常是开源的、几百 KB、直接用。 当然也有例外:目标特别偏(比如某种工业零件、某种罕见病虫害),没有现成模型, 那就得自己训——但那是另一个话题了。
五、是同一个人吗:比特征
演示第五段拿同一张脸做三种变化比像素距离:原样 0.0、整体调亮 25 之后 25.0、加噪声之后 10.6。 像素直接比是很脆的:光一变,距离就上去了。 而「调亮」在现实里天天发生——上午一张、下午一张, 距离就够把同一个人判成两个人。 所以真正的人脸识别不比像素,比「特征」:把脸压成一串数字,再看这串数字离得多近。
教材用的 face_recognition 是把一张脸压成 128 个数。
同一个人的两张照片,这 128 个数的距离通常小于 0.6;不同人大多大于 0.6。
那个 0.6 就是判断「是不是同一个人」的那条线。调低更严(容易把本人拒之门外),调高更容易认错。
这条线定在哪,是安全性和便利性的取舍,不是技术问题。
六、真机上长这样
演示第六段是平台原样:face_recognition 要编译 dlib,本沙箱装不上,所以代码只摆形状,不执行。
face_locations 就是侦测,face_encodings 就是识别。
两行代码对应两件事,上一篇那个「先侦测再识别」在这儿是字面意义上的两行。
encodings 一定要把 locations 传进去,否则它会自己再算一遍,白花时间。
这个坑很常见,因为不传也能跑,只是慢。
这个库在 Windows 上装起来最麻烦——它依赖 dlib,而 dlib 要 C++ 编译环境。
装不上别硬扛,用 OpenCV 的 FaceDetectorYN 也能做侦测,只是没有现成的「128 维特征」那一层。
最后一句得说清楚:人脸是敏感个人信息。 做门禁、考勤这类应用,采集前要告知、要取得同意,
存下来的特征也要当敏感数据保护。这不是技术问题,但做技术的人躲不开。
七、动手 Demo:人脸侦测与识别不是一回事
先用 Haar 级联真检出一张脸,再换四组参数看结果怎么变。 然后拿猫图试两种模型,最后用像素距离演示「为什么不能直接比像素」。
import os
import cv2
import numpy as np
from PIL import Image
IMG_DIR = 'img' # 放 astronaut.png / coffee.png / chelsea.png
def load(name):
return np.array(Image.open(os.path.join(IMG_DIR, name)).convert('RGB'))
def gray_of(rgb):
return cv2.cvtColor(cv2.cvtColor(rgb, cv2.COLOR_RGB2BGR), cv2.COLOR_BGR2GRAY)
print(' ① 先把两件事分清楚')
print(' ' + '-' * 68)
print()
print('| | 人脸侦测(detection) | 人脸识别(recognition) |')
print('|---|---|---|')
for r in (('回答什么', '这张脸在哪', '这张脸是谁'),
('输出', '一个方框的坐标', '一个身份,或者一串特征向量'),
('难在哪', '侧脸、遮挡、光线', '同一个人不同角度要认成同一个'),
('本课用什么', 'Haar 级联 / OpenCV', '特征比对 / face_recognition')):
print('| %s | %s | %s |' % r)
print()
print(' 这两件事经常被混着说,但技术上是两套东西。先找到脸,才能谈「是谁」。')
print()
print(' ② 真检测一张脸')
print(' ' + '-' * 68)
arr = load('astronaut.png')
gray = gray_of(arr)
cas_path = os.path.join(cv2.data.haarcascades, 'haarcascade_frontalface_default.xml')
cas = cv2.CascadeClassifier(cas_path)
print(' 级联文件:%s' % os.path.basename(cas_path))
print(' 加载成功?%r(empty() 为 False 就是成功)' % (not cas.empty()))
faces = cas.detectMultiScale(gray, 1.1, 5)
print(' 检测到 %d 张脸' % len(faces))
for x, y, w, h in faces:
print(' 左上角 (%d, %d),宽高 %d×%d' % (x, y, w, h))
print(' 占全图面积 %.1f%%' % (w * h * 100.0 / (arr.shape[0] * arr.shape[1])))
print(' Haar 级联是老技术,思路是「用一堆简单的黑白条纹模板一层层筛」。快,但对侧脸和遮挡很弱。')
print(' 现在主流是深度学习的人脸检测(YuNet、RetinaFace),精度高得多。')
print(' 但 Haar 有个好处:文件只有几百 KB,不需要显卡,几十年了还留着当入门的第一个例子。')
print()
print(' ③ 参数一动,结果就变')
print(' ' + '-' * 68)
print()
print('| scaleFactor | minNeighbors | 检出 | 框大小 |')
print('|---:|---:|---|---|')
for sf, mn in ((1.1, 5), (1.05, 3), (1.3, 5), (1.1, 8)):
got = cas.detectMultiScale(gray, sf, mn)
boxes = '、'.join('%d×%d' % (w, h) for _, _, w, h in got[:2]) or '—'
print('| %.2f | %d | %d 张 | %s |' % (sf, mn, len(got), boxes))
print()
print(' scaleFactor 是每次把检测窗口放大多少:1.1 是每次大 10%,数越小搜得越细、越慢、越容易误检。')
print(' minNeighbors 是「要有几个邻居同意才算脸」:数越大越严,漏检的风险越高。')
print(' 这两个参数没有最优值,得拿你自己的图去试。')
print()
print(' ④ 换个模型,就换一种脸')
print(' ' + '-' * 68)
cgray = gray_of(load('chelsea.png'))
ccas = cv2.CascadeClassifier(os.path.join(cv2.data.haarcascades,
'haarcascade_frontalcatface.xml'))
catfaces = ccas.detectMultiScale(cgray, 1.05, 3)
print(' 猫脸模型对这张猫图:%d 张' % len(catfaces))
print(' 人脸模型对同一张图:%d 张' % len(cas.detectMultiScale(cgray, 1.1, 5)))
for x, y, w, h in catfaces:
print(' 猫脸在 (%d, %d),%d×%d' % (x, y, w, h))
print(' 模型是「专门认某一种东西」的。')
print(' 猫脸模型认不出人脸,人脸模型也认不出猫脸——虽然偶尔会认错,那叫误检。')
print(' 这也说明「AI 识别」并不通用:换一种目标,就得换一份训练好的模型。')
print(' 所以真实项目里第一件事是问「有没有现成的模型」,而不是自己从头训。')
print()
print(' ⑤ 「是同一个人吗」:比特征')
print(' ' + '-' * 68)
x, y, w, h = faces[0]
face = cv2.resize(gray[y:y + h, x:x + w], (100, 100))
brighter = np.clip(face.astype(int) + 25, 0, 255).astype(np.uint8)
noisy = np.clip(face.astype(int) + np.random.RandomState(7).randint(-18, 19, face.shape),
0, 255).astype(np.uint8)
def dist(a, b):
return float(np.sqrt(((a.astype(float) - b.astype(float)) ** 2).mean()))
print(' 自己和自己比 距离 %.1f' % dist(face, face.astype(float)))
print(' 整体调亮 25 之后 距离 %.1f' % dist(face, brighter))
print(' 加噪声之后 距离 %.1f' % dist(face, noisy))
print(' 像素直接比是很脆的:光一变,距离就上去了。')
print(' 所以真正的人脸识别不比像素,比「特征」——把脸压成一串数字,再看这串数字离得多近。')
print(' 教材用的 face_recognition 库,是把一张脸压成 128 个数。')
print(' 同一个人的两张照片,这 128 个数的距离通常小于 0.6;不同人大多大于 0.6。')
print(' 那个阈值就是判断「是不是同一个人」的那条线。')
print()
print(' ⑥ 真机上长这样')
print(' ' + '-' * 68)
SRC = """# 本书第 14-5、14-6 节(平台原样,本沙箱不执行:要装 dlib 与 face_recognition)
import face_recognition
img = face_recognition.load_image_file('a.jpg')
boxes = face_recognition.face_locations(img)
encs = face_recognition.face_encodings(img, boxes)
known = face_recognition.face_encodings(
face_recognition.load_image_file('known.jpg'))[0]
for e in encs:
print(face_recognition.compare_faces([known], e, tolerance=0.6))"""
print()
print(' [平台原样 · 本沙箱不执行]')
for ln in SRC.split('\n'):
print(' | ' + ln)
print(' tolerance 就是那条线,0.6 是官方给的常用值;调低更严,调高更容易认错')
print()
print(' face_locations 就是侦测,face_encodings 就是识别。')
print(' 注意 encodings 一定要把 locations 传进去,否则它会自己再算一遍,白花时间。')
print(' 这个库在 Windows 上装起来最麻烦——它依赖 dlib,而 dlib 要 C++ 编译环境。')
print(' 装不上的话,用 OpenCV 的 FaceDetectorYN 也能做侦测,只是没有现成的「128 维特征」那一层。')
实跑输出:
--------------------------------------------------------------------
人脸侦测与识别不是一回事
--------------------------------------------------------------------
① 先把两件事分清楚
--------------------------------------------------------------------
| | 人脸侦测(detection) | 人脸识别(recognition) |
|---|---|---|
| 回答什么 | 这张脸在哪 | 这张脸是谁 |
| 输出 | 一个方框的坐标 | 一个身份,或者一串特征向量 |
| 难在哪 | 侧脸、遮挡、光线 | 同一个人不同角度要认成同一个 |
| 本课用什么 | Haar 级联 / OpenCV | 特征比对 / face_recognition |
这两件事经常被混着说,但技术上是两套东西。先找到脸,才能谈「是谁」。
② 真检测一张脸
--------------------------------------------------------------------
级联文件:haarcascade_frontalface_default.xml
加载成功?True(empty() 为 False 就是成功)
检测到 1 张脸
左上角 (177, 66),宽高 95×95
占全图面积 3.4%
Haar 级联是老技术,思路是「用一堆简单的黑白条纹模板一层层筛」。快,但对侧脸和遮挡很弱。
现在主流是深度学习的人脸检测(YuNet、RetinaFace),精度高得多。
但 Haar 有个好处:文件只有几百 KB,不需要显卡,几十年了还留着当入门的第一个例子。
③ 参数一动,结果就变
--------------------------------------------------------------------
| scaleFactor | minNeighbors | 检出 | 框大小 |
|---:|---:|---|---|
| 1.10 | 5 | 1 张 | 95×95 |
| 1.05 | 3 | 3 张 | 127×127、97×97 |
| 1.30 | 5 | 1 张 | 95×95 |
| 1.10 | 8 | 1 张 | 95×95 |
scaleFactor 是每次把检测窗口放大多少:1.1 是每次大 10%,数越小搜得越细、越慢、越容易误检。
minNeighbors 是「要有几个邻居同意才算脸」:数越大越严,漏检的风险越高。
这两个参数没有最优值,得拿你自己的图去试。
④ 换个模型,就换一种脸
--------------------------------------------------------------------
猫脸模型对这张猫图:1 张
人脸模型对同一张图:0 张
猫脸在 (270, 208),64×64
模型是「专门认某一种东西」的。
猫脸模型认不出人脸,人脸模型也认不出猫脸——虽然偶尔会认错,那叫误检。
这也说明「AI 识别」并不通用:换一种目标,就得换一份训练好的模型。
所以真实项目里第一件事是问「有没有现成的模型」,而不是自己从头训。
⑤ 「是同一个人吗」:比特征
--------------------------------------------------------------------
自己和自己比 距离 0.0
整体调亮 25 之后 距离 25.0
加噪声之后 距离 10.6
像素直接比是很脆的:光一变,距离就上去了。
所以真正的人脸识别不比像素,比「特征」——把脸压成一串数字,再看这串数字离得多近。
教材用的 face_recognition 库,是把一张脸压成 128 个数。
同一个人的两张照片,这 128 个数的距离通常小于 0.6;不同人大多大于 0.6。
那个阈值就是判断「是不是同一个人」的那条线。
⑥ 真机上长这样
--------------------------------------------------------------------
[平台原样 · 本沙箱不执行]
| # 本书第 14-5、14-6 节(平台原样,本沙箱不执行:要装 dlib 与 face_recognition)
| import face_recognition
| img = face_recognition.load_image_file('a.jpg')
| boxes = face_recognition.face_locations(img)
| encs = face_recognition.face_encodings(img, boxes)
| known = face_recognition.face_encodings(
| face_recognition.load_image_file('known.jpg'))[0]
| for e in encs:
| print(face_recognition.compare_faces([known], e, tolerance=0.6))
tolerance 就是那条线,0.6 是官方给的常用值;调低更严,调高更容易认错
face_locations 就是侦测,face_encodings 就是识别。
注意 encodings 一定要把 locations 传进去,否则它会自己再算一遍,白花时间。
这个库在 Windows 上装起来最麻烦——它依赖 dlib,而 dlib 要 C++ 编译环境。
装不上的话,用 OpenCV 的 FaceDetectorYN 也能做侦测,只是没有现成的「128 维特征」那一层。
第一段那张对照表是全篇的骨架。
后面五段都在补它:第二三段讲侦测,第四段讲模型专用,第五段讲识别,第六段给真机代码。
第三段那张参数表最值得盯:1.05, 3 检出 3 张,其余都是 1 张。
多出来的两张就是误检——放松一点点,误检就来了。
第四段猫脸模型检出 1 张、人脸模型 0 张,这个对比很有说服力:模型是专用的,不是万能的。
第五段那个「调亮 25 之后距离 25.0」是关键。光变一点,像素距离就跳上去;
而真实的两个人,距离可能比这小得多。所以像素比对根本不能用。
第六段那个 [平台原样 · 本沙箱不执行] 块是真机代码。
它跑不了,但形状是真的——照着抄到装了 dlib 的机器上就能用。
30 秒实验
- 把演示第二段的
1.1, 5改成1.01, 2,看检出几张,顺便感觉一下跑得慢了多少。 - 把演示第五段的
+25改成+50,看距离怎么变——是不是差不多翻倍。 - 拿你自己的一张正脸照片替换
img/astronaut.png,看 Haar 能不能检出; 再换一张侧脸,看它是不是就检不到了。
常见误区
- 「能框出脸在哪,就等于能认出是谁。 」侦测和识别是两套技术。 Haar 给你一个方框,它不知道框里是谁。
- 「Haar 太老了,没用。」它几百 KB、不要显卡、能跑在单片机上。 入门和嵌入式场景还在用,只是别指望它对付侧脸和遮挡。
- 「识别不准就自己训一个模型。」先找现成的。 训练一个能用的检测模型要几万张标注图,而开源模型常常几百 KB 直接能用。
自测三问(点开对答案)
1. 人脸侦测和人脸识别,区别到底在哪? —— 侦测回答「这张脸在哪」,输出是方框坐标;识别回答「这张脸是谁」,输出是身份或特征向量。 顺序上必须先侦测再识别——框都没框出来,比什么都没意义。两者的算法、算力、失败方式都不一样。 2. 为什么人脸识别不能直接比像素? —— 因为像素对光和噪声太敏感。演示里同一张脸调亮 25,距离就从 0 跳到 25。 现实里上午下午光线不同很常见,直接比像素会把同一个人判成两个人。 所以要先把脸压成特征向量(比如 128 个数),再比这串数的距离。 3. `scaleFactor` 和 `minNeighbors` 各管什么?调小了/调大了会怎样? —— `scaleFactor` 是每次把检测窗口放大多少,调小搜得更细、更慢、更容易误检; `minNeighbors` 是「要有几个邻居同意才算脸」,调大更严、更容易漏检。 演示里 1.05/3 那组检出 3 张,多出来的就是误检。**没有通用最优值,得拿自己的图试。**第 14 章结束,正文到此为止。下一篇进附录 A:列表生成式、装饰器和递归。
- ☐ (本篇完,下一篇 A.1 列表生成式与装饰器)