OpenCV特征提取与检测实战
01. 概述
(1)什么是图像特征
可以表达图像中对象的主要信息、并且以此为依据可以从其它未知图像中检测出相似或者相同对象。
(2)常见图像特征
边缘、角点、纹理。
(3)图像特征描述
描述子生成
(4)特征提取与特征描述
- SIFT
- SURF
- HOG
- Haar
- LBP
- KAZE
- AKAZE
- BRISK
(5)DDM
Detection、Descriotion、Matching
02. OpenCV3.2.0编译
03. Harris角点检测
(1)Harris角点检测理论
(2)API介绍
void cornerHarris(
cv::InputArray src,
cv::OutputArray dst,
int blockSize,
int ksize=3,
double k,
int borderType=cv::BORDER_DEFAULT
}
- src 输入单通道float图像
- dst 类型为CV_32FC(6),包含2个特征值,以及对应的2个2维向量,总计6个结果
- blockSize 角点检测中要考虑的领域大小
- ksize Sobel求导中使用的窗口大小
- k 表示计算角度响应时候的参数大小,默认在0.04~0.06
- borderType 边缘补齐方式,使用默认即可
(3)代码演示
#include <opencv2/opencv.hpp>
using namespace cv;
using namespace std;
Mat src, dst, gray_src, temp;
int threshold_val = 100;
int threshold_max_val = 255;
void Harris_Demo(int, void*)
{
dst = Mat::zeros(gray_src.size(), CV_32FC1);
cornerHarris(gray_src, dst, 2, 3, 0.04);
normalize(dst, temp, 0, 255, NORM_MINMAX, CV_32FC1, Mat());
convertScaleAbs(temp, dst);
Mat resultImg = src.clone();
// cout << dst << endl;
for (int i = 0; i < resultImg.rows; i++) {
uchar* cur = dst.ptr(i);
for (int j = 0; j < resultImg.cols; j++) {
int val = cur[j];
if (val > threshold_val){
circle(resultImg, Point(i, j), 1, Scalar(0, 0, 255), 1);
}
}
}
imshow("output", resultImg);
}
int main() {
src = imread("C:/images/chessboard.jpg");
if (src.empty())
{
return -1;
}
imshow("test", src);
cvtColor(src, gray_src, CV_BGR2GRAY);
namedWindow("output", CV_WINDOW_AUTOSIZE);
createTrackbar("value", "output", &threshold_val, threshold_max_val, Harris_Demo);
Harris_Demo(0, 0);
waitKey(0);
return 0;
}
04. 自定义角点检测器
(1)Shi-Tomasi角点检测理论
跟Harris角点检测的理论几乎完全一致,唯一不同的是在使用矩阵特征值$\lambda _1和\lambda _2$计算角度响应的时候
下面是Harris角点检测时候计算角点响应时使用的公式:
这是Shi-Tomasi角点检测时候计算角点响应时使用的公:
(2)API介绍
void cv::goodFeaturesToTrack(
cv::InputArray image,
cv::OutputArray corners,
int maxCorners,
double qualityLevel,
double minDistance,
cv::InputArray mask = noArray(),
int blockSize = 3,
bool useHarrisDetector = false,
double k = 0.04
);
- image 表示输入图像,8位或32位单通道图
- corners 输出检测到的所有角点,类型为vector或数组,由实际给定的参数类型而定。如果是vector,那么它应该是一个包含cv::Point2f的vector对象;如果类型是cv::Mat,那么它的每一行对应一个角点,点的x、y位置分别是两列
- maxCorners 表示返回角点的数目,如果检测出来角点数目大于最大数目则返回响应值最强前规定数目
- qualityLevel 表示检测到的角点的质量水平,通常是0.10到0.01之间的数值,不能大于1.0
- minDistance 用于区分相邻两个角点之间的最小距离(欧几里得距离)
- blockSize 表示在计算角点时参与运算的区域大小,常用值为3,但是如果图像的分辨率较高则可以考虑使用较大一点的值
- useHarrisDetector 用于指定角点检测的方法,如果是true则使用Harris角点检测,false则使用Shi Tomasi算法, 默认为false
- k 在使用Harris算法时使用,一般处于0.04-0.06之间,这里是经验值,一般使用默认0.04
void cv::cornerEigenValsAndVecs(
cv::InputArray src,
cv::OutputArray dst,
int blockSize,
int ksize=3,
int borderType=cv::BORDER_DEFAULT
)
- blockSize 表示在计算角点时参与运算的区域大小,常用值为3,但是如果图像的分辨率较高则可以考虑使用较大一点的值
- ksize Soble算子当中的核大小
void cornerMinEigenVal(
cv::InputArray src,
cv::OutputArray dst,
int blockSize,
int ksize=3,
int borderType=cv::BORDER_DEFAULT
}
cornerMinEigenVal和cornerEigenValsAndVecs参数相同。
(3)代码演示
#include <opencv2/opencv.hpp>
using namespace cv;
using namespace std;
Mat src, dst, gray_src, temp, harris_dist, harris_rsp_img, shi_tomas_rsp_img;
double harris_min_rsp, harris_max_rsp;
double shi_tomas_min_rsp, shi_tomas_max_rsp;
int quality_level = 30, max_count = 100, shi_quality_level = 30;
void Custom_Harris_Demo(int, void*)
{
if (quality_level < 10) quality_level = 10;
Mat result_image = src.clone();
double t = harris_min_rsp + quality_level * 1.0 / max_count * (harris_max_rsp - harris_min_rsp);
for (int i = 0; i < src.rows; i++)
for (int j = 0; j < src.cols; j++)
{
float v = harris_rsp_img.at<float>(i, j);
if (v > t){
circle(result_image, Point(i, j), 2, Scalar(0, 0, 255), 2, 8, 0);
}
}
imshow("custom harris", result_image);
}
void Custom_Shi_Tomas_Demo(int, void*){
if (shi_quality_level < 20) shi_quality_level = 20;
Mat result_image = src.clone();
double t = shi_tomas_min_rsp + shi_quality_level * 1.0 / max_count * (shi_tomas_max_rsp - shi_tomas_min_rsp);
for (int i = 0; i < src.rows; i++)
for (int j = 0; j < src.cols; j++)
{
float v = shi_tomas_rsp_img.at<float>(i, j);
if (v > t){
circle(result_image, Point(i, j), 2, Scalar(0, 0, 255), 2, 8, 0);
}
}
imshow("custom tomas", result_image);
}
int main() {
src = imread("C:/images/chessboard.jpg");
if (src.empty()){
return -1;
}
imshow("input", src);
// 灰度图像
cvtColor(src, gray_src, COLOR_BGR2GRAY);
// 计算特征值与特征向量
harris_dist = Mat::zeros(src.size(), CV_32FC(6));
harris_rsp_img = Mat::zeros(src.size(), CV_32FC1);
cornerEigenValsAndVecs(gray_src, harris_dist, 2, 3);
// 计算响应
for (int i = 0; i < harris_dist.rows; i++){
for (int j = 0; j < harris_dist.cols; j++){
double lamda1 = harris_dist.at<Vec6f>(i, j)[0];
double lamda2 = harris_dist.at<Vec6f>(i, j)[1];
harris_rsp_img.at<float>(i, j) = lamda1 * lamda2 - 0.04 * pow(lamda1 + lamda2, 2);
}
}
minMaxLoc(harris_rsp_img, &harris_min_rsp, &harris_max_rsp, 0, 0, Mat());
namedWindow("custom harris", CV_WINDOW_AUTOSIZE);
createTrackbar("value:", "custom harris", &quality_level, max_count, Custom_Harris_Demo);
Custom_Harris_Demo(0, 0);
// 计算最小特征值
shi_tomas_rsp_img = Mat::zeros(src.size(), CV_32FC1);
cornerMinEigenVal(gray_src, shi_tomas_rsp_img, 2, 3, BORDER_DEFAULT);
minMaxLoc(shi_tomas_rsp_img, &shi_tomas_min_rsp, &shi_tomas_max_rsp, 0, 0, Mat());
namedWindow("custom tomas", CV_WINDOW_AUTOSIZE);
createTrackbar("value", "custom tomas", &shi_quality_level, max_count, Custom_Shi_Tomas_Demo);
Custom_Shi_Tomas_Demo(0, 0);
waitKey(0);
return 0;
}
05. 亚像素级别角点检测
(1)提高检测精准度
理论与现实总是不一致的,实际情况下几乎所有的角点不会是一个真正的准确像素点。(100, 5) 实际上是(100.234, 5.789)。cv::goodFeaturesToTrack()提取到的角点只能达到像素级别,在很多情况下并不能满足实际的需求,这时,我们则需要使用cv::cornerSubPix()对检测到的角点作进一步的优化计算,可使角点的精度达到亚像素级别。在跟踪、三维重建、相机校正当中就要用到亚像素精准检测。
(2)API介绍
void cv::cornerSubPix(
cv::InputArray image,
cv::InputOutputArray corners,
cv::Size winSize,
cv::Size zeroZone,
cv::TermCriteria criteria
);
- image 表示输入图像,和cv::goodFeaturesToTrack()中的输入图像是同一个图像
- corners 表示检测到的角点,即作为输入也作为输出
- winSize 计算亚像素角点时考虑的区域的大小,大小为$N\times N$; $N=(winSize\times 2+1)$
- zerozone 类似于winSize,但是总是具有较小的范围,通常忽略,即Size(-1, -1)
- criteria 计算亚像素时停止迭代的标准,可选的值有cv::TermCriteria::MAX_ITER 、cv::TermCriteria::EPS(可以是两者其一,或两者均选),前者表示迭代次数达到了最大次数时停止,后者表示角点位置变化的最小值已经达到最小时停止迭代,二者均使用cv::TermCriteria()构造函数进行指定
(3)代码演示
#include <opencv2/opencv.hpp>
#include <iostream>
using namespace cv;
using namespace std;
int max_corners = 20;
int max_count = 50;
Mat src, gray_src;
void SubPixel_Demo(int, void*);
int main(int argc, char** argv) {
src = imread("C:/images/chessboard.jpg");
if (src.empty()) {
return -1;
}
namedWindow("input image", CV_WINDOW_AUTOSIZE);
imshow("input image", src);
cvtColor(src, gray_src, COLOR_BGR2GRAY);
namedWindow("SubPixel Result", CV_WINDOW_AUTOSIZE);
createTrackbar("Corners:", "SubPixel Result", &max_corners, max_count, SubPixel_Demo);
SubPixel_Demo(0, 0);
waitKey(0);
return 0;
}
void SubPixel_Demo(int, void*) {
if (max_corners < 5) {
max_corners = 5;
}
vector<Point2f> corners;
double quality_level = 0.01;
double min_distance = 10;
// 角点检测
goodFeaturesToTrack(gray_src, corners, max_corners, quality_level, min_distance, Mat(), 3, false, 0.04);
cout << "number of corners: " << corners.size() << endl;
Mat result_image = src.clone();
// 将角点绘制在原图上
for (int t = 0; t < corners.size(); t++) {
circle(result_image, corners[t], 2, Scalar(0, 0, 255), 2, 8, 0);
}
imshow("SubPixel Result", result_image);
Size win_size = Size(5, 5);
Size zero_zone = Size(-1, -1);
// 指定亚像素迭代标注
TermCriteria tc = TermCriteria(TermCriteria::EPS + TermCriteria::MAX_ITER, 40, 0.001);
// 输出Harris角点检测位置
for (size_t t = 0; t < corners.size(); t++) {
cout << (t + 1) << " .point[x, y] = " << corners[t].x << " , " << corners[t].y << endl;
}
// 亚像素检测
cornerSubPix(gray_src, corners, win_size, zero_zone, tc);
// 输出亚像素角点位置
for (size_t t = 0; t < corners.size(); t++) {
cout << (t + 1) << " .point[x, y] = " << corners[t].x << " , " << corners[t].y << endl;
}
}
06. SURF 特征检测
(1)SUFR特征检测介绍
如果在不同的尺度内用高斯滤波器计算指定像素的拉普拉斯算子,会得到不同的数值。观察滤波器对不同尺度因子的响应规律,所得曲线最终在给定的。值处达到最大值。对于以不同尺度拍摄的两幅图像的同一个物体,对应的两个。值的比率等于拍摄两幅图像的尺度的比率。这一重要观察是尺度不变特征提取过程的核心。也就是说,为了检测尺度不变特征,需要在图像空间(图像中)和尺度空间(通过在不同尺度下应用导数滤波器得到)分别计算局部最大值。
SURF用以下方法实现了这个理论。首先,为了检测特征而对每个像素计算Hessian矩阵。
该矩阵衡量了一个函数的局部曲率,定义如下所示:
根据矩阵的行列式值,可以得到曲率的强度。该方法把角点定义为局部高曲率(即在多个方向上的变化幅度都很高)的像素点。这个矩阵由二阶导数构成,因此可以用高斯内核的拉普拉斯算子在不同的尺度(即不同的。值)下计算得到。这样,Hessian矩阵就成了三个变量的函数,即H(x,y,o)。如果Hessian矩阵的行列式值在普通空间和尺度空间(即需要执行3x3x3次非最大值抑制)都达到了局部最大值,那么就认为这是一个尺度不变特征。注意,为了确认点的有效性,必须在cv::xfeatures2d::SurfFeatureDetector类的create方法的第一个参数中指定最小行列式值。
但是在不同尺度下计算全部导数值的计算量非常大。SURF算法的目标是使这个过程尽可能地高效,具体做法是使用近似的高斯内核,只附带几个整数。它们的结构如下所示:
左边的内核用于估算混合二阶导数,右边的内核用于估算垂直方向的二阶导数。将右边的内核旋转后,就可估算水平方向的二阶导数。最小的内核尺寸为9$\times$9像素,对应$\sigma \approx $1.2,要在尺度空间中使用,需要连续应用一系列内核,并且内核的尺寸逐个增大。可以在cv::xfeatures2d::sur fFeaturebetector::ereate方法的附加参数中指定滤波器的准确数量。默认使用12个不同尺寸的内核(最大尺寸为99$\times$99)。注意,在用直方图统计像素时采用积分图像,是为了确保只用三个加法运算就可以计算每个滤波器分支的累加值,与滤波器尺寸无关。
一旦找到局部最大值,就可以使用尺度空间和图像空间的插值法,获得被检测兴趣点的精确位置。最后得到一批亚像素级的特征点,并且每个特征点都关联一个尺度值。
(2)相关API
SURF构造函数
SURF::SURF(
double hessianThreshold=100,
int nOctaves=4,
int nOctaveLayers=3,
bool extended=false,
bool upright=false
)
- hessianThreshold 阈值检测器使用Hessian的关键点,默认值在300-500之间
- nOctaves 表示在四个尺度空间
- nOctaveLayers 表示每个尺度的层数
- upright 0表示计算选择不变性,1表示不计算,速度更快
特征点绘制函数
void drawKeypoints( const Mat& image,
const vector<KeyPoint>& keypoints,
CV_OUT Mat& outImage,
const Scalar& color=Scalar::all(-1),
int flags=DrawMatchesFlags::DEFAULT
)
- image:原始图像,可以使三通道或单通道图像;
- keypoints:特征点向量,向量内每一个元素是一个KeyPoint对象,包含了特征点的各种属性信息;
- outImage:特征点绘制的画布图像,可以是原图像;
- color:绘制的特征点的颜色信息,默认绘制的是随机彩色;
- flags:特征点的绘制模式,其实就是设置特征点的那些信息需要绘制,那些不需要绘制,有以下几种模式可选:
DEFAULT:只绘制特征点的坐标点,显示在图像上就是一个个小圆点,每个小圆点的圆心坐标都是特征点的坐标。
DRAW_OVER_OUTIMG:函数不创建输出的图像,而是直接在输出图像变量空间绘制,要求本身输出图像变量就是一个初始化好了的,size与type都是已经初始化好的变量
NOT_DRAW_SINGLE_POINTS:单点的特征点不被绘制
DRAW_RICH_KEYPOINTS:绘制特征点的时候绘制的是一个个带有方向的圆,这种方法同时显示图像的坐标,size,和方向,是最能显示特征信息的一种绘制方式。
(3)演示代码
#include <opencv2/opencv.hpp>
#include <opencv2/xfeatures2d.hpp>
#include <iostream>
using namespace cv;
using namespace cv::xfeatures2d;
using namespace std;
int main(int argc, char** argv) {
Mat src = imread("C:/images/lena_full.jpg", IMREAD_GRAYSCALE);
if (src.empty()) {
return -1;
}
namedWindow("input image", CV_WINDOW_AUTOSIZE);
imshow("input image", src);
// SURF特征检测
int minHessian = 100;
Ptr<SURF> detector = SURF::create(minHessian);
vector<KeyPoint> keypoints;
detector->detect(src, keypoints, Mat());
// 绘制关键点
Mat keypoint_img;
drawKeypoints(src, keypoints, keypoint_img, Scalar::all(-1), DrawMatchesFlags::DEFAULT);
imshow("KeyPoints Image", keypoint_img);
waitKey(0);
return 0;
}
07. SIFT特征检测
SURF算法是SIFT算法的加速版,而 SIFT ( Scale-Invariant Feature Transform,尺度不变特征转换)是另—种著名的尺度不变特征检测法。
SIFT检测特征时也采用了图像空间和尺度空间的局部最大值,但它使用拉普拉斯滤波器响应,而不是 Hessian行列式值。这个拉普拉斯算子是利用高斯滤波器的差值,在不同尺度(即逐步加大$\sigma$值)下计算得到的。为了提高性能,$\sigma$值每翻一倍,图像的尺寸就缩小一半。每个金字塔级别代表一个八度( octave),每个尺度是一图层( layer )。一个八度通常有三个图层。
下图表示两个八度的金字塔,其中第一个八度的四个高斯滤波图像产生了三个DoG图层。
(2)相关API
cv::xfeatures2d::SIFT::create(
int nfeatures = 0,
int nOctaveLayers = 3,
double contrastThreshold = 0.04,
double edgeThreshold = 10,
double sigma = 1.6
)
- nfeatures 特征点数目(算法对检测出的特征点排名,返回最好的nfeatures个特征点)。
- nOctaveLayers 金字塔中每组的层数(算法中会自己计算这个值)。
- contrastThreshold 过滤掉较差的特征点的对阈值。contrastThreshold越大,返回的特征点越少。
- edgeThreshold 过滤掉边缘效应的阈值。edgeThreshold越大,特征点越多,被过滤掉的越少。
- sigma:金字塔第0层图像高斯滤波系数,也就是$\sigma$。
(3)代码演示
#include <opencv2/opencv.hpp>
#include <opencv2/xfeatures2d.hpp>
using namespace cv;
using namespace cv::xfeatures2d;
using namespace std;
int main(int argc, char** argv) {
Mat src = imread("C:/images/lena_full.jpg", IMREAD_GRAYSCALE);
if (src.empty()) {
return -1;
}
namedWindow("input image", CV_WINDOW_AUTOSIZE);
imshow("input image", src);
// SIFT特征检测
int numFeatures = 300;
Ptr<SIFT> detector = SIFT::create(numFeatures);
vector<KeyPoint> key_points;
detector->detect(src, key_points, Mat());
// 绘制关键点
Mat keypoint_image;
drawKeypoints(src, key_points, keypoint_image, Scalar::all(-1), DrawMatchesFlags::DEFAULT);
namedWindow("SIFT", CV_WINDOW_AUTOSIZE);
imshow("SIFT", keypoint_image);
waitKey(0);
return 0;
}
08. HOG特征检测
(1)HOG特征检测介绍
方向梯度直方图(Histogram of Oriented Gradient, HOG)特征是一种在计算机视觉和图像处理中用来进行物体检测的特征描述子。它通过计算和统计图像局部区域的梯度方向直方图来构成特征。Hog特征结合SVM分类器已经被广泛应用于图像识别中,尤其在行人检测中获得了极大的成功。需要提醒的是,HOG+SVM进行行人检测的方法是法国研究人员Dalal在2005的CVPR上提出的,而如今虽然有很多行人检测算法不断提出,但基本都是以HOG+SVM的思路为主。
(2)主要思想
在一副图像中,局部目标的表象和形状(appearance and shape)能够被梯度或边缘的方向密度分布很好地描述。(本质:梯度的统计信息,而梯度主要存在于边缘的地方)。
(3)实现方法
首先将图像分成小的连通区域,我们把它叫细胞单元。然后采集细胞单元中各像素点的梯度的或边缘的方向直方图。最后把这些直方图组合起来就可以构成特征描述器。
(4)实现步骤
- 灰度化(将图像看做一个x,y,z(灰度)的三维图像);
- 采用Gamma校正法对输入图像进行颜色空间的标准化(归一化);目的是调节图像的对比度,降低图像局部的阴影和光照变化所造成的影响,同时可以抑制噪音的干扰;
- 计算图像每个像素的梯度(包括大小和方向);主要是为了捕获轮廓信息,同时进一步弱化光照的干扰。
- 将图像划分成小cells(例如6*6像素/cell);
- 统计每个cell的梯度直方图(不同梯度的个数),即可形成每个cell的descriptor;
- 将每几个cell组成一个block(例如3*3个cell/block),一个block内所有cell的特征descriptor串联起来便得到该block的HOG特征descriptor。
- 将图像image内的所有block的HOG特征descriptor串联起来就可以得到该image(你要检测的目标)的HOG特征descriptor了。这个就是最终的可供分类使用的特征向量了。
(5)代码演示
#include <opencv2/opencv.hpp>
#include <iostream>
using namespace cv;
using namespace std;
int main(int argc, char** argv) {
Mat src = imread("C:/images/peoples.jpg");
if (src.empty()) {
return -1;
}
namedWindow("input image", CV_WINDOW_AUTOSIZE);
imshow("input image", src);
/*Mat dst, dst_gray;
resize(src, dst, Size(64, 128));
cvtColor(dst, dst_gray, COLOR_BGR2GRAY);
HOGDescriptor detector(Size(64, 128), Size(16, 16), Size(8, 8), Size(8, 8), 9);
vector<float> descriptors;
vector<Point> locations;
detector.compute(dst_gray, descriptors, Size(0, 0), Size(0, 0), locations);
printf("number of HOG descriptors : %d", descriptors.size());
*/
HOGDescriptor hog = HOGDescriptor();
hog.setSVMDetector(hog.getDefaultPeopleDetector());
vector<Rect> foundLocations;
hog.detectMultiScale(src, foundLocations, 0, Size(8, 8), Size(32, 32), 1.05, 2);
Mat result = src.clone();
for (size_t t = 0; t < foundLocations.size(); t++) {
rectangle(result, foundLocations[t], Scalar(0, 0, 255), 2, 8, 0);
}
namedWindow("HOG SVM Detector Demo", CV_WINDOW_AUTOSIZE);
imshow("HOG SVM Detector Demo", result);
waitKey(0);
return 0;
}
09. LBP特征
(1)LBP特征介绍
LBP是一种简单,有效的纹理分类的特征提取算法。LBP算子是由Ojala等人于1996年提出的,主要的论文是"Multiresolution gray-scale and rotation invariant texture classification with local binary patterns", pami, vol 24, no.7, July 2002。LBP就是"local binary pattern"的缩写。局部二值模式是一个简单但非常有效的纹理运算符。它将各个像素与其附近的像素进行比较,并把结果保存为二进制数。由于其辨别力强大和计算简单,局部二值模式纹理算子已经在不同的场景下得到应用。LBP最重要的属性是对诸如光照变化等造成的灰度变化的鲁棒性。它的另外一个重要特性是它的计算简单,这使得它可以对图像进行实时分析。
(2)局部二值模式特征向量实现步骤
- 将检测窗口切分为区块(cells,例如,每个区块16x16像素)。
- 对区块中的每个像素,与它的八个邻域像素进行比较(左上、左中、左下、右上等)。可以按照顺时针或者逆时针的顺序进行比较。
- 对于中心像素大于某个邻域的,设置为1;否则,设置为0。这就获得了一个8位的二进制数(通常情况下会转换为十进制数字),作为该位置的特征。
- 对每一个区块计算直方图。
- 此时,可以选择将直方图归一化;
- 串联所有区块的直方图,这就得到了当前检测窗口的特征向量。
(3)LBP扩展与多尺度表达
基本的LBP算子只局限在$3\times3$的邻域内,对于较大图像大尺度的结构不能很好的提取需要的纹理特征,因此研究者们对LBP算子进行了扩展。新的LBP算子LBP(P,R) 可以计算不同半径邻域大小和不同像素点数的特征值,其中P表示周围像素点个数,R表示邻域半径,同时把原来的方形邻域扩展到了圆形,下图给出了四种扩展后的LBP例子,其中,R可以是小数,对于没有落到整数位置的点,根据轨道内离其最近的两个整数位置像素灰度值,利用双线性差值的方法可以计算它的灰度值。
(4)LBP统一模式
基本地LBP算子可以产生不同的二进制模式,对于半径为R的圆形区域内含有P个采样点的LBP算子将会产生$P^2$种模式。很显然,随着邻域集内采样点数的增加,二进制模式的种类是急剧增加的。统一模式就是一个二进制序列从0到1或是从1到0的变过不超过2次(这个二进制序列首尾相连)。比如:10100000的变化次数为3次所以不是一个uniform pattern。所有的8位二进制数中共有58个uniform pattern。为什么要提出这么个uniform LBP呢,例如:5×5邻域内20个采样点,有$2^20$=1,048,576种二进制模式。如此多的二值模式无论对于纹理的提取还是对于纹理的识别、分类及信息的存取都是不利的。同时,过多的模式种类对于纹理的表达是不利的。例如,将LBP算子用于纹理分类或人脸识别时,常采用LBP模式的统计直方图来表达图像的信息,而较多的模式种类将使得数据量过大,且直方图过于稀疏。因此,需要对原始的LBP模式进行降维,使得数据量减少的情况下能最好的代表图像的信息。
为了解决二进制模式过多的问题,提高统计性,Ojala提出了采用一种“等价模式”(Uniform Pattern)来对LBP算子的模式种类进行降维。Ojala等认为,在实际图像中,绝大多数LBP模式最多只包含两次从1到0或从0到1的跳变。因此,Ojala将“等价模式”定义为:当某个LBP所对应的循环二进制数从0到1或从1到0最多有两次跳变时,该LBP所对应的二进制就称为一个等价模式类。如00000000(0次跳变),00000111(只含一次从0到1的跳变),10001111(先由1跳到0,再由0跳到1,共两次跳变)都是等价模式类。除等价模式类以外的模式都归为另一类,称为混合模式类,例如10010111(共四次跳变)。通过这样的改进,二进制模式的种类大大减少,而不会丢失任何信息。模式数量由原来的2P种减少为 P ( P-1)+2种,其中P表示邻域集内的采样点数。对于3×3邻域内8个采样点来说,二进制模式由原始的256种减少为58种,即:它把值分为59类,58个uniform pattern为一类,其它的所有值为第59类。这样直方图从原来的256维变成59维。这使得特征向量的维数更少,并且可以减少高频噪声带来的影响。
#include <opencv2/opencv.hpp>
#include <iostream>
#include "math.h"
using namespace cv;
using namespace std;
Mat src, gray_src;
int current_radius = 3;
int max_count = 20;
void ELBP_Demo(int, void*);
int main(int argc, char** argv) {
src = imread("C:/images/lena_full.jpg");
if (src.empty()) {
return -1;
}
const char* output_tt = "LBP Result";
namedWindow("input image", CV_WINDOW_AUTOSIZE);
namedWindow(output_tt, CV_WINDOW_AUTOSIZE);
imshow("input image", src);
// convert to gray
cvtColor(src, gray_src, COLOR_BGR2GRAY);
int width = gray_src.cols;
int height = gray_src.rows;
// 基本LBP演示
Mat lbpImage = Mat::zeros(gray_src.rows - 2, gray_src.cols - 2, CV_8UC1);
for (int row = 1; row < height - 1; row++) {
for (int col = 1; col < width - 1; col++) {
uchar c = gray_src.at<uchar>(row, col);
uchar code = 0;
code |= (gray_src.at<uchar>(row - 1, col - 1) > c) << 7;
code |= (gray_src.at<uchar>(row - 1, col) > c) << 6;
code |= (gray_src.at<uchar>(row - 1, col + 1) > c) << 5;
code |= (gray_src.at<uchar>(row, col + 1) > c) << 4;
code |= (gray_src.at<uchar>(row + 1, col + 1) > c) << 3;
code |= (gray_src.at<uchar>(row + 1, col) > c) << 2;
code |= (gray_src.at<uchar>(row + 1, col - 1) > c) << 1;
code |= (gray_src.at<uchar>(row, col - 1) > c) << 0;
lbpImage.at<uchar>(row - 1, col - 1) = code;
}
}
imshow(output_tt, lbpImage);
// ELBP 演示
namedWindow("ELBP Result", CV_WINDOW_AUTOSIZE);
createTrackbar("ELBP Radius:", "ELBP Result", ¤t_radius, max_count, ELBP_Demo);
ELBP_Demo(0, 0);
waitKey(0);
return 0;
}
void ELBP_Demo(int, void*) {
int offset = current_radius * 2;
Mat elbpImage = Mat::zeros(gray_src.rows - offset, gray_src.cols - offset, CV_8UC1);
int width = gray_src.cols;
int height = gray_src.rows;
int numNeighbors = 8;
for (int n = 0; n < numNeighbors; n++) {
float x = static_cast<float>(current_radius) * cos(2.0 * CV_PI*n / static_cast<float>(numNeighbors));
float y = static_cast<float>(current_radius) * -sin(2.0 * CV_PI*n / static_cast<float>(numNeighbors));
int fx = static_cast<int>(floor(x));
int fy = static_cast<int>(floor(y));
int cx = static_cast<int>(ceil(x));
int cy = static_cast<int>(ceil(y));
float ty = y - fy;
float tx = x - fx;
float w1 = (1 - tx)*(1 - ty);
float w2 = tx * (1 - ty);
float w3 = (1 - tx)* ty;
float w4 = tx * ty;
for (int row = current_radius; row < (height - current_radius); row++) {
for (int col = current_radius; col < (width - current_radius); col++) {
float t = w1 * gray_src.at<uchar>(row + fy, col + fx) + w2 * gray_src.at<uchar>(row + fy, col + cx) +
w3 * gray_src.at<uchar>(row + cy, col + fx) + w4 * gray_src.at<uchar>(row + cy, col + cx);
elbpImage.at<uchar>(row - current_radius, col - current_radius) +=
((t > gray_src.at<uchar>(row, col)) && (abs(t - gray_src.at<uchar>(row, col)) > std::numeric_limits<float>::epsilon())) << n;
}
}
}
imshow("ELBP Result", elbpImage);
return;
}
10. 积分图计算
(1)积分图计算介绍
(2)相关API
void integral(InputArray image,
OutputArray sum,
OutputArray sqsum,
OutputArray tilted,
int sdepth=-1 )
- image 输入W×H源图像,8bit字符型,或32bit、64bit浮点型矩阵
- sum 输出(W+1)×(H +1)积分图像,32bit整型或32bit、64bit浮点型矩阵
- sqsum 输出(W+1)×(H +1)平方积分图像,双精度浮点型矩阵。
- tilted 输出旋转45°的(W+1)×(H +1)积分图像,数据类型同sum
- sdepth 积分图像sum或titled的位深度:CV_32S、CV_32F或CV_64F
(3)代码演示
#include <opencv2/opencv.hpp>
#include <iostream>
using namespace cv;
int main(int argc, char** argv) {
Mat src = imread("C:/images/lena_full.jpg", IMREAD_GRAYSCALE);
if (src.empty()) {
return -1;
}
namedWindow("input image", CV_WINDOW_AUTOSIZE);
imshow("input image", src);
Mat sum = Mat::zeros(src.rows + 1, src.cols + 1, CV_32FC1);
Mat sqsum = Mat::zeros(src.rows + 1, src.cols + 1, CV_64FC1);
integral(src, sum, sqsum);
Mat result;
normalize(sum, result, 0, 255, NORM_MINMAX, CV_8UC1, Mat());
imshow("Integral Image", result);
waitKey(0);
return 0;
}
11. Haar特征
(1)Harr特征介绍
Haar-like特征最早是由Papageorgiou等应用于人脸表示,Viola和Jones在此基础上,使用3种类型4种形式的特征。
Haar特征分为三类:边缘特征、线性特征、中心特征和对角线特征,组合成特征模板。特征模板内有白色和黑色两种矩形,并定义该模板的特征值为白色矩形像素和减去黑色矩形像素和。Haar特征值反映了图像的灰度变化情况。例如:脸部的一些特征能由矩形特征简单的描述,如:眼睛要比脸颊颜色要深,鼻梁两侧比鼻梁颜色要深,嘴巴比周围颜色要深等。但矩形特征只对一些简单的图形结构,如边缘、线段较敏感,所以只能描述特定走向(水平、垂直、对角)的结构。
(2)Haar特征积分图提高效率
积分图就是只遍历一次图像就可以求出图像中所有区域像素和的快速算法,大大的提高了图像特征值计算的效率。
积分图主要的思想是将图像从起点开始到各个点所形成的矩形区域像素之和作为一个数组的元素保存在内存中,当要计算某个区域的像素和时可以直接索引数组的元素,不用重新计算这个区域的像素和,从而加快了计算(这有个相应的称呼,叫做动态规划算法)。积分图能够在多种尺度下,使用相同的时间(常数时间)来计算不同的特征,因此大大提高了检测速度。
(3)Haar矩形特征拓展
Lienhart R.等对Haar-like矩形特征库作了进一步扩展,加入了旋转45。角的矩形特征。扩展后的特征大致分为4种类型:边缘特征、线特征环、中心环绕特征和对角线特征。
在特征值的计算过程中,黑色区域的权值为负值,白色区域的权值为正值。而且权值与矩形面积成反比(使两种矩形区域中像素数目一致)。
12. 特征描述子
(1)特征描述子介绍
下面用Brute-Force匹配来介绍
(2)代码演示
#include <opencv2/opencv.hpp>
#include <opencv2/xfeatures2d.hpp>
#include <iostream>
using namespace cv;
using namespace std;
using namespace cv::xfeatures2d;
int main(int argc, char** argv) {
Mat img1 = imread("C:/images/lena_full.jpg", IMREAD_GRAYSCALE);
Mat img2 = imread("C:/images/lena.jpg", IMREAD_GRAYSCALE);
if (!img1.data || !img2.data) {
return -1;
}
imshow("image1", img1);
imshow("image2", img2);
int minHessian = 400;
Ptr<SURF> detector = SURF::create(minHessian);
vector<KeyPoint> keypoints_1;
vector<KeyPoint> keypoints_2;
Mat descriptor_1, descriptor_2;
detector->detectAndCompute(img1, Mat(), keypoints_1, descriptor_1);
detector->detectAndCompute(img2, Mat(), keypoints_2, descriptor_2);
BFMatcher matcher(NORM_L2);
vector<DMatch> matches;
matcher.match(descriptor_1, descriptor_2, matches);
Mat matchesImg;
drawMatches(img1, keypoints_1, img2, keypoints_2, matches, matchesImg);
imshow("Descriptor Demo", matchesImg);
waitKey(0);
return 0;
}
13. FLANN特征匹配
(1)FLANN特征匹配介绍
FLANN是一种高效的数值或者字符串匹配算法,SIFT/SURF是基于浮点数的匹配,ORB是二值匹配,速度更快。对于FLANN匹配算法,当使用ORB匹配算法的时候,需要重新构造HASH。这个在C++的代码种做了演示。对匹配之后的输出结果,根据距离进行排序,就会得到距离比较的匹配点。
(2)代码演示
#include <opencv2/opencv.hpp>
#include <opencv2/xfeatures2d.hpp>
#include <iostream>
#include <math.h>
using namespace cv;
using namespace std;
using namespace cv::xfeatures2d;
int main(int argc, char** argv) {
Mat img1 = imread("C:/images/left01.jpg", IMREAD_GRAYSCALE);
Mat img2 = imread("C:/images/left07.jpg", IMREAD_GRAYSCALE);
if (!img1.data || !img2.data) {
return -1;
}
imshow("object image", img1);
imshow("object in scene", img2);
// surf featurs extraction
int minHessian = 400;
Ptr<SURF> detector = SURF::create(minHessian);
vector<KeyPoint> keypoints_obj;
vector<KeyPoint> keypoints_scene;
Mat descriptor_obj, descriptor_scene;
detector->detectAndCompute(img1, Mat(), keypoints_obj, descriptor_obj);
detector->detectAndCompute(img2, Mat(), keypoints_scene, descriptor_scene);
// matching
FlannBasedMatcher matcher;
vector<DMatch> matches;
matcher.match(descriptor_obj, descriptor_scene, matches);
// find good matched points
double minDist = 1000;
double maxDist = 0;
for (int i = 0; i < descriptor_obj.rows; i++) {
double dist = matches[i].distance;
if (dist > maxDist) {
maxDist = dist;
}
if (dist < minDist) {
minDist = dist;
}
}
printf("max distance : %f\n", maxDist);
printf("min distance : %f\n", minDist);
vector<DMatch> goodMatches;
for (int i = 0; i < descriptor_obj.rows; i++) {
double dist = matches[i].distance;
if (dist < max(3 * minDist, 0.02)) {
goodMatches.push_back(matches[i]);
}
}
Mat matchesImg;
drawMatches(img1, keypoints_obj, img2, keypoints_scene, goodMatches, matchesImg, Scalar::all(-1),
Scalar::all(-1), vector<char>(), DrawMatchesFlags::NOT_DRAW_SINGLE_POINTS
);
imshow("Flann Matching Result", matchesImg);
waitKey(0);
return 0;
}
14. 平面对象识别
(1)对象形变与位置变换
待检测图像样本一般与检测样本中的实际状态在形态学上会有不同,例如一本书会发生扭曲。这时,我们就要进行对象形变与位置变换。
(2)API介绍
Mat cv::findHomography (InputArray srcPoints,
InputArray dstPoints,
int method = 0,
double ransacReprojThreshold = 3,
OutputArray mask = noArray(),
const int maxIters = 2000,
const double confidence = 0.995
)
- srcPoints 源平面中点的坐标矩阵,可以是CV_32FC2类型,也可以是vector<Point2f>类型
- dstPoints目标平面中点的坐标矩阵,可以是CV_32FC2类型,也可以是vector<Point2f>类型
- method 计算单应矩阵所使用的方法。不同的方法对应不同的参数,具体如下:
0 - 利用所有点的常规方法
RANSAC - RANSAC - 基于RANSAC的鲁棒算法
LMEDS - 最小中值鲁棒算法
RHO - 基于PROSAC的鲁棒算法 - ransacReprojThreshold 将点对视为内点的最大允许重投影错误阈值(仅用于RANSAC和RHO方法)。如果
则点i被认为是个外点(即错误匹配点对)。若srcPoints和dstPoints是以像素为单位的,则该参数通常设置在1到10的范围内。
- mask 可选输出掩码矩阵,通常由鲁棒算法(RANSAC或LMEDS)设置。 请注意,输入掩码矩阵是不需要设置的。
- maxIters RANSAC算法的最大迭代次数,默认值为2000。
- confidence 可信度值,取值范围为0到1.
(3)代码演示
#include <opencv2/opencv.hpp>
#include <opencv2/xfeatures2d.hpp>
#include <iostream>
#include <math.h>
using namespace cv;
using namespace std;
using namespace cv::xfeatures2d;
int main(int argc, char** argv) {
Mat img1 = imread("C:/images/left01.jpg", IMREAD_GRAYSCALE);
Mat img2 = imread("C:/images/left07.jpg", IMREAD_GRAYSCALE);
if (!img1.data || !img2.data) {
return -1;
}
imshow("object image", img1);
imshow("object in scene", img2);
// surf featurs extraction
int minHessian = 400;
Ptr<SURF> detector = SURF::create(minHessian);
vector<KeyPoint> keypoints_obj;
vector<KeyPoint> keypoints_scene;
Mat descriptor_obj, descriptor_scene;
detector->detectAndCompute(img1, Mat(), keypoints_obj, descriptor_obj);
detector->detectAndCompute(img2, Mat(), keypoints_scene, descriptor_scene);
// matching
FlannBasedMatcher matcher;
vector<DMatch> matches;
matcher.match(descriptor_obj, descriptor_scene, matches);
// find good matched points
double minDist = 1000;
double maxDist = 0;
for (int i = 0; i < descriptor_obj.rows; i++) {
double dist = matches[i].distance;
if (dist > maxDist) {
maxDist = dist;
}
if (dist < minDist) {
minDist = dist;
}
}
printf("max distance : %f\n", maxDist);
printf("min distance : %f\n", minDist);
vector<DMatch> goodMatches;
for (int i = 0; i < descriptor_obj.rows; i++) {
double dist = matches[i].distance;
if (dist < max(3 * minDist, 0.02)) {
goodMatches.push_back(matches[i]);
}
}
Mat matchesImg;
drawMatches(img1, keypoints_obj, img2, keypoints_scene, goodMatches, matchesImg, Scalar::all(-1),
Scalar::all(-1), vector<char>(), DrawMatchesFlags::NOT_DRAW_SINGLE_POINTS
);
vector<Point2f> obj;
vector<Point2f> objInScene;
for (size_t t = 0; t < goodMatches.size(); t++) {
obj.push_back(keypoints_obj[goodMatches[t].queryIdx].pt);
objInScene.push_back(keypoints_scene[goodMatches[t].trainIdx].pt);
}
Mat H = findHomography(obj, objInScene, RANSAC);
// 第一张图四个角点
vector<Point2f> obj_corners(4);
obj_corners[0] = Point(0, 0);
obj_corners[1] = Point(img1.cols, 0);
obj_corners[2] = Point(img1.cols, img1.rows);
obj_corners[3] = Point(0, img1.rows);
// 第二张图四个角点
vector<Point2f> scene_corners(4);
// 进行透视变换
perspectiveTransform(obj_corners, scene_corners, H);
// draw line
line(matchesImg, scene_corners[0] + Point2f(img1.cols, 0), scene_corners[1] + Point2f(img1.cols, 0), Scalar(0, 0, 255), 2, 8, 0);
line(matchesImg, scene_corners[1] + Point2f(img1.cols, 0), scene_corners[2] + Point2f(img1.cols, 0), Scalar(0, 0, 255), 2, 8, 0);
line(matchesImg, scene_corners[2] + Point2f(img1.cols, 0), scene_corners[3] + Point2f(img1.cols, 0), Scalar(0, 0, 255), 2, 8, 0);
line(matchesImg, scene_corners[3] + Point2f(img1.cols, 0), scene_corners[0] + Point2f(img1.cols, 0), Scalar(0, 0, 255), 2, 8, 0);
Mat dst;
cvtColor(img2, dst, COLOR_GRAY2BGR);
line(dst, scene_corners[0], scene_corners[1], Scalar(0, 0, 255), 2, 8, 0);
line(dst, scene_corners[1], scene_corners[2], Scalar(0, 0, 255), 2, 8, 0);
line(dst, scene_corners[2], scene_corners[3], Scalar(0, 0, 255), 2, 8, 0);
line(dst, scene_corners[3], scene_corners[0], Scalar(0, 0, 255), 2, 8, 0);
imshow("find known object demo", matchesImg);
imshow("Draw object", dst);
waitKey(0);
return 0;
}
15. AKAZE局部匹配
(1)AKAZE局部匹配介绍
- AOS构造尺度空间
- Hessian矩阵特征点检测
- 方向指定基于一阶微分图像
- 描述子生成
(2)与SIFT/SURF比较
- 更加稳定
- 非线性尺度空间
- AKAZE速度更加快
- 比较新的算法,只有在opencv新版本中才有
- KAZE是日语音译过来的 , KAZE与SIFT、SURF最大的区别在于构造尺度空间,KAZE是利用非线性方式构造,得到的关键点也就更准确(尺度不变性 )
(3)代码演示
#include <opencv2/opencv.hpp>
#include <iostream>
#include <math.h>
using namespace cv;
using namespace std;
int main(int argc, char** argv) {
Mat img1 = imread("C:/images/lena.jpg", IMREAD_GRAYSCALE);
Mat img2 = imread("C:/images/lena_full.jpg", IMREAD_GRAYSCALE);
if (img1.empty() || img2.empty()) {
printf("could not load images...\n");
return -1;
}
imshow("box image", img1);
imshow("scene image", img2);
// extract akaze features
Ptr<AKAZE> detector = AKAZE::create();
vector<KeyPoint> keypoints_obj;
vector<KeyPoint> keypoints_scene;
Mat descriptor_obj, descriptor_scene;
double t1 = getTickCount();
detector->detectAndCompute(img1, Mat(), keypoints_obj, descriptor_obj);
detector->detectAndCompute(img2, Mat(), keypoints_scene, descriptor_scene);
double t2 = getTickCount();
double tkaze = 1000 * (t2 - t1) / getTickFrequency();
printf("AKAZE Time consume(ms) : %f\n", tkaze);
// matching
FlannBasedMatcher matcher(new flann::LshIndexParams(20, 10, 2));
// FlannBasedMatcher matcher;
vector<DMatch> matches;
matcher.match(descriptor_obj, descriptor_scene, matches);
// draw matches(key points)
Mat akazeMatchesImg;
drawMatches(img1, keypoints_obj, img2, keypoints_scene, matches, akazeMatchesImg);
imshow("akaze match result", akazeMatchesImg);
vector<DMatch> goodMatches;
double minDist = 100000, maxDist = 0;
for (int i = 0; i < descriptor_obj.rows; i++) {
double dist = matches[i].distance;
if (dist < minDist) {
minDist = dist;
}
if (dist > maxDist) {
maxDist = dist;
}
}
printf("min distance : %f", minDist);
for (int i = 0; i < descriptor_obj.rows; i++) {
double dist = matches[i].distance;
if (dist < max( 1.5*minDist, 0.02)) {
goodMatches.push_back(matches[i]);
}
}
drawMatches(img1, keypoints_obj, img2, keypoints_scene, goodMatches, akazeMatchesImg, Scalar::all(-1),
Scalar::all(-1), vector<char>(), DrawMatchesFlags::NOT_DRAW_SINGLE_POINTS);
imshow("good match result", akazeMatchesImg);
waitKey(0);
return 0;
}
16. Brisk特征检测与匹配
(1)Brisk特征检测介绍
- 构建尺度空间
- FAST9-16寻找特征点
- 特征点定位
- 关键点描述子
(2)代码演示
#include <opencv2/opencv.hpp>
#include <iostream>
using namespace cv;
using namespace std;
int main(int argc, char** argv) {
Mat img1 = imread("C:/images/lena.jpg", IMREAD_GRAYSCALE);
Mat img2 = imread("C:/images/lena_full.jpg", IMREAD_GRAYSCALE);
if (img1.empty() || img2.empty()) {
printf("could not load images...\n");
return -1;
}
imshow("box image", img1);
imshow("scene image", img2);
// extract BRISK features
Ptr<Feature2D> detector = BRISK::create();
vector<KeyPoint> keypoints_obj;
vector<KeyPoint> keypoints_scene;
Mat descriptor_obj, descriptor_scene;
detector->detectAndCompute(img1, Mat(), keypoints_obj, descriptor_obj);
detector->detectAndCompute(img2, Mat(), keypoints_scene, descriptor_scene);
// matching
BFMatcher matcher(NORM_L2);
vector<DMatch> matches;
matcher.match(descriptor_obj, descriptor_scene, matches);
// search good matches
vector<DMatch> goodMatches;
double minDist = 100000, maxDist = 0;
for (int i = 0; i < descriptor_obj.rows; i++) {
double dist = matches[i].distance;
if (dist < minDist) {
minDist = dist;
}
if (dist > maxDist) {
maxDist = dist;
}
}
printf("min distance : %f", minDist);
for (int i = 0; i < descriptor_obj.rows; i++) {
double dist = matches[i].distance;
if (dist < max(minDist * 3, 0.02)) {
goodMatches.push_back(matches[i]);
}
}
// draw matches
Mat matchesImg;
drawMatches(img1, keypoints_obj, img2, keypoints_scene, goodMatches, matchesImg);
imshow("BRISK MATCH RESULT", matchesImg);
// draw key points
// Mat resultImg;
// drawKeypoints(src, keypoints, resultImg, Scalar::all(-1), DrawMatchesFlags::DEFAULT);
// imshow("Brisk Key Points", resultImg);
waitKey(0);
return 0;
}
17. 级联分类器——人脸检测
(1)检测基本原理
(2)弱分类
(3)代码演示
#include <opencv2/opencv.hpp>
#include <iostream>
using namespace cv;
using namespace std;
int main(int argc, char** argv) {
String cascadeFilePath = "C:/Tony/opencv/build/etc/haarcascades/haarcascade_frontalface_alt.xml";
CascadeClassifier face_cascade;
if (!face_cascade.load(cascadeFilePath)) {
printf("could not load haar data...\n");
return -1;
}
Mat src, gray_src;
src = imread("C:/images/lena_full.jpg");
cvtColor(src, gray_src, COLOR_BGR2GRAY);
equalizeHist(gray_src, gray_src);
imshow("input image", src);
vector<Rect> faces;
face_cascade.detectMultiScale(gray_src, faces, 1.1, 2, 0, Size(30, 30));
for (size_t t = 0; t < faces.size(); t++) {
rectangle(src, faces[t], Scalar(0, 0, 255), 2, 8, 0);
}
namedWindow("output", CV_WINDOW_AUTOSIZE);
imshow("output", src);
waitKey(0);
return 0;
}
百度未收录