OpenCV 学习实践:从人脸检测到人脸识别

最近学习了 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 实践,我对人脸检测和人脸识别有了更清晰的理解。

人脸检测主要负责定位,结果是图像中的矩形框;微笑检测是在脸部区域中寻找符合笑容特征的局部区域;人脸识别则需要利用带标签的训练图片,判断输入人脸与哪个类别更加相似。

整个项目的基本流程可以总结为:

读取图像,灰度化处理,加载分类器,检测人脸,提取人脸区域,训练识别模型,预测测试图像,

显示识别结果

相关推荐
财复视界1 小时前
光智科技磷化铟第二曲线:从红外感知到光通信链接
人工智能
beiju1 小时前
为什么4个并行Agent,不该一起改同一个文件
人工智能
dh2711987791 小时前
AI幻觉与信任赤字:南京GEO服务商如何帮企业重建AI时代的品牌信用
大数据·人工智能
YOLO数据集集合1 小时前
遥感影像树木检测数据集 | 遥感树木 目标检测 城市绿化 森林监测 YOLO格式9052期
人工智能·yolo·目标检测·计算机视觉·目标跟踪·树木检测·遥感树影
YonyouHRSaaS1 小时前
2026年AI招聘系统怎么选?AI面试功能怎么评估?
人工智能·面试·职场和发展·求职招聘·ai面试
会编程的吕洞宾1 小时前
LangChain4j RAG 分块策略实战:检索质量提升 80% 的 Chunking 调优全解
java·人工智能·后端
月华路1 小时前
《模型不玄学》第21章 稳定性与偏置审计
人工智能·深度学习·机器学习
用户5274675614211 小时前
Agent 提交 PR 后别急着下班:把“可合并”做成一等状态
人工智能
小阿鑫1 小时前
AI 越来越强,为什么打工人反而越来越累、越来越内耗了?
人工智能·ai·程序员·职业发展·agi·工作效率