作者:xukejing
1 前言
OpenCV是一种经常被用到的计算机视觉库。 然而,它的文档是只用英文发布的。 这对习惯中文阅读的国内计算机爱好者来说并不是太友好,特别是对那些还没受过高等教育但对计算机科学抱有美好向往的普通大众。 诚然,英文阅读对于业内的大牛们来说并不会带来什么障碍。 大牛们虽然技术水平高,但是能力越大责任越大,他们并不能天天来社区陪大家吹水。 客观地看,我们的社区的生存根基,其实并不是那少数几个大牛,而是那更广大的初级爱好者,他们才是社区的活力源泉,给论坛源源不断的创造内容和点击。 作者认为独乐乐不如众乐乐,想给大家一些关怀。
OpenCV各版本间的使用方法并不是完全统一的,最新的是OpenCV4.0.1,已经与2.x版本的用法有较大的区别。 翻译工作与官方数据手册的发布不可避免的会有滞后性,虽然本文作者已经尽可能选取目前最新版本的文档了,但是这些内容也不可避免的会有一天变成过时的糟粕。 所以提醒初学者们,尽信书不如无书。 很荣幸能成为大家学习OpenCV的领路人。 作者祝大家都能在学习的过程中找人生到真正的意义。
本文的参考文档见https://docs.opencv.org/4.0.1/d1/dfb/intro.html
2 图像的基本操作
对于人类来说,图像可以解构为画面结构、色彩和非常丰富的意象。 你可以把它解构为各种色块或线条,然后用故事性的语言把这幅图像存在脑海里。
反过来,对于计算机来说,这个过程就要机械得多了,计算机只认识组成一副图像的那一个个像素。 为了存储这一个个像素,需要像素的坐标和色彩信息。 OpenCV以一种叫Mat的结构存储图像,你可以把它理解为一种数据结构,结构上是下图这样的一个表格,表格的行列分布代表像素的行列。
tutorial_how_matrix_stored_1.png
在上图的各个像素中,颜色又用三原色来表示,即BGR(蓝绿红)。 这些BGR数据依次排列如下图。 其中,三原色里的每个颜色元素的值域是0-255(即8位),因此每个像素具有8x3位(即24位真彩色)。
tutorial_how_matrix_stored_2.png
我们来建立一个Mat数据M,存储一个最简单的2x2像素的图像,且每个像素为蓝色(0,0,255),可以这样写。 要注意的是,目前最新的OpenCV 4.0.1已经不支持C了,代码文件需要按C++写。
Mat M(2,2, CV_8UC3, Scalar(0,0,255));//新建2x2像素图像
imshow("image", M);//显示M的图像
当然,大多数时候,我们是不会这样傻傻地新建一个图像的。 我们可以直接从一个jpg文件里面读取图像,我将把读取图片文件的方法教给大家。 举个例子,把一个叫Lena的照片放进一个叫M的Mat结构,可以像下面这么写。 另外,我们还演示了怎么把一个彩色图像以灰度图的方式读取。
Mat M = imread("lena.jpg");//图像来自图片文件
imshow("image", M);//显示M的图像
Mat img = imread("lena.jpg", IMREAD_GRAYSCALE);//以灰度形式读取图片文件
imshow("grayimage", img);//显示img的图像(它是灰度图)
反过来,一个Mat数据,也可以写入文件,这样就把图片给存起来了。 举个例子,读取Lena的照片文件,然后存到一个叫out的文件里。
Mat M = imread("lena.jpg");//图像来自图片文件
imwrite("image.jpg", M);//显示M的图像
Mat img = imread("lena.jpg", IMREAD_GRAYSCALE);//以灰度形式读取图片文件
imwrite("grayimage.jpg", img);//显示img的图像(它是灰度图)
3 一个人脸检测的例子
这个例子,需要引用3个OpenCV的头文件
#include "opencv2/objdetect.hpp"
#include "opencv2/highgui.hpp"
#include "opencv2/imgproc.hpp"
当然,也可以一步到位,把所有的OpenCv库都引了
#include "opencv2/opencv.hpp"
应用iostream库,并使用命名空间来减少代码里面文字的输入量
#include <iostream>
using namespace std;
using namespace cv;
定义两个分类器,分别用来检测脸和眼睛
CascadeClassifier face_cascade;
CascadeClassifier eyes_cascade;
加载已经训练好的分类器,其中face_cascade_name和eyes_cascade_name是xml格式的分类器文件名。 (OpenCV的Github里有已经训练好的模型,可以免费下载)
face_cascade.load( face_cascade_name );
eyes_cascade.load( eyes_cascade_name );
定义一个检测人脸并显示检测结果的函数,函数的输入变量是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<Rect> 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<Rect> 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 );//把每个眼睛用圈圈框起来
}
接下来,我们把上面的内容整合一下,这个人脸检测并显示检测结果的函数detectAndDisplay可以写成这样
void detectAndDisplay( Mat frame )
{
Mat frame_gray;
cvtColor( frame, frame_gray, COLOR_BGR2GRAY );
equalizeHist( frame_gray, frame_gray );
//-- Detect faces
std::vector<Rect> 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<Rect> 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 );
}
实际中,循环读摄像头并用这个detectAndDisplay函数进行检测的代码可以这么写。
Mat frame;//存放摄像头捕获图像的frame变量,它是个Mat数据
while ( capture.read(frame) )//循环把摄像头图像放入frame变量
{
detectAndDisplay( frame );//检测人脸并显示结果
}
最后,我们来写一下主函数,把上面那个摄像头读取和人脸检测功能加上
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( "\nThis program demonstrates using the cv::CascadeClassifier class to detect objects (Face + eyes) in a video stream.\n"
"You can use Haar or LBP features.\n\n" );
parser.printMessage();
String face_cascade_name = parser.get<String>("face_cascade");
String eyes_cascade_name = parser.get<String>("eyes_cascade");
//-- 1. Load the cascades
if( !face_cascade.load( face_cascade_name ) )
{
cout << "--(!)Error loading face cascade\n";
return -1;
};
if( !eyes_cascade.load( eyes_cascade_name ) )
{
cout << "--(!)Error loading eyes cascade\n";
return -1;
};
int camera_device = parser.get<int>("camera");
VideoCapture capture;
//-- 2. Read the video stream
capture.open( camera_device );
if ( ! capture.isOpened() )
{
cout << "--(!)Error opening video capture\n";
return -1;
}
Mat frame;
while ( capture.read(frame) )
{
if( frame.empty() )
{
cout << "--(!) No captured frame -- Break!\n";
break;
}
//-- 3. Apply the classifier to the frame
detectAndDisplay( frame );
if( waitKey(10) == 27 )
{
break; // escape
}
}
return 0;
}