本教程专为初次接触 OpenCV 的国内计算机爱好者打造,采用通俗易懂的语言,配合详尽的代码示例,帮助您快速掌握图像基础操作及人脸检测实战项目。无论您是刚入门的新手,还是希望巩固基础的开发者,都能从中获得清晰的学习路径。
一、图像的基本操作
对于人类而言,图像可分解为画面结构、色彩与丰富的意象。您可以将它拆解为各种色块或线条,并用故事性的语言存储在大脑中。
而计算机处理图像的方式则更加机械——它只识别构成图像的每一个像素。为了存储这些像素,计算机需要像素的坐标和色彩信息。OpenCV 采用名为 Mat 的数据结构来存储图像,您可以将其理解为一种表格形式的数据结构,如下所示,表格的行列对应像素的分布。

在上图中,每个像素的颜色由三原色(蓝绿红,即 BGR)表示。这些 BGR 数据按顺序排列,每个颜色分量的取值范围为 0-255(8 位),因此每个像素拥有 8×3 位(即 24 位真彩色)。

若要创建一个存储 2×2 像素图像且每个像素为蓝色(0,0,255)的 Mat 数据 M,可编写如下代码。注意:目前最新的 OpenCV 4.0.1 已不再支持 C 语言,代码文件需采用 C++ 编写。
Mat M(2,2, CV_8UC3, Scalar(0,0,255));//新建2x2像素图像
imshow("image", M);//显示M的图像
当然,大多数情况下我们不会手动创建图像,而是直接从文件读取。下面介绍如何从 JPG 文件加载图像。例如,将名为 Lena 的图片存入 Mat 结构 M,并演示如何以灰度模式读取彩色图像。
Mat M = imread("lena.jpg");//图像来自图片文件
imshow("image", M);//显示M的图像
Mat img = imread("lena.jpg", IMREAD_GRAYSCALE);//以灰度形式读取图片文件
imshow("grayimage", img);//显示img的图像(它是灰度图)
反过来,我们也可以将 Mat 数据写入文件,从而实现图像的保存。例如,读取 Lena 照片后,将其存入名为 out 的文件:
imwrite("image.jpg", M);//显示M的图像
Mat img = imread("lena.jpg", IMREAD_GRAYSCALE);//以灰度形式读取图片文件
imwrite("grayimage.jpg", img);//显示img的图像(它是灰度图)
小提示:
imread()默认以彩色方式加载图像。如需灰度加载,请指定第二个参数IMREAD_GRAYSCALE。- 使用
imwrite()保存图像时,文件扩展名(如.jpg、.png)决定存储格式。
二、一个人脸检测的例子
本示例需要引用三个 OpenCV 头文件:
#include "opencv2/objdetect.hpp"
#include "opencv2/highgui.hpp"
#include "opencv2/imgproc.hpp"
也可以一次性引用全部 OpenCV 库:
#include "opencv2/opencv.hpp"
引入 iostream 库,并使用命名空间以减少代码输入量:
#include
using namespace std;
using namespace cv;
2.1 定义两个分类器,分别用于检测人脸和眼睛
CascadeClassifier face_cascade;
CascadeClassifier eyes_cascade;
2.2 加载已训练好的分类器
其中 face_cascade_name 和 eyes_cascade_name 为 XML 格式的分类器文件名(可从 OpenCV 官方 GitHub 免费下载预训练模型)。
face_cascade.load( face_cascade_name );
eyes_cascade.load( eyes_cascade_name );
2.3 定义检测人脸并显示结果的函数
函数的输入参数为 Mat 图像。
void detectAndDisplay( Mat frame )
输入的 Mat 变量 frame 为彩色图像,但色彩信息对检测价值不大,因此先转为灰度图并进行直方图均衡化(标准化)。
Mat frame_gray;
cvtColor( frame, frame_gray, COLOR_BGR2GRAY );//转灰度
equalizeHist( frame_gray, frame_gray );//标准化
一张图中可能检测到多个人脸,我们将结果存储在名为 faces 的向量中。多尺度检测函数 detectMultiScale 接收两个参数:输入图像 frame_gray 和输出结果 faces。
std::vector faces;//准备要存入人脸检测结果的向量
face_cascade.detectMultiScale( frame_gray, faces );//检测
由于 faces 向量存储了所有检测结果,我们可以通过 size() 获取其长度。以下循环遍历每个结果,计算每张脸的位置并绘制椭圆框。
for ( size_t i = 0; i < faces.size(); i++ )
{
Point center( faces[i].x + faces[i].width/2, faces[i].y + faces[i].height/2 );//获得每张脸部中心的xy像素坐标
ellipse( frame, center, Size( faces[i].width/2, faces[i].height/2 ), 0, 0, 360, Scalar( 255, 0, 255 ), 4 );//给每个脸画个圈圈框起来
}
在该循环中,我们还可以提取每张脸的区域,进一步检测眼睛,并使用圆形框标记。同样使用 detectMultiScale 进行检测。
Mat faceROI = frame_gray( faces[i] );//提取脸部区域
std::vector eyes;//准备要存入眼睛检测结果的向量
eyes_cascade.detectMultiScale( faceROI, eyes );//检测眼睛
for ( size_t j = 0; j < eyes.size(); j++ )
{
Point eye_center( faces[i].x + eyes[j].x + eyes[j].width/2, faces[i].y + eyes[j].y + eyes[j].height/2 );//获得每个眼睛的中心
int radius = cvRound( (eyes[j].width + eyes[j].height)*0.25 );
circle( frame, eye_center, radius, Scalar( 255, 0, 0 ), 4 );//把每个眼睛用圈圈框起来
}
2.4 整合后的 detectAndDisplay 函数
void detectAndDisplay( Mat frame )
{
Mat frame_gray;
cvtColor( frame, frame_gray, COLOR_BGR2GRAY );
equalizeHist( frame_gray, frame_gray );
//-- Detect faces
std::vector faces;
face_cascade.detectMultiScale( frame_gray, faces );
for ( size_t i = 0; i < faces.size(); i++ )
{
Point center( faces[i].x + faces[i].width/2, faces[i].y + faces[i].height/2 );
ellipse( frame, center, Size( faces[i].width/2, faces[i].height/2 ), 0, 0, 360, Scalar( 255, 0, 255 ), 4 );
Mat faceROI = frame_gray( faces[i] );
//-- In each face, detect eyes
std::vector eyes;
eyes_cascade.detectMultiScale( faceROI, eyes );
for ( size_t j = 0; j < eyes.size(); j++ )
{
Point eye_center( faces[i].x + eyes[j].x + eyes[j].width/2, faces[i].y + eyes[j].y + eyes[j].height/2 );
int radius = cvRound( (eyes[j].width + eyes[j].height)*0.25 );
circle( frame, eye_center, radius, Scalar( 255, 0, 0 ), 4 );
}
}
//-- Show what you got
imshow( "Capture - Face detection", frame );
}
2.5 摄像头循环读取与检测
实际应用中,循环读取摄像头并调用 detectAndDisplay 函数进行检测的代码如下:
Mat frame;//存放摄像头捕获图像的frame变量,它是个Mat数据
while ( capture.read(frame) )//循环把摄像头图像放入frame变量
{
detectAndDisplay( frame );//检测人脸并显示结果
}
2.6 主函数:整合所有功能
最后,编写主函数,将上述摄像头读取与人脸检测功能整合在一起。
int main( int argc, const char** argv )
{
CommandLineParser parser(argc, argv,
"{help h||}"
"{face_cascade|../../data/haarcascades/haarcascade_frontalface_alt.xml|Path to face cascade.}"
"{eyes_cascade|../../data/haarcascades/haarcascade_eye_tree_eyeglasses.xml|Path to eyes cascade.}"
"{camera|0|Camera device number.}");
parser.about( " This program demonstrates using the cv::CascadeClassifier class to detect objects (Face + eyes) in a video stream. "
"You can use Haar or LBP features. " );
parser.printMessage();
String face_cascade_name = parser.get("face_cascade");
String eyes_cascade_name = parser.get("eyes_cascade");
//-- 1. Load the cascades
if( !face_cascade.load( face_cascade_name ) )
{
cout << "--(!)Error loading face cascade ";
return -1;
};
if( !eyes_cascade.load( eyes_cascade_name ) )
{
cout << "--(!)Error loading eyes cascade ";
return -1;
};
int camera_device = parser.get("camera");
VideoCapture capture;
//-- 2. Read the video stream
capture.open( camera_device );
if ( ! capture.isOpened() )
{
cout << "--(!)Error opening video capture ";
return -1;
}
Mat frame;
while ( capture.read(frame) )
{
if( frame.empty() )
{
cout << "--(!) No captured frame -- Break! ";
break;
}
//-- 3. Apply the classifier to the frame
detectAndDisplay( frame );
if( waitKey(10) == 27 )
{
break; // escape
}
}
return 0;
}
小提示:
- 运行前请确保已将对应的 Haar 级联分类器 XML 文件下载到指定路径(如
../../data/haarcascades/),否则分类器加载会失败。 - 摄像头编号通常为0(内置摄像头),若使用外接摄像头可尝试1、2等数字。
- 按键盘
Esc键(ASCII码27)退出程序。
三、常见问题
Q1: 运行人脸检测时提示“Error loading face cascade”,怎么办?
A: 最常见的原因是分类器 XML 文件路径不正确。请务必检查 face_cascade 与 eyes_cascade 的路径是否与文件名匹配。您可以从 OpenCV 官方 GitHub 仓库下载 haarcascade_frontalface_alt.xml 和 haarcascade_eye_tree_eyeglasses.xml,并放置在与可执行文件相同的目录下。然后通过命令行参数指定路径,例如:./程序名 --face_cascade=haarcascade_frontalface_alt.xml --eyes_cascade=haarcascade_eye_tree_eyeglasses.xml。
Q2: 为什么我的摄像头无法打开?
A: 请确认摄像头已被系统识别且未被其他程序占用。在 Linux 下可运行 ls /dev/video* 查看摄像头设备;在 Windows 下可前往设备管理器检查。程序中默认使用摄像头编号 0,若只有单个摄像头通常正确;若有多个,请尝试修改参数 --camera=1 等。
Q3: 检测结果不准确,经常漏检或误检?
A: Haar 级联分类器对正面人脸检测效果较好,但容易受光照、角度、遮挡等因素影响。可尝试以下优化方法:
- 确保图像光线均匀,避免过暗或过亮。
- 使用
equalizeHist进行直方图均衡化(代码中已包含)。 - 调整
detectMultiScale的参数,例如设置scaleFactor=1.1、minNeighbors=3可提高检测精度,但可能降低速度。
希望本教程能助您顺利入门 OpenCV。记住,实践是最好的老师,多动手调试代码才能深入理解。祝您在 OpenCV 的学习之路上不断进步,找到属于自己的意义。
