最近学习了 OpenCV 的一些基础内容,并尝试完成了几个和人脸相关的小项目,包括人脸检测、微笑检测以及人脸识别。虽然这些项目的代码量并不算特别大,但其中包含了计算机视觉中比较典型的处理流程:读取图像、转换颜色空间、加载分类器、检测目标、提取区域、训练模型、进行预测以及显示结果。
通过这次实践,我对 OpenCV 的使用方式有了更加直观的认识,也进一步理解了"检测"和"识别"之间的区别。
一、OpenCV 可以做什么?
OpenCV 是一个开源的计算机视觉库,提供了大量图像处理和视频分析功能。它不仅可以读取、保存和显示图片,还可以完成边缘检测、目标识别、摄像头采集、图像变换以及机器学习等任务。
在实际应用中,OpenCV 常见的使用场景包括:人脸识别,车辆检测,表情识别等
这次学习主要围绕人脸相关功能展开。项目使用了几张人脸图片、摄像头视频,以及 OpenCV 自带的 Haar 级联分类器。
二、人脸检测的基本思路
人脸检测和人脸识别虽然经常同时出现,但它们解决的问题并不一样。
人脸检测的任务是判断一张图片中是否存在人脸,并且找出人脸所在的位置。例如,输入一张包含多个人的照片,检测程序会在每个人脸周围画出一个矩形框。
人脸识别则是在检测到人脸之后,进一步判断这张脸属于谁。例如,训练数据中有两个人的照片,程序可以判断测试图片更接近哪一个人。
所以,人脸检测的结果通常是矩形框,而人脸识别的结果通常是一个标签或姓名。
OpenCV 中常见的人脸检测方法是 Haar 级联分类器。它通过预先训练好的 XML 文件,对图像中的不同区域进行扫描,从而寻找符合人脸特征的区域。
加载分类器的代码如下:
import cv2
faceCascade = cv2.CascadeClassifier(
"haarcascade_frontalface_default.xml"
)
这里的 XML 文件中保存了训练好的分类器参数。使用时需要保证文件路径正确,否则分类器无法加载。
三、从摄像头读取视频
使用 OpenCV 进行实时人脸检测时,首先需要打开摄像头:
cap = cv2.VideoCapture(0)
其中,参数 0 通常表示电脑的默认摄像头。如果电脑连接了多个摄像头,也可以尝试使用 1、2 等编号。
摄像头打开之后,可以通过循环不断读取视频帧:
while True:
ret, image = cap.read()
if not ret:
break
cv2.imshow("camera", image)
key = cv2.waitKey(1)
if key == 27:
break
cap.read() 会返回两个结果。第一个是 ret,用来表示是否读取成功;第二个是当前读取到的图像帧。如果读取失败,就应该退出循环。
cv2.imshow() 用于显示图像,cv2.waitKey() 用于等待键盘输入,同时也可以让窗口及时刷新。代码中设置按下 Esc 键退出程序。
在摄像头画面中,通常还会使用水平翻转:
image = cv2.flip(image, 1)
这样显示出来的画面和我们照镜子时的感觉比较接近,操作起来更加自然。
程序结束时,需要释放摄像头并关闭窗口:
cap.release()
cv2.destroyAllWindows()
如果忘记释放摄像头,设备可能会一直被程序占用,导致下一次运行时无法正常打开。
四、灰度化处理
原始摄像头图像一般是彩色图像,使用 BGR 三个通道表示。为了降低计算量,人脸检测之前通常会先将图像转换为灰度图像:
gray = cv2.cvtColor(
image,
cv2.COLOR_BGR2GRAY
)
灰度图像只有一个通道,每个像素只表示亮度信息。由于 Haar 分类器主要依赖亮暗变化和局部纹理,所以灰度图像已经可以满足检测需要。
灰度化的优点主要有两个。
第一,图像数据量更小,计算速度更快。第二,可以减少颜色信息带来的干扰,让分类器重点关注轮廓、边缘和明暗变化。
五、使用 Haar 分类器检测人脸
调用分类器进行检测时,常用的方法是 detectMultiScale():
faces = faceCascade.detectMultiScale(
gray,
scaleFactor=1.1,
minNeighbors=10,
minSize=(5, 5)
)
这个函数会在不同大小的窗口中扫描图像,并返回检测到的人脸区域。
返回的每一项通常包含四个数:
x, y, w, h
其中:
1.x 表示矩形左上角的横坐标;
2.y 表示矩形左上角的纵坐标;
3.w 表示矩形的宽度;
4.h 表示矩形的高度。
得到这些坐标后,就可以在原图上画出矩形框:
for (x, y, w, h) in faces:
cv2.rectangle(
image,
(x, y),
(x + w, y + h),
(0, 255, 0),
2
)
这样运行程序时,摄像头画面中的人脸周围就会出现绿色方框。
六、检测参数的含义
detectMultiScale() 中有几个参数会明显影响检测效果。
scaleFactor 表示每次缩放搜索窗口时的比例。如果数值比较接近 1,扫描会更加细致,但速度可能变慢。如果数值比较大,检测速度会变快,但可能漏掉一些目标。
minNeighbors 表示一个候选区域需要得到多少次相邻检测结果支持。数值较小时,检测会更加宽松,容易找到人脸,但也可能出现误报。数值较大时,检测结果比较严格,误报减少,但某些角度或光线下的人脸可能被漏掉。
minSize 表示检测目标的最小尺寸。设置较大的值可以忽略远处的小目标,同时提高运行速度;设置太小则可能会增加计算量。
这些参数没有统一的最佳答案,需要结合具体环境进行调整。如果摄像头距离人脸较近,可以适当增大 minSize;如果画面中经常出现漏检,可以适当降低 minNeighbors。
七、微笑检测的实现
微笑检测是在已经检测到人脸的基础上进行的。首先加载人脸分类器和笑容分类器:
faceCascade = cv2.CascadeClassifier(
"haarcascade_frontalface_default.xml"
)
smile = cv2.CascadeClassifier(
"haarcascade_smile.xml"
)
检测到人脸之后,先截取人脸区域:
roi_gray_face = gray[y:y + h, x:x + w]
这里的 roi 表示感兴趣区域。因为笑容只可能出现在人脸区域内,所以没有必要在整张图片中进行检测。
然后在这块人脸区域中检测笑容:
smiles = smile.detectMultiScale(
roi_gray_face,
scaleFactor=1.5,
minNeighbors=9,
minSize=(50, 50)
)
如果检测到笑容,就在画面上标记出来:
for (sx, sy, sw, sh) in smiles:
a = x + sx
b = y + sy
cv2.rectangle(
image,
(a, b),
(a + sw, b + sh),
(255, 0, 0),
2
)
cv2.putText(
image,
"smile",
(x, y),
cv2.FONT_HERSHEY_COMPLEX_SMALL,
1,
(0, 255, 255),
2
)
这里需要注意,笑容检测返回的坐标是相对于人脸 ROI 的坐标,而不是整张图片的坐标。因此,需要把人脸左上角的坐标加回去,才能在原图中准确地显示检测结果。
八、微笑检测存在的问题
微笑检测看起来很有趣,但实际效果会受到很多因素影响。
例如,光线太暗时,嘴巴区域的纹理不明显;脸部转向侧面时,分类器可能无法找到完整的嘴巴;如果佩戴口罩、胡须较多或者嘴巴被遮挡,也可能造成误检。
此外,微笑分类器检测到的只是与笑容相似的局部特征,并不能真正理解人的情绪。因此,程序显示"smile"并不一定代表这个人真的感到开心,只能说明当前画面中出现了符合分类器特征的区域。
九、人脸识别实验
在人脸检测之后,可以继续进行人脸识别。实验中准备了不同人物的人脸图片,并为每组图片设置对应的标签。
例如:
images = []
labels = [0, 0, 1, 1]
这里的两个 0 表示属于第一个人的图片,两个 1 表示属于第二个人的图片。
为了让图片能够进行比较,通常需要将所有训练图片调整到相同尺寸:
def image_re(image):
a = cv2.imread(image, 0)
a = cv2.resize(a, (120, 180))
return a
图像尺寸统一后,就可以使用 OpenCV 提供的人脸识别器进行训练。
十、LBPH 人脸识别
LBPH 的全称是 Local Binary Patterns Histograms,也就是局部二值模式直方图。它主要分析人脸局部区域的纹理特征。
创建 LBPH 识别器:
recognizer = cv2.face.LBPHFaceRecognizer_create()
使用训练图片和标签训练模型:
recognizer.train(
images,
np.array(labels)
)
然后使用一张新的图片进行预测:
label, confidence = recognizer.predict(
predict_image
)
label 是预测得到的标签,confidence 是匹配距离。通常情况下,距离越小表示测试图片和训练图片越接近。
可以使用字典把数字标签转换为名称:
dic = {
0: "hg",
1: "pyy",
-1: "无法识别"
}
需要注意,confidence 并不是百分比,它更像是一种距离分数。因此不能简单理解为"置信度 80%"。在实际应用中,通常还要设置一个阈值。如果距离超过阈值,就认为当前人脸不属于训练数据中的任何一个人。
十一、EigenFace 人脸识别
EigenFace 的核心思想与 PCA 降维有关。它会把人脸图像看成高维数据,然后提取其中比较重要的主成分。
代码结构和 LBPH 比较相似:
recognizer = cv2.face.EigenFaceRecognizer_create()
recognizer.train(
images,
np.array(labels)
)
label, confidence = recognizer.predict(
pre_image
)
EigenFace 更关注整张脸的整体变化,例如脸型、光照和主要轮廓。它可以帮助我们理解特征降维的思想,但对光照、姿态和背景变化比较敏感。
如果训练图片数量太少,或者每张图片的姿态差异很大,识别结果可能不够稳定。
十二、FisherFace 人脸识别
FisherFace 使用的是线性判别分析思想。它不仅分析图像之间的差异,还会利用类别标签,尽可能寻找能够区分不同人物的特征方向。
创建识别器的方法如下:
recognizer = cv2.face.FisherFaceRecognizer_create()
训练和预测过程与其他识别器类似:
recognizer.train(
images,
np.array(labels)
)
label, confidence = recognizer.predict(
pre_image
)
FisherFace 更注重不同类别之间的区分能力,在训练样本较合适时可能取得比较好的分类效果。不过,它同样依赖训练数据。如果每个人只有一两张照片,模型很难真正学到这个人的稳定特征。
十三、三种方法的简单比较
LBPH 主要关注局部纹理,方法直观,使用方便,适合小规模实验。它对一定程度的光照变化有适应能力,也比较适合实时场景。
EigenFace 主要基于 PCA,通过提取主成分来表示人脸。它可以减少特征维度,但容易受到光照和姿态变化的影响。
FisherFace 使用类别信息进行判别,更关注不同人物之间的区分。它的思想比较适合分类问题,但对训练数据的质量和数量有一定要求。
在实际应用中,不能只根据一次测试结果判断哪种算法最好。应该准备更多训练图片和测试图片,记录每种方法的预测结果,再比较准确率和运行速度。
十四、如何改进这个项目?
目前的实验主要是为了理解算法流程,训练数据量比较少,代码结构也比较简单。如果要将它改造成更加稳定的项目,还可以进行很多改进。
首先,可以增加训练图片数量。每个人至少准备几十张不同角度、不同光照和不同表情的照片,模型的泛化能力会更好。
其次,可以先进行人脸检测,再裁剪出人脸区域进行识别。这样可以减少背景和其他物体对识别结果的影响。
再次,可以对输入图片进行直方图均衡化等预处理,增强图像对比度,降低光照变化的影响。
还可以把代码拆分成多个函数,例如:
def load_images():
pass
def train_model():
pass
def predict_face():
pass
def show_result():
pass
这样代码更加清晰,后续想更换 LBPH、EigenFace 或 FisherFace 时也更方便。
在实时检测时,还可以对连续多帧的识别结果进行统计。如果某个标签只出现一帧,就不立即显示;只有当多个连续帧都得到相同结果时,才认为识别结果比较稳定。
十五、传统方法与深度学习方法的对比
Haar、LBPH、EigenFace 和 FisherFace 都属于比较经典的传统方法。它们结构简单、运行速度快,非常适合入门学习。
但是,传统方法也有明显的局限。例如,当光照变化很大、脸部角度变化明显、人数较多或者背景复杂时,识别效果可能下降。
现在很多实际的人脸识别系统已经使用深度学习方法。深度神经网络可以自动学习更加复杂的人脸特征,例如眼睛、鼻子、嘴巴、脸部轮廓以及更深层次的结构关系。
不过,深度学习模型通常需要更多的数据、更强的计算资源和更加复杂的训练过程。因此,对于刚开始学习计算机视觉的人来说,先通过 OpenCV 传统方法理解图像读取、目标检测、特征提取和分类流程,仍然是很有帮助的。
十六、总结
通过这次 OpenCV 实践,我对人脸检测和人脸识别有了更清晰的理解。
人脸检测主要负责定位,结果是图像中的矩形框;微笑检测是在脸部区域中寻找符合笑容特征的局部区域;人脸识别则需要利用带标签的训练图片,判断输入人脸与哪个类别更加相似。
整个项目的基本流程可以总结为:
读取图像,灰度化处理,加载分类器,检测人脸,提取人脸区域,训练识别模型,预测测试图像,
显示识别结果