OpenCV是一種經(jīng)常被用到的計算機視覺庫。然而,它的文檔是只用英文發(fā)布的。這對習慣中文閱讀的國內(nèi)計算機愛好者來說并不是太友好,特別是對那些還沒受過高等教育但對計算機科學抱有美好向往的普通大眾。
OpenCV各版本間的使用方法并不是完全統(tǒng)一的。翻譯工作與官方數(shù)據(jù)手冊的發(fā)布不可避免的會有滯后性,雖然本文作者已經(jīng)盡可能選取目前最新版本的文檔了,但是這些內(nèi)容也不可避免的會有一天變成過時的糟粕。所以提醒初學者們,盡信書不如無書。
很榮幸能成為大家學習OpenCV的領路人。作者祝大家都能在學習的過程中找人生到真正的意義。
圖像的基本操作 ? ? ? ? ?
對于人類來說,圖像可以解構(gòu)為畫面結(jié)構(gòu)、色彩和非常豐富的意象。你可以把它解構(gòu)為各種色塊或線條,然后用故事性的語言把這幅圖像存在腦海里。
反過來,對于計算機來說,這個過程就要機械得多了,計算機只認識組成一副圖像的那一個個像素。為了存儲這一個個像素,需要像素的坐標和色彩信息。OpenCV以一種叫Mat的結(jié)構(gòu)存儲圖像,你可以把它理解為一種數(shù)據(jù)結(jié)構(gòu),結(jié)構(gòu)上是下圖這樣的一個表格,表格的行列分布代表像素的行列。
在上圖的各個像素中,顏色又用三原色來表示,即BGR(藍綠紅)。這些BGR數(shù)據(jù)依次排列如下圖。其中,三原色里的每個顏色元素的值域是0-255(即8位),因此每個像素具有8x3位(即24位真彩色)。
我們來建立一個Mat數(shù)據(jù)M,存儲一個最簡單的2x2像素的圖像,且每個像素為藍色(0,0,255),可以這樣寫。要注意的是,目前最新的OpenCV 4.0.1已經(jīng)不支持C了,代碼文件需要按C++寫。
Mat M(2,2, CV_8UC3, Scalar(0,0,255));//新建2x2像素圖像
imshow("image", M);//顯示M的圖像
當然,大多數(shù)時候,我們是不會這樣傻傻地新建一個圖像的。我們可以直接從一個jpg文件里面讀取圖像,我將把讀取圖片文件的方法教給大家。
舉個例子,把一個叫Lena的照片放進一個叫M的Mat結(jié)構(gòu),可以像下面這么寫。另外,我們還演示了怎么把一個彩色圖像以灰度圖的方式讀取。
Mat M = imread("lena.jpg");//圖像來自圖片文件
imshow("image", M);//顯示M的圖像
Mat img = imread("lena.jpg", IMREAD_GRAYSCALE);//以灰度形式讀取圖片文件
imshow("grayimage", img);//顯示img的圖像(它是灰度圖)
反過來,一個Mat數(shù)據(jù),也可以寫入文件,這樣就把圖片給存起來了。舉個例子,讀取Lena的照片文件,然后存到一個叫out的文件里。
imwrite("image.jpg", M);//顯示M的圖像
Mat img = imread("lena.jpg", IMREAD_GRAYSCALE);//以灰度形式讀取圖片文件
imwrite("grayimage.jpg", img);//顯示img的圖像(它是灰度圖)
一個人臉檢測的例子 ? ? ? ? ?
這個例子,需要引用3個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;
定義兩個分類器,分別用來檢測臉和眼睛
CascadeClassifier face_cascade;
CascadeClassifier eyes_cascade;
加載已經(jīng)訓練好的分類器,其中face_cascade_name和eyes_cascade_name是xml格式的分類器文件名。(OpenCV的Github里有已經(jīng)訓練好的模型,可以免費下載)
face_cascade.load( face_cascade_name );
eyes_cascade.load( eyes_cascade_name );
定義一個檢測人臉并顯示檢測結(jié)果的函數(shù),函數(shù)的輸入變量是Mat圖像。
void detectAndDisplay( Mat frame )
輸入的Mat變量frame是彩色的,色彩信息對檢測沒什么用。所以我們把它轉(zhuǎn)成灰度圖,并標準化。
Mat frame_gray;
cvtColor( frame, frame_gray, COLOR_BGR2GRAY );//轉(zhuǎn)灰度
equalizeHist( frame_gray, frame_gray );//標準化
一張圖里有可能會檢測到多個人臉,我們可以把檢測結(jié)果用一個叫faces的向量來存儲。用分類器進行多尺度檢測的函數(shù)叫detectMultiScale,這個函數(shù)有兩個變量,第一個是輸入的圖像frame_gray,第二個是輸出的結(jié)果faces。
std::vector
face_cascade.detectMultiScale( frame_gray, faces );//檢測
由于faces是把檢測結(jié)果存成了一個向量,所以我們可以用size()函數(shù)得到這個向量的長度。下面這個代碼循環(huán)遍歷了每個結(jié)果,計算了每張臉的位置。
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 );//給每個臉畫個圈圈框起來
}
上面這個循環(huán)里,還可以把檢測到個每個臉的區(qū)域單獨提取出來,再檢測一下眼睛。然后嵌入一個用圈圈框出眼睛的循環(huán)。這一步,依然使用detectMultiScale函數(shù)進行檢測。
Mat faceROI = frame_gray( faces[i] );//提取臉部區(qū)域
std::vector
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 );//把每個眼睛用圈圈框起來
}
接下來,我們把上面的內(nèi)容整合一下,這個人臉檢測并顯示檢測結(jié)果的函數(shù)
detectAndDisplay可以寫成這樣
void detectAndDisplay( Mat frame )
{
Mat frame_gray;
cvtColor( frame, frame_gray, COLOR_BGR2GRAY );
equalizeHist( frame_gray, frame_gray );
//-- Detect faces
std::vector
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_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 );
}
實際中,循環(huán)讀攝像頭并用這個detectAndDisplay函數(shù)進行檢測的代碼可以這么寫。
Mat frame;//存放攝像頭捕獲圖像的frame變量,它是個Mat數(shù)據(jù)
while ( capture.read(frame) )//循環(huán)把攝像頭圖像放入frame變量
{
detectAndDisplay( frame );//檢測人臉并顯示結(jié)果
}
最后,我們來寫一下主函數(shù),把上面那個攝像頭讀取和人臉檢測功能加上。
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
String eyes_cascade_name = parser.get
//-- 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
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;
}
編輯:黃飛
評論
查看更多