🐍 第14章 影像处理与人脸辨识
📄 一句话概括

侦测是「这张脸在哪」,识别是「这张脸是谁」。技术上是两套东西,别混着说。 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.1, 5 改成 1.01, 2,看检出几张,顺便感觉一下跑得慢了多少。
  2. 把演示第五段的 +25 改成 +50,看距离怎么变——是不是差不多翻倍。
  3. 拿你自己的一张正脸照片替换 img/astronaut.png,看 Haar 能不能检出; 再换一张侧脸,看它是不是就检不到了。

常见误区

  1. 「能框出脸在哪,就等于能认出是谁。 」侦测和识别是两套技术。 Haar 给你一个方框,它不知道框里是谁。
  2. 「Haar 太老了,没用。」它几百 KB、不要显卡、能跑在单片机上。 入门和嵌入式场景还在用,只是别指望它对付侧脸和遮挡。
  3. 「识别不准就自己训一个模型。」先找现成的。 训练一个能用的检测模型要几万张标注图,而开源模型常常几百 KB 直接能用。
自测三问(点开对答案) 1. 人脸侦测和人脸识别,区别到底在哪? —— 侦测回答「这张脸在哪」,输出是方框坐标;识别回答「这张脸是谁」,输出是身份或特征向量。 顺序上必须先侦测再识别——框都没框出来,比什么都没意义。两者的算法、算力、失败方式都不一样。 2. 为什么人脸识别不能直接比像素? —— 因为像素对光和噪声太敏感。演示里同一张脸调亮 25,距离就从 0 跳到 25。 现实里上午下午光线不同很常见,直接比像素会把同一个人判成两个人。 所以要先把脸压成特征向量(比如 128 个数),再比这串数的距离。 3. `scaleFactor` 和 `minNeighbors` 各管什么?调小了/调大了会怎样? —— `scaleFactor` 是每次把检测窗口放大多少,调小搜得更细、更慢、更容易误检; `minNeighbors` 是「要有几个邻居同意才算脸」,调大更严、更容易漏检。 演示里 1.05/3 那组检出 3 张,多出来的就是误检。**没有通用最优值,得拿自己的图试。**

第 14 章结束,正文到此为止。下一篇进附录 A:列表生成式、装饰器和递归。

相关笔记