【行人檢測】利用HOG+SVM訓練模型,檢測行人
1.準備樣本
2.提取hog特徵
3. 訓練SVM分類器
4.利用SVM訓練的分類器,進行檢測
理論基礎
使用OpenCv進行行人檢測的主要思想: HOG + SVM
HOG: 方向梯度直方圖(Histogram of Oriented Gradient, HOG)特徵是一種在計算機視覺和圖像處理中用來進行物體檢測的特徵描述子。HOG特徵通過計算和統計圖像局部區域的梯度方向直方圖來構成特徵.
SVM: (Support Vector Machine)指的是支持向量機,是常見的一種判別方法。在機器學習領域,是一個有監督的學習模型,通常用來進行模式識別、分類以及迴歸分析, 在行人檢測中可以用作區分行人和非行人的分類器。
在使用HOG + SVM進行行人檢測時, 採集HOG特徵的主要思想是通過對一幅圖像進行分析, 局部目標的表象和形狀可以被剃度或者邊緣密度方向分佈很好的好的描述. 我們對圖像的各個像素點採集土堆或者邊緣的方向直方圖, 根據直方圖的信息就可以描述圖片的特徵. 好在OpenCv 中已經提供了計算HOG特徵的方法, 根據採集到的HOG特徵向量, 供SVM分類使用.
SVM簡單來說就是一個分類器, 在行人檢測中就可以轉化爲行人與非行人的兩類分類問題, 在OpenCv中運用到的是基於網格法的SVM.使用採集到的正樣本(行人)和負樣本(非行人, 可以是汽車, 樹木, 路燈等等)的HOG特徵, 然後使用SVM分類器進行訓練, 得到行人檢測模型, 進行行人檢測.
整個流程
1.準備樣本:準備正負樣本,注意:大小一致,64*128
2.利用樣本提取hog描述子:維度爲3780+1=3781
--imgSize: 64*128
--block: 16*16(每個img中的block數目與block大小和步長都有關)
--cell: 8*8(每個block有2*2個cell,每個cell裏有9個特徵值)
--stride: 8*8(所以,一個img中的block計算,x向有(64-16)/8+1=7個block, y方向有(128-16)/8+1=15個block)
--bin: 9(每個cell裏的梯度直方圖分爲9個bin,180°分9份,每份180÷9=20°。爲什麼不是360而是180°?因爲tan的週期爲π )
所以hog特徵維度爲:
9*(2*2)*7 *15 =3780
再加上一個偏置共 :
3780+1=3781
具體hog特徵計算原理,可戳:https://blog.csdn.net/u012679707/article/details/80657020
3.利用提取到的hog特徵來訓練SVM
m個樣本,帶m個標籤,可提取到的hog特徵數目爲(m*3781),用特徵和標籤來訓練SVM。
具體SVM原理,可戳:https://blog.csdn.net/u012679707/article/details/80501358
4.利用SVM訓練的模型,來進行檢測。
1.準備樣本
正樣本:INRIA數據集中的pos樣本614張(..\INRIAPerson\Train\pos),因圖片大小不一,後續利用時需統一resize成(64*128)
負樣本:INRIA數據集中的neg樣本1218張(..\INRIAPerson\Train\neg),需將負樣本裁剪成和正樣本相同大小(64*128)。在裁剪時,將每張負樣本圖片經過兩次隨機裁剪,所以總數變爲1218*2=2436。
數據集下載地址:點擊打開鏈接
負樣本裁剪程序:
/********************************* 隨機剪裁負樣本 *******************************************/
void crop_negsample_random()
{
string imgName;
char saveName[200];
//string newNegFile = "F:\\opencv_project\\data\\INRIAPerson\\Train\\new_neg\\" ;
ifstream fileNeg("F:\\opencv_project\\data\\INRIAPerson\\Train\\neg\\ANegData.txt");
int num=0;
//讀取負樣本
for (int i = 0;i < cropNegNum && getline(fileNeg, imgName); i++)
{
imgName = "F:\\opencv_project\\data\\INRIAPerson\\Train\\neg\\" + imgName; //加路徑
Mat img = imread(imgName, IMREAD_UNCHANGED);
//Mat img;
if (img.empty())
{
cout << "can not load the image:" << imgName << endl;
continue;
}
if (img.cols >= 64 && img.rows >= 128)
{
num = 0;
//從每張圖片中隨機剪裁2張64*128的負樣本
for (int j = 0;j < 2;j++)
{
srand(time(NULL));//設置隨機數種子
int x = rand() % (img.cols - 64); //左上角x
int y = rand() % (img.rows - 128); //左上角y
Mat src = img(Rect(x, y, 64, 128));
sprintf_s(saveName, "F:\\opencv_project\\data\\INRIAPerson\\Train\\new_neg\\neg%dCropped%d.png",i, num++);
imwrite(saveName,src);
}
}
}
cout << "crop ok!" << endl;
}
然後,利用cmd命令行操作,生成列表文件,得到APosData.txt和ANegData.txt。
2.提取hog特徵
計算hog特徵,主要是compute()函數:
vector<float> descriptors;//hog描述符 向量
hog.compute(img, descriptors, Size(8, 8));//計算hog描述子,檢測窗口移動步長(8,8)
//cout << "負樣本描述子維數:" << descriptors.size() << endl;
/**
CV_WRAP virtual void compute(InputArray img,CV_OUT std::vector<float>& descriptors,Size winStride = Size(), Size padding = Size(),
const std::vector<Point>& locations = std::vector<Point>()) const;
@brief Computes HOG descriptors of given image.
@param img Matrix of the type CV_8U containing an image where HOG features will be calculated.要計算hog的img
@param descriptors Matrix of the type CV_32F 計算得到的hog特徵描述符,vector<float>型
@param winStride Window stride. It must be a multiple of block stride.窗口移動的步長
@param padding Padding
@param locations Vector of Point
*/
2.1讀取正負樣本,計算hog描述子 ----相當於特徵提取
//讀取正樣本,生成hog描述符
for (int i = 0;i < PosNum && getline(filePos, imgName); i++)
{
//cout << "處理正樣本:" << imgName << endl;
imgName = "F:\\opencv_project\\data\\INRIAPerson\\Train\\pos\\" + imgName; //加路徑
Mat img1 = imread(imgName, IMREAD_UNCHANGED);
Mat img;
if (img1.empty())
{
cout << "can not load the image:" << imgName << endl;
continue;
}
//img = img(Rect(16, 16, 64, 128));//將96*160的正樣本裁剪未64*128
resize(img1,img,Size(64,128));
vector<float> descriptors;//hog描述符 向量
hog.compute(img, descriptors, Size(8, 8));//計算hog描述子,檢測窗口移動步長(8,8)
//cout << "描述子維數:" << descriptors.size() << endl;
//根據第一個樣本,計算出維數,創建特徵矩陣和標籤矩陣
if (0 == i)
{
hogDescriptorDim = descriptors.size();//hogDescriptorDim=3780
sampleFeatureMat = Mat::zeros(PosNum + NegNum, hogDescriptorDim, CV_32FC1);
sampleLabelMat= Mat::zeros(1,PosNum + NegNum, CV_32FC1);
}
//將計算得到的描述子複製到樣本特徵矩陣
for (int j = 0;j < hogDescriptorDim;j++)
{
sampleFeatureMat.at<float>(i, j) = descriptors[j];//第i個樣本的特徵向量中的第j個元素
}
sampleLabelMat.at<int>(0, i) = 1;//正樣本標籤
}
cout << "pos ok!" << endl;
//讀取負樣本,生成hog描述符
for (int i = 0;i < NegNum && getline(fileNeg, imgName); i++)
{
//cout << "處理正樣本:" << imgName << endl;
imgName = "F:\\opencv_project\\data\\INRIAPerson\\Train\\new_neg\\" + imgName; //加路徑
Mat img = imread(imgName);
vector<float> descriptors;//hog描述符 向量
hog.compute(img, descriptors, Size(8, 8));//計算hog描述子,檢測窗口移動步長(8,8)
//cout << "負樣本描述子維數:" << descriptors.size() << endl;
/**
CV_WRAP virtual void compute(InputArray img,CV_OUT std::vector<float>& descriptors, Size winStride = Size(), Size padding = Size(),
const std::vector<Point>& locations = std::vector<Point>()) const;
@brief Computes HOG descriptors of given image.
@param img Matrix of the type CV_8U containing an image where HOG features will be calculated.
@param descriptors Matrix of the type CV_32F
@param winStride Window stride. It must be a multiple of block stride.
@param padding Padding
@param locations Vector of Point
*/
//將計算得到的描述子複製到樣本特徵矩陣
for (int j = 0;j < hogDescriptorDim;j++)
{
sampleFeatureMat.at<float>(i+PosNum , j) = descriptors[j];//第i個樣本的特徵向量中的第j個元素
}
sampleLabelMat.at<int>(0, i) = -1;//負樣本標籤
}
cout << "neg ok!" << endl;
3. 訓練SVM分類器
訓練SVM:
1.設置參數(線性SVM、核、迭代次數1000、誤差)
2.使用樣本和標籤進行學習(訓練),將學得的分類器保存.xml(包括sv、α、rho)。
3.爲了後續得到檢測器,可利用函數svm->getSupportVectors()獲得sv(支持向量),利用svm->getDecisionFunction(0, alpha, svIndex)獲得α(拉格朗日乘子)、rho(偏置)
//設置參數,注意Ptr的使用
Ptr<ml::SVM> svm = ml::SVM::create();
svm->setType(ml::SVM::C_SVC);
svm->setKernel(ml::SVM::LINEAR);//注意必須使用線性SVM進行訓練,因爲HogDescriptor檢測函數只支持線性檢測!!!
svm->setTermCriteria(TermCriteria(CV_TERMCRIT_ITER, 1000, FLT_EPSILON));
//使用SVM學習
svm->train(sampleFeatureMat, ml::ROW_SAMPLE, sampleLabelVec);
//保存分類器(裏面包括了SVM的參數,支持向量support vector , α和rho)
svm->save("data\\HOG_SVM_Classifier.xml");
/*
SVM訓練完成後得到的XML文件裏面,有一個數組,叫做support vector,還有一個數組,叫做alpha,有一個浮點數,叫做rho;
將alpha矩陣同support vector相乘,注意,alpha*supportVector,將得到一個行向量,將該向量前面乘以-1。
之後,在該行向量的最後添加一個元素rho。如此,便得到了一個分類器,利用該分類器,直接替換opencv中行人檢測默認的
那個分類器(cv::HOGDescriptor::setSVMDetector()),
*/
//獲取支持向量機:矩陣默認是CV_32F浮點型
Mat supportVector = svm->getSupportVectors();
//獲取alpha和rho
Mat alpha;//每個支持向量對應的參數α(拉格朗日乘子),默認alpha是float64的
Mat svIndex;//支持向量所在的索引
float rho = svm->getDecisionFunction(0, alpha, svIndex);
//轉換類型:這裏一定要注意,需要轉換爲32的
Mat alpha2;
alpha.convertTo(alpha2, CV_32FC1);
//結果矩陣,兩個矩陣相乘
Mat resultMat(1, hogDescriptorDim, CV_32FC1);
resultMat = alpha2 * supportVector;
//乘以-1,這裏爲什麼會乘以-1?
//注意因爲svm.predict使用的是alpha*sv*another-rho,如果爲負的話則認爲是正樣本,在HOG的檢測函數中,使用rho+alpha*sv*another(another爲-1)
for (int i = 0; i < hogDescriptorDim; ++i)
resultMat.at<float>(0, i) *= -1;
訓練得到的分類器xml如下:
4.利用SVM訓練的分類器,進行檢測
運行結果:
檢測結果:
檢測結果不太理想,畢竟樣本有限,此次目的主要是爲了瞭解整個過程,熟悉HOG特徵以及利用Hog特徵進行SVM訓練。爲日後自己設計檢測方法,提供思路。
完整程序:
// Hog_SVM_Pedestrian_detection.cpp: 定義控制檯應用程序的入口點。
// 自己訓練HOG+SVM實現圖片中的行人檢測
#include"stdafx.h"
#include<fstream>//fstream
#include<opencv2/core/core.hpp>
#include<opencv2/highgui/highgui.hpp>
#include<opencv2/imgproc/imgproc.hpp>
#include<opencv2/objdetect/objdetect.hpp> // hog
#include<opencv2/ml/ml.hpp>//SVM
#include<iostream>
#include<time.h>
#include<stdlib.h>
using namespace std;
using namespace cv;
//using namespace ml;
#define PosNum 614
#define NegNum 2436
#define cropNegNum 1218
//#define totalDim 3780
/********************************* 隨機剪裁負樣本 *******************************************/
void crop_negsample_random()
{
string imgName;
char saveName[200];
//string newNegFile = "F:\\opencv_project\\data\\INRIAPerson\\Train\\new_neg\\" ;
ifstream fileNeg("F:\\opencv_project\\data\\INRIAPerson\\Train\\neg\\ANegData.txt");
int num=0;
//讀取負樣本
for (int i = 0;i < cropNegNum && getline(fileNeg, imgName); i++)
{
imgName = "F:\\opencv_project\\data\\INRIAPerson\\Train\\neg\\" + imgName; //加路徑
Mat img = imread(imgName, IMREAD_UNCHANGED);
//Mat img;
if (img.empty())
{
cout << "can not load the image:" << imgName << endl;
continue;
}
if (img.cols >= 64 && img.rows >= 128)
{
num = 0;
//從每張圖片中隨機剪裁2張64*128的負樣本
for (int j = 0;j < 2;j++)
{
srand(time(NULL));//設置隨機數種子
int x = rand() % (img.cols - 64); //左上角x
int y = rand() % (img.rows - 128); //左上角y
Mat src = img(Rect(x, y, 64, 128));
sprintf_s(saveName, "F:\\opencv_project\\data\\INRIAPerson\\Train\\new_neg\\neg%dCropped%d.png",i, num++);
imwrite(saveName,src);
}
}
}
cout << "crop ok!" << endl;
}
/********************************* 訓練hog特徵 *******************************************/
vector<float> trainHogSvm(HOGDescriptor &hog)
{
size_t hogDescriptorDim; // hogDesDim ,與imgSize、blockSize、cellSize、paddingBlock、bin 有關
string imgName;
ifstream filePos("F:\\opencv_project\\data\\INRIAPerson\\Train\\pos\\APosData.txt");
ifstream fileNeg("F:\\opencv_project\\data\\INRIAPerson\\Train\\new_neg\\ANegData.txt");
Mat sampleFeatureMat; //所有訓練樣本的特徵組成的矩陣,每個樣本佔一行,列數等於hogDescriptorDim
//Mat sampleLabelMat; //樣本標籤,正樣本=1,負樣本=-1
vector<int> sampleLabelVec;
//讀取正樣本,生成hog描述符
for (int i = 0;i < PosNum && getline(filePos, imgName); i++)
{
//cout << "處理正樣本:" << imgName << endl;
imgName = "F:\\opencv_project\\data\\INRIAPerson\\Train\\pos\\" + imgName; //加路徑
Mat img1 = imread(imgName, IMREAD_UNCHANGED);
Mat img;
if (img1.empty())
{
cout << "can not load the image:" << imgName << endl;
continue;
}
//img = img(Rect(16, 16, 64, 128));//將96*160的正樣本裁剪未64*128
resize(img1,img,Size(64,128));
vector<float> descriptors;//hog描述符 向量
hog.compute(img, descriptors, Size(8, 8));//計算hog描述子,檢測窗口移動步長(8,8)
//cout << "描述子維數:" << descriptors.size() << endl;
//根據第一個樣本,計算出維數,創建特徵矩陣和標籤矩陣
if (0 == i)
{
hogDescriptorDim = descriptors.size();//hogDescriptorDim=3780
sampleFeatureMat = Mat::zeros(PosNum + NegNum, hogDescriptorDim, CV_32FC1);
//sampleLabelMat= Mat::zeros(1,PosNum + NegNum, );
}
//將計算得到的描述子複製到樣本特徵矩陣
for (int j = 0;j < hogDescriptorDim;j++)
{
sampleFeatureMat.at<float>(i, j) = descriptors[j];//第i個樣本的特徵向量中的第j個元素
}
//sampleLabelMat.at<int>(0, i) = 1;//正樣本標籤
sampleLabelVec.push_back(1);
}
cout << "pos ok!" << endl;
//讀取負樣本,生成hog描述符
for (int i = 0;i < NegNum && getline(fileNeg, imgName); i++)
{
//cout << "處理正樣本:" << imgName << endl;
imgName = "F:\\opencv_project\\data\\INRIAPerson\\Train\\new_neg\\" + imgName; //加路徑
Mat img = imread(imgName);
vector<float> descriptors;//hog描述符 向量
hog.compute(img, descriptors, Size(8, 8));//計算hog描述子,檢測窗口移動步長(8,8)
//cout << "負樣本描述子維數:" << descriptors.size() << endl;
/**
CV_WRAP virtual void compute(InputArray img,CV_OUT std::vector<float>& descriptors, Size winStride = Size(), Size padding = Size(),
const std::vector<Point>& locations = std::vector<Point>()) const;
@brief Computes HOG descriptors of given image.
@param img Matrix of the type CV_8U containing an image where HOG features will be calculated.
@param descriptors Matrix of the type CV_32F
@param winStride Window stride. It must be a multiple of block stride.
@param padding Padding
@param locations Vector of Point
*/
//將計算得到的描述子複製到樣本特徵矩陣
for (int j = 0;j < hogDescriptorDim;j++)
{
sampleFeatureMat.at<float>(i+PosNum , j) = descriptors[j];//第i個樣本的特徵向量中的第j個元素
}
//sampleLabelMat.at<int>(0, i) = -1;//負樣本標籤
sampleLabelVec.push_back(-1);
}
cout << "neg ok!" << endl;
//輸出樣本的HOG特徵向量矩陣到文件
ofstream fout("data\\hogDescriptor.txt");
for (int i = 0;i < PosNum + NegNum;i++)
{
fout << i << endl;
for (int j = 0;j < hogDescriptorDim;j++)
fout << sampleFeatureMat.at<float>(i, j) << " ";
fout << endl;
}
fout.close();
///////////////////////////////////使用SVM分類器訓練///////////////////////////////////////////////////
//設置參數,注意Ptr的使用
Ptr<ml::SVM> svm = ml::SVM::create();
svm->setType(ml::SVM::C_SVC);
svm->setKernel(ml::SVM::LINEAR);//注意必須使用線性SVM進行訓練,因爲HogDescriptor檢測函數只支持線性檢測!!!
svm->setTermCriteria(TermCriteria(CV_TERMCRIT_ITER, 1000, FLT_EPSILON));
//使用SVM學習
svm->train(sampleFeatureMat, ml::ROW_SAMPLE, sampleLabelVec);
/**
train(InputArray samples, int layout, InputArray responses);
@brief Trains the statistical model
@param samples training samples
@param layout See ml::SampleTypes.
@param responses vector of responses associated with the training samples.
*/
//保存分類器(裏面包括了SVM的參數,支持向量support vector , α和rho)
svm->save("data\\HOG_SVM_Classifier.xml");
/*
SVM訓練完成後得到的XML文件裏面,有一個數組,叫做support vector,還有一個數組,叫做alpha,有一個浮點數,叫做rho;
將alpha矩陣同support vector相乘,注意,alpha*supportVector,將得到一個行向量,將該向量前面乘以-1。
之後,在該行向量的最後添加一個元素rho。如此,便得到了一個分類器,利用該分類器,直接替換opencv中行人檢測默認的
那個分類器(cv::HOGDescriptor::setSVMDetector()),
*/
//獲取支持向量機:矩陣默認是CV_32F浮點型
Mat supportVector = svm->getSupportVectors();
/**
getSupportVectors();
@brief Retrieves all the support vectors
the method returns all the support vectors as a floating-point matrix, where support vectors are
stored as matrix rows.
*/
//獲取alpha和rho
Mat alpha;//每個支持向量對應的參數α(拉格朗日乘子),默認alpha是float64的
Mat svIndex;//支持向量所在的索引
float rho = svm->getDecisionFunction(0, alpha, svIndex);
/**
CV_WRAP virtual double getDecisionFunction(int i, OutputArray alpha, OutputArray svidx) const = 0;
@brief Retrieves the decision function決策函數
@param i the index of the decision function. If the problem solved is regression, 1-class or
2-class classification, then there will be just one decision function and the index should
always be 0. Otherwise, in the case of N-class classification, there will be N(N-1)/2 (ovr一對多)
decision functions.
@param alpha the optional output vector for weights, corresponding to different support vectors.
In the case of linear %SVM all the alpha's will be 1's.
@param svidx the optional output vector of indices of support vectors within the matrix of
support vectors (which can be retrieved by SVM::getSupportVectors). In the case of linear
%SVM each decision function consists of a single "compressed" support vector.
The method returns rho parameter of the decision function, a scalar subtracted from the weighted
sum of kernel responses.
*/
//轉換類型:這裏一定要注意,需要轉換爲32的
Mat alpha2;
alpha.convertTo(alpha2, CV_32FC1);
//結果矩陣,兩個矩陣相乘
Mat resultMat(1, hogDescriptorDim, CV_32FC1);
resultMat = alpha2 * supportVector;
//乘以-1,這裏爲什麼會乘以-1?
//注意因爲svm.predict使用的是alpha*sv*another-rho,如果爲負的話則認爲是正樣本,在HOG的檢測函數中,使用rho+alpha*sv*another(another爲-1)
for (int i = 0; i < hogDescriptorDim; ++i)
resultMat.at<float>(0, i) *= -1;
//得到最終的setSVMDetector(const vector<float>& detector)參數中可用的檢測子
vector<float> myDetector;
//將resultMat中的數據複製到數組myDetector中
for (int i = 0; i<hogDescriptorDim; i++)
{
myDetector.push_back(resultMat.at<float>(0, i));
}
//最後添加偏移量rho,得到檢測子
myDetector.push_back(rho);
cout << "檢測子維數:" << myDetector.size() << endl;
//將分類器保存到文件,便於HOG識別
//這個纔是真正的判別函數的參數(ω),HOG可以直接使用該參數進行識別
ofstream fopen1("data\\HOG_SVM.txt");
for (int i = 0; i< myDetector.size(); i++)
{
fopen1<<myDetector[i]<<" ";
}
fopen1.close();
//設置HOGDescriptor的檢測子
/*HOGDescriptor myHOG;
myHOG.setSVMDetector(myDetector);*/
//myHOG.setSVMDetector(HOGDescriptor::getDefaultPeopleDetector());
return myDetector;
}
/**
getSupportVectors()
@brief Retrieves all the support vectors
The method returns all the support vectors as a floating-point matrix, where support vectors are
stored as matrix rows.
*/
void detectAndDraw(HOGDescriptor &hog, Mat &img)
{
vector<Rect> found, found_filtered;
double t = (double)getTickCount();
hog.detectMultiScale(img, found, 0, Size(8, 8), Size(32, 32), 1.05, 2);//多尺度檢測目標,返回的矩形從大到小排列
t = (double)getTickCount() - t;
cout << "detection time = " << (t*1000. / cv::getTickFrequency()) << " ms" << endl;
cout << "detection result = " << found.size() << " Rects" << endl;
for (size_t i = 0; i < found.size(); i++)
{
Rect r = found[i];
size_t j;
// Do not add small detections inside a bigger detection. 如果有嵌套的話,則取外面最大的那個矩形框放入found_filtered中
for (j = 0; j < found.size(); j++)
if (j != i && (r & found[j]) == r)
break;
if (j == found.size())
found_filtered.push_back(r);
}
cout << "Real detection result = " << found_filtered.size() << " Rects" << endl;
for (size_t i = 0; i < found_filtered.size(); i++)
{
Rect r = found_filtered[i];
// The HOG detector returns slightly larger rectangles than the real objects,
// hog檢測結果返回的矩形比實際的要大一些
// so we slightly shrink the rectangles to get a nicer output.
// r.x += cvRound(r.width*0.1);
// r.width = cvRound(r.width*0.8);
// r.y += cvRound(r.height*0.07);
// r.height = cvRound(r.height*0.8);
rectangle(img, r.tl(), r.br(), cv::Scalar(0, 255, 0), 3);
}
}
/********************************* main() *******************************************/
int main()
{
//crop_negsample_random(); //裁剪負樣本
//Hog描述符:圖片尺寸64*128,block尺寸16*16,塊步長(8,8),cell尺寸8*8,方向梯度直方圖bin=9
HOGDescriptor hog(Size(64, 128), Size(16, 16), Size(8, 8), Size(8, 8), 9);
vector<float> myDetector;
myDetector=trainHogSvm(hog);//HOG特徵 + SVM訓練
hog.setSVMDetector(myDetector);//設置SVM檢測器
Mat img = imread("pedestrian.jpg");
if(img.empty()) cout<<"read fail\n"<<endl;
detectAndDraw(hog, img);//檢測
namedWindow("frame");
imshow("frame", img);
while(waitKey(10)!=27) ;
destroyWindow("frame");
return 0;
}
------------------------------------------- END -------------------------------------參考:https://blog.csdn.net/masibuaa/article/details/16113373
https://blog.csdn.net/masibuaa/article/details/16104981
https://blog.csdn.net/qq_37753409/article/details/79047063
https://blog.csdn.net/qianqing13579/article/details/46509037