说明:本文旨在给出 SIFT 算法的具体实现,而在 SIFT 详解上只是做出简单介绍,在这里可以给大家推荐一篇好文:https://blog.csdn.net/zddblog/article/details/7521424;结合这篇文章和下文的具体代码实现,我相信你能很快掌握并运用 SIFT 算法,加油哦!!!如有疑问大家可以一起讨论学习!!!
一、SIFT算法简介
SIFT,即尺度不变特征变换(Scale-invariant feature transform,SIFT),是用于图像处理领域的一种描述。这种描述具有尺度不变性,可在图像中检测出关键点,是一种局部特征描述子。该方法于1999年由David Lowe 首先发表于计算机视觉国际会议(International Conference on Computer Vision,ICCV),2004年再次经David Lowe整理完善后发表于International journal of computer vision(IJCV)。截止2014年8月,该论文单篇被引次数达25000余次。
1、SIFT算法的特点
(1) SIFT特征是图像的局部特征,其对旋转、尺度缩放、亮度变化保持不变性,对视角变化、仿射变换、噪声也保持一定程度的稳定性;
(2)独特性(Distinctiveness)好,信息量丰富,适用于在海量特征数据库中进行快速、准确的匹配;
(3)多量性,即使少数的几个物体也可以产生大量的SIFT特征向量;
(4)高速性,经优化的SIFT匹配算法甚至可以达到实时的要求;
(5)可扩展性,可以很方便的与其他形式的特征向量进行联合。
2、SIFT算法可以解决的问题
目标的自身状态、场景所处的环境和成像器材的成像特性等因素影响图像配准/目标识别跟踪的性能。而SIFT算法在一定程度上可解决:
(1)目标的旋转、缩放、平移(RST)
(2)图像仿射/投影变换(视点viewpoint)
(3)光照影响(illumination)
(4)目标遮挡(occlusion)
(5)杂物场景(clutter)
(6)噪声
SIFT算法的实质是在不同的尺度空间上查找关键点(特征点),并计算出关键点的方向。SIFT所查找到的关键点是一些十分突出,不会因光照,仿射变换和噪音等因素而变化的点,如角点、边缘点、暗区的亮点及亮区的暗点等。
二、SIFT算法分为如下四步
1. 尺度空间极值检测
搜索所有尺度上的图像位置。通过高斯微分函数来识别潜在的对于尺度和旋转不变的兴趣点。
2. 关键点定位
在每个候选的位置上,通过一个拟合精细的模型来确定位置和尺度。关键点的选择依据于它们的稳定程度。
3. 方向确定
基于图像局部的梯度方向,分配给每个关键点位置一个或多个方向。所有后面的对图像数据的操作都相对于关键点的方向、尺度和位置进行变换,从而提供对于这些变换的不变性。
4. 关键点描述
在每个关键点周围的邻域内,在选定的尺度上测量图像局部的梯度。这些梯度被变换成一种表示,这种表示允许比较大的局部形状的变形和光照变化。
三、SIFT具体实现
说明:代码实现主要有七个源文件,我和大家一样有时候下载了好几个资源然而没有一个能用的,导致最后都不知道要下载哪一个、是否要下载;所以这里直接把代码贴出来了!
1、mySift.h
#pragma once //防止头文件重复包含和下面作用一样 #include<iostream> #include<vector> #include<opencv2\core\core.hpp> #include<opencv2\features2d\features2d.hpp> using namespace std; using namespace cv; /*************************定义常量*****************************/ //高斯核大小和标准差关系,size=2*(GAUSS_KERNEL_RATIO*sigma)+1,经常设置GAUSS_KERNEL_RATIO=2-3之间 const double GAUSS_KERNEL_RATIO = 3; const int MAX_OCTAVES = 8; //金字塔最大组数 const float CONTR_THR = 0.04f; //默认是的对比度阈值(D(x)) const float CURV_THR = 10.0f; //关键点主曲率阈值 const float INIT_SIGMA = 0.5f; //输入图像的初始尺度 const int IMG_BORDER = 2; //图像边界忽略的宽度,也可以改为 1 const int MAX_INTERP_STEPS = 5; //关键点精确插值次数 const int ORI_HIST_BINS = 36; //计算特征点方向直方图的BINS个数 const float ORI_SIG_FCTR = 1.5f; //计算特征点主方向时候,高斯窗口的标准差因子 const float ORI_RADIUS = 3 * ORI_SIG_FCTR; //计算特征点主方向时,窗口半径因子 const float ORI_PEAK_RATIO = 0.8f; //计算特征点主方向时,直方图的峰值比 const int DESCR_WIDTH = 4; //描述子直方图的网格大小(4x4) const int DESCR_HIST_BINS = 8; //每个网格中直方图角度方向的维度 const float DESCR_MAG_THR = 0.2f; //描述子幅度阈值 const float DESCR_SCL_FCTR = 3.0f; //计算描述子时,每个网格的大小因子 const int SAR_SIFT_GLOH_ANG_GRID = 8; //GLOH网格沿角度方向等分区间个数 const int SAR_SIFT_DES_ANG_BINS = 8; //像素梯度方向在0-360度内等分区间个数 const float SAR_SIFT_RADIUS_DES = 12.0f; //描述子邻域半径 const int Mmax = 8; //像素梯度方向在0-360度内等分区间个数 const double T = 100.0; //sobel算子去除冗余特征点的阈值 const float SAR_SIFT_GLOH_RATIO_R1_R2 = 0.73f;//GLOH网格中间圆半径和外圆半径之比 const float SAR_SIFT_GLOH_RATIO_R1_R3 = 0.25f;//GLOH网格最内层圆半径和外圆半径之比 #define Feature_Point_Minimum 1500 //输入图像特征点最小个数 #define We 0.2 #define Wn 0.5 #define Row_num 3 #define Col_num 3 #define SIFT_FIXPT_SCALE 48 //不理解,后面可查原论文 /************************sift类*******************************/ class mySift { public: //默认构造函数 mySift(int nfeatures = 0, int nOctaveLayers = 3, double contrastThreshold = 0.03, double edgeThreshold = 10, double sigma = 1.6, bool double_size = true) :nfeatures(nfeatures), nOctaveLayers(nOctaveLayers), contrastThreshold(contrastThreshold), edgeThreshold(edgeThreshold), sigma(sigma), double_size(double_size) {} //获得尺度空间每组中间层数 int get_nOctave_layers() { return nOctaveLayers; } //获得图像尺度是否扩大一倍 bool get_double_size() { return double_size; } //计算金字塔组数 int num_octaves(const Mat& image); //生成高斯金字塔第一组,第一层图像 void create_initial_image(const Mat& image, Mat& init_image); //使用 sobel 算子创建高斯金字塔第一层图像,以减少冗余特征点 void sobel_create_initial_image(const Mat& image, Mat& init_image); //创建高斯金字塔 void build_gaussian_pyramid(const Mat& init_image, vector<vector<Mat>>& gauss_pyramid, int nOctaves); //创建高斯差分金字塔 void build_dog_pyramid(vector<vector<Mat>>& dog_pyramid, const vector<vector<Mat>>& gauss_pyramid); //该函数生成高斯差分金字塔 void amplit_orient(const Mat& image, vector<Mat>& amplit, vector<Mat>& orient, float scale, int nums); //DOG金字塔特征点检测 void find_scale_space_extrema(const vector<vector<Mat>>& dog_pyr, const vector<vector<Mat>>& gauss_pyr, vector<KeyPoint>& keypoints); //DOG金字塔特征点检测,特征点方向细化版 void find_scale_space_extrema1(const vector<vector<Mat>>& dog_pyr, vector<vector<Mat>>& gauss_pyr, vector<KeyPoint>& keypoints); //计算特征点的描述子 void calc_sift_descriptors(const vector<vector<Mat>>& gauss_pyr, vector<KeyPoint>& keypoints, Mat& descriptors, const vector<Mat>& amplit, const vector<Mat>& orient); //构建空间尺度—主要是为了获取 amplit 和 orient 在使用 GLOH 描述子的时候使用 void build_sar_sift_space(const Mat& image, vector<Mat>& sar_harris_fun, vector<Mat>& amplit, vector<Mat>& orient); //GLOH 计算一个特征描述子 void calc_gloh_descriptors(const vector<Mat>& amplit, const vector<Mat>& orient, const vector<KeyPoint>& keys, Mat& descriptors); //特征点检测 void detect(const Mat& image, vector<vector<Mat>>& gauss_pyr, vector<vector<Mat>>& dog_pyr, vector<KeyPoint>& keypoints, vector<vector<vector<float>>>& all_cell_contrasts, vector<vector<float>>& average_contrast, vector<vector<int>>& n_cells, vector<int>& num_cell, vector<vector<int>>& available_n, vector<int>& available_num, vector<KeyPoint>& final_keypoints, vector<KeyPoint>& Final_keypoints, vector<KeyPoint>& Final_Keypoints); //特征点描述 void comput_des(const vector<vector<Mat>>& gauss_pyr, vector<KeyPoint>& final_keypoints, const vector<Mat>& amplit, const vector<Mat>& orient, Mat& descriptors); private: int nfeatures; //设定检测的特征点的个数值,如果此值设置为0,则不影响结果 int nOctaveLayers; //每组金字塔中间层数 double contrastThreshold; //对比度阈值(D(x)) double edgeThreshold; //特征点边缘曲率阈值 double sigma; //高斯尺度空间初始层的尺度 bool double_size; //是否上采样原始图像 };//注意类结束的分号
2、mySift.cpp
#include"mySift.h" #include<string> #include<cmath> #include<iostream> //输入输出 #include<vector> //vector #include<algorithm> #include<numeric> //用于容器元素求和 #include<opencv2\core\hal\hal.hpp> #include<opencv2\core\hal\intrin.hpp> #include<opencv2\opencv.hpp> #include<opencv2\core\core.hpp> //opencv基本数据结构 #include<opencv2\highgui\highgui.hpp> //图像界面 #include<opencv2\imgproc\imgproc.hpp> //基本图像处理函数 #include<opencv2\features2d\features2d.hpp> //特征提取 #include<opencv2\imgproc\types_c.h> #include<opencv2\videoio.hpp> #include <highgui\highgui.hpp> #include <imgproc\imgproc.hpp> /******************根据输入图像大小计算高斯金字塔的组数****************************/ /*image表示原始输入灰度图像,inline函数必须在声明处定义 double_size_image表示是否在构建金字塔之前上采样原始图像 */ int mySift::num_octaves(const Mat& image) { int temp; float size_temp = (float)min(image.rows, image.cols); temp = cvRound(log(size_temp) / log((float)2) - 2); if (double_size) temp += 1; if (temp > MAX_OCTAVES) //尺度空间最大组数设置为MAX_OCTAVES temp = MAX_OCTAVES; return temp; } /************************sobel滤波器计算高斯尺度空间图像梯度大小*****************************/ void sobelfilter(Mat& image, Mat& G) { // image 是经过归一化后的数据 (0,1) int rows = image.rows; int cols = image.cols; float dx = 0.0, dy = 0.0; //cv::Mat Gx = cv::Mat::zeros(rows, cols, CV_32FC1); //包含了图像像素在水平方向上的导数的近似值的图像 //cv::Mat Gy = cv::Mat::zeros(rows, cols, CV_32FC1); //包含了图像像素在垂直方向上的导数的近似值的图像 G = Mat::zeros(rows, cols, CV_32FC1); //在每个像素点处的灰度大小由Gx和Gy共同决定 double v = 0.0, vx, vy; //利用 sobel 算子求梯度幅度图像 for (int i = 0; i < rows; i++) { for (int j = 0; j < cols; j++) { v = 0.0; if (i == 0 || j == 0 || i == rows - 1 || j == cols - 1) { G.at<float>(i, j) = 0.0; } else { float dx = image.at<float>(i - 1, j + 1) - image.at<float>(i - 1, j - 1) + 2 * image.at<float>(i, j + 1) - 2 * image.at<float>(i, j - 1) + image.at<float>(i + 1, j + 1) - image.at<float>(i + 1, j - 1); float dy = image.at<float>(i + 1, j - 1) - image.at<float>(i - 1, j - 1) + 2 * image.at<float>(i + 1, j) - 2 * image.at<float>(i - 1, j) + image.at<float>(i + 1, j + 1) - image.at<float>(i - 1, j + 1); v = abs(dx) + abs(dy); //简化后 G = |Gx| + |Gy| //保证像素值在有效范围内 v = fmax(v, 0); //返回浮点数中较大的一个 v = fmin(v, 255); //返回浮点数中较小的一个 if (v > T) //T为阈值等于50 G.at<float>(i, j) = (float)v; else G.at<float>(i, j) = 0.0; } } } //水平方向上的导数的近似值的图像 /*for (int i = 0; i < rows; i++) { for (int j = 0; j < cols; j++) { vx = 0; if (i == 0 || j == 0 || i == rows - 1 || j == cols - 1) Gx.at<float>(i, j) = 0; else { dx = image.at<float>(i - 1, j + 1) - image.at<float>(i - 1, j - 1) + 2 * image.at<float>(i, j + 1) - 2 * image.at<float>(i, j - 1) + image.at<float>(i + 1, j + 1) - image.at<float>(i + 1, j - 1); vx = abs(dx); vx = fmax(vx, 0); vx = fmin(vx, 255); Gx.at<float>(i, j) = (float)vx; } } }*/ //垂直方向上的导数的近似值的图像 /*for (int i = 0; i < rows; i++) { for (int j = 0; j < cols; j++) { vy = 0; if (i == 0 || j == 0 || i == rows - 1 || j == cols - 1) Gy.at<float>(i, j) = 0; else { dy = image.at<float>(i + 1, j - 1) - image.at<float>(i - 1, j - 1) + 2 * image.at<float>(i + 1, j) - 2 * image.at<float>(i - 1, j) + image.at<float>(i + 1, j + 1) - image.at<float>(i - 1, j + 1); vy = abs(dy); vy = fmax(vy, 0); vx = fmin(vy, 255); Gy.at<float>(i, j) = (float)vy; } } }*/ //cv::imshow("Gx", Gx); // horizontal //cv::imshow("Gy", Gy); // vertical //cv::imshow("G", G); // gradient } /*********该函数根据尺度和窗口半径生成ROEWA滤波模板************/ /*size表示核半径,因此核宽度是2*size+1 scale表示指数权重参数 kernel表示生成的滤波核 */ static void roewa_kernel(int size, float scale, Mat& kernel) { kernel.create(2 * size + 1, 2 * size + 1, CV_32FC1); for (int i = -size; i <= size; ++i) { float* ptr_k = kernel.ptr<float>(i + size); for (int j = -size; j <= size; ++j) { ptr_k[j + size] = exp(-1.f * (abs(i) + abs(j)) / scale); } } } /************************创建高斯金字塔第一组,第一层图像************************************/ /*image表示输入原始图像 init_image表示生成的高斯尺度空间的第一层图像 */ void mySift::create_initial_image(const Mat& image, Mat& init_image) { Mat gray_image; if (image.channels() != 1) cvtColor(image, gray_image, CV_RGB2GRAY); //转换为灰度图像 else gray_image = image.clone(); Mat floatImage; //转换到0-1之间的浮点类型数据归一化,方便接下来的处理 //float_image=(float)gray_image*(1.0/255.0) gray_image.convertTo(floatImage, CV_32FC1, 1.0 / 255.0, 0); double sig_diff = 0; if (double_size) { Mat temp_image; //通过插值的方法改变图像尺寸的大小 resize(floatImage, temp_image, Size(2 * floatImage.cols, 2 * floatImage.rows), 0.0, 0.0, INTER_LINEAR); //高斯平滑的标准差,值较大时平滑效果比较明显 sig_diff = sqrt(sigma * sigma - 4.0 * INIT_SIGMA * INIT_SIGMA); //高斯滤波窗口大小选择很重要,这里选择(4*sig_diff_1+1)-(6*sig_diff+1)之间,且四舍五入 int kernel_width = 2 * cvRound(GAUSS_KERNEL_RATIO * sig_diff) + 1; Size kernel_size(kernel_width, kernel_width); //对图像进行平滑处理(高斯模糊),即降低图像的分辨率,高斯模糊是实现尺度变换的唯一变换核,并其实唯一的线性核 GaussianBlur(temp_image, init_image, kernel_size, sig_diff, sig_diff); } else { sig_diff = sqrt(sigma * sigma - 1.0 * INIT_SIGMA * INIT_SIGMA); //高斯滤波窗口大小选择很重要,这里选择(4*sig_diff_1+1)-(6*sig_diff+1)之间 int kernel_width = 2 * cvRound(GAUSS_KERNEL_RATIO * sig_diff) + 1; Size kernel_size(kernel_width, kernel_width); GaussianBlur(floatImage, init_image, kernel_size, sig_diff, sig_diff); } } /************************使用 sobel 算子创建高斯金字塔第一组,第一层图像****************************/ //目的是为了减少冗余特征点 void mySift::sobel_create_initial_image(const Mat& image, Mat& init_image) { Mat gray_image, gray_images; //gray_images用于存放经过sobel算子操作后的图像 if (image.channels() != 1) cvtColor(image, gray_image, CV_RGB2GRAY); //转换为灰度图像 else gray_image = image.clone(); sobelfilter(gray_image, gray_images); Mat floatImage; //转换到0-1之间的浮点类型数据归一化,方便接下来的处理 //float_image=(float)gray_image*(1.0/255.0) gray_images.convertTo(floatImage, CV_32FC1, 1.0 / 255.0, 0); double sig_diff = 0; if (double_size) { Mat temp_image; //通过插值的方法改变图像尺寸的大小 resize(floatImage, temp_image, Size(2 * floatImage.cols, 2 * floatImage.rows), 0.0, 0.0, INTER_LINEAR); //高斯平滑的标准差,值较大时平滑效果比较明显 sig_diff = sqrt(sigma * sigma - 4.0 * INIT_SIGMA * INIT_SIGMA); //高斯滤波窗口大小选择很重要,这里选择(4*sig_diff_1+1)-(6*sig_diff+1)之间,且四舍五入 int kernel_width = 2 * cvRound(GAUSS_KERNEL_RATIO * sig_diff) + 1; Size kernel_size(kernel_width, kernel_width); //对图像进行平滑处理(高斯模糊),即降低图像的分辨率,高斯模糊是实现尺度变换的唯一变换核,并其实唯一的线性核 GaussianBlur(temp_image, init_image, kernel_size, sig_diff, sig_diff); } else { sig_diff = sqrt(sigma * sigma - 1.0 * INIT_SIGMA * INIT_SIGMA); //高斯滤波窗口大小选择很重要,这里选择(4*sig_diff_1+1)-(6*sig_diff+1)之间 int kernel_width = 2 * cvRound(GAUSS_KERNEL_RATIO * sig_diff) + 1; Size kernel_size(kernel_width, kernel_width); GaussianBlur(floatImage, init_image, kernel_size, sig_diff, sig_diff); } } /**************************生成高斯金字塔*****************************************/ /*init_image表示已经生成的高斯金字塔第一层图像 gauss_pyramid表示生成的高斯金字塔 nOctaves表示高斯金字塔的组数 */ void mySift::build_gaussian_pyramid(const Mat& init_image, vector<vector<Mat>>& gauss_pyramid, int nOctaves) { vector<double> sig; sig.push_back(sigma); double k = pow(2.0, 1.0 / nOctaveLayers); //高斯金字塔每一层的系数 k for (int i = 1; i < nOctaveLayers + 3; ++i) { double prev_sig = pow(k, i - 1) * sigma; //每一个尺度层的尺度 double curr_sig = k * prev_sig; //组内每层的尺度坐标计算公式 sig.push_back(sqrt(curr_sig * curr_sig - prev_sig * prev_sig)); } gauss_pyramid.resize(nOctaves); for (int i = 0; i < nOctaves; ++i) { gauss_pyramid[i].resize(nOctaveLayers + 3); } for (int i = 0; i < nOctaves; ++i) //对于每一组 { for (int j = 0; j < nOctaveLayers + 3; ++j) //对于组内的每一层 { if (i == 0 && j == 0) //第一组,第一层 gauss_pyramid[0][0] = init_image; else if (j == 0) { resize(gauss_pyramid[i - 1][3], gauss_pyramid[i][0], Size(gauss_pyramid[i - 1][3].cols / 2, gauss_pyramid[i - 1][3].rows / 2), 0, 0, INTER_LINEAR); } else { //高斯滤波窗口大小选择很重要,这里选择(4*sig_diff_1+1)-(6*sig_diff+1)之间 int kernel_width = 2 * cvRound(GAUSS_KERNEL_RATIO * sig[j]) + 1; Size kernel_size(kernel_width, kernel_width); GaussianBlur(gauss_pyramid[i][j - 1], gauss_pyramid[i][j], kernel_size, sig[j], sig[j]); } } } } /*******************生成高斯差分金字塔,即LOG金字塔*************************/ /*dog_pyramid表示DOG金字塔 gauss_pyramin表示高斯金字塔*/ void mySift::build_dog_pyramid(vector<vector<Mat>>& dog_pyramid, const vector<vector<Mat>>& gauss_pyramid) { vector<vector<Mat>>::size_type nOctaves = gauss_pyramid.size(); for (vector<vector<Mat>>::size_type i = 0; i < nOctaves; ++i) { //用于存放每一个梯度中的所有尺度层 vector<Mat> temp_vec; for (auto j = 0; j < nOctaveLayers + 2; ++j) { Mat temp_img = gauss_pyramid[i][j + 1] - gauss_pyramid[i][j]; temp_vec.push_back(temp_img); } dog_pyramid.push_back(temp_vec); temp_vec.clear(); } } /***********生成高斯差分金字塔当前层对应的梯度幅度图像和梯度方向图像***********/ /*image为高斯差分金字塔当前层图像 *amplit为当前层梯度幅度图像 *orient为当前层梯度方向图像 *scale当前层尺度 *nums为相对底层的层数 */ void mySift::amplit_orient(const Mat& image, vector<Mat>& amplit, vector<Mat>& orient, float scale, int nums) { //分配内存 amplit.resize(Mmax * nOctaveLayers); orient.resize(Mmax * nOctaveLayers); int radius = cvRound(2 * scale); Mat kernel; //kernel(2 * radius + 1, 2 * radius + 1, CV_32FC1); roewa_kernel(radius, scale, kernel); //返回滤波核,也即指数部分,存放在矩阵的右下角 //四个滤波模板生成 Mat W34 = Mat::zeros(2 * radius + 1, 2 * radius + 1, CV_32FC1); //把kernel矩阵下半部分复制到对应部分 Mat W12 = Mat::zeros(2 * radius + 1, 2 * radius + 1, CV_32FC1); //把kernel矩阵上半部分复制到对应部分 Mat W14 = Mat::zeros(2 * radius + 1, 2 * radius + 1, CV_32FC1); //把kernel矩阵右半部分复制到对应部分 Mat W23 = Mat::zeros(2 * radius + 1, 2 * radius + 1, CV_32FC1); //把kernel矩阵左半部分复制到对应部分 kernel(Range(radius + 1, 2 * radius + 1), Range::all()).copyTo(W34(Range(radius + 1, 2 * radius + 1), Range::all())); kernel(Range(0, radius), Range::all()).copyTo(W12(Range(0, radius), Range::all())); kernel(Range::all(), Range(radius + 1, 2 * radius + 1)).copyTo(W14(Range::all(), Range(radius + 1, 2 * radius + 1))); kernel(Range::all(), Range(0, radius)).copyTo(W23(Range::all(), Range(0, radius))); //滤波 Mat M34, M12, M14, M23; double eps = 0.00001; //float_image为图像归一化后的图像数据,做卷积运算 filter2D(image, M34, CV_32FC1, W34, Point(-1, -1), eps); filter2D(image, M12, CV_32FC1, W12, Point(-1, -1), eps); filter2D(image, M14, CV_32FC1, W14, Point(-1, -1), eps); filter2D(image, M23, CV_32FC1, W23, Point(-1, -1), eps); //计算水平梯度和竖直梯度 Mat Gx, Gy; log((M14) / (M23), Gx); log((M34) / (M12), Gy); //计算梯度幅度和梯度方向 magnitude(Gx, Gy, amplit[nums]); //梯度幅度图像,平方和开平方 phase(Gx, Gy, orient[nums], true); //梯度方向图像 } /***********************该函数计算尺度空间特征点的主方向,用于后面特征点的检测***************************/ /*image表示特征点所在位置的高斯图像,后面可对着源码进行修改 pt表示特征点的位置坐标(x,y) scale特征点的尺度 n表示直方图bin个数 hist表示计算得到的直方图 函数返回值是直方图hist中的最大数值*/ static float clac_orientation_hist(const Mat& image, Point pt, float scale, int n, float* hist) { int radius = cvRound(ORI_RADIUS * scale); //特征点邻域半径(3*1.5*scale) int len = (2 * radius + 1) * (2 * radius + 1); //特征点邻域像素总个数(最大值) float sigma = ORI_SIG_FCTR * scale; //特征点邻域高斯权重标准差(1.5*scale) float exp_scale = -1.f / (2 * sigma * sigma); //权重的指数部分 //使用AutoBuffer分配一段内存,这里多出4个空间的目的是为了方便后面平滑直方图的需要 AutoBuffer<float> buffer((4 * len) + n + 4); //X保存水平差分,Y保存数值差分,Mag保存梯度幅度,Ori保存梯度角度,W保存高斯权重 float* X = buffer, * Y = buffer + len, * Mag = Y, * Ori = Y + len, * W = Ori + len; float* temp_hist = W + len + 2; //临时保存直方图数据 for (int i = 0; i < n; ++i) temp_hist[i] = 0.f; //数据清零 //计算邻域像素的水平差分和竖直差分 int k = 0; for (int i = -radius; i < radius; ++i) { int y = pt.y + i; //邻域点的纵坐标 if (y <= 0 || y >= image.rows - 1) continue; for (int j = -radius; j < radius; ++j) { int x = pt.x + j; if (x <= 0 || x >= image.cols - 1) continue; float dx = image.at<float>(y, x + 1) - image.at<float>(y, x - 1); //水平差分 float dy = image.at<float>(y + 1, x) - image.at<float>(y - 1, x); //竖直差分 //保存水平差分和竖直差分及其对应的权重 X[k] = dx; Y[k] = dy; W[k] = (i * i + j * j) * exp_scale; ++k; } } len = k; //邻域内特征点的个数 cv::hal::exp(W, W, len); //计算邻域内所有像素的高斯权重 cv::hal::fastAtan2(Y, X, Ori, len, true); //计算邻域内所有像素的梯度方向,角度范围0-360度 cv::hal::magnitude32f(X, Y, Mag, len); //计算邻域内所有像素的梯度幅度,计算的是数学意义上的梯度 //遍历邻域的像素 for (int i = 0; i < len; ++i) { int bin = cvRound((n / 360.f) * Ori[i]); //利用像素的梯度方向,约束bin的范围在[0,(n-1)] //像素点梯度方向为360度时,和0°一样 if (bin >= n) bin = bin - n; if (bin < 0) bin = bin + n; temp_hist[bin] = temp_hist[bin] + Mag[i] * W[i]; //统计邻域内像素各个方向在梯度直方图的幅值(加权后的幅值) } //平滑直方图 temp_hist[-1] = temp_hist[n - 1]; temp_hist[-2] = temp_hist[n - 2]; temp_hist[n] = temp_hist[0]; temp_hist[n + 1] = temp_hist[1]; for (int i = 0; i < n; ++i) { hist[i] = (temp_hist[i - 2] + temp_hist[i + 2]) * (1.f / 16.f) + (temp_hist[i - 1] + temp_hist[i + 1]) * (4.f / 16.f) + temp_hist[i] * (6.f / 16.f); } //获得直方图中最大值 float max_value = hist[0]; for (int i = 1; i < n; ++i) { if (hist[i] > max_value) max_value = hist[i]; } return max_value; } /***********************使用 sobel 滤波器定义的新梯度计算尺度空间特征点的主方向**************************/ static float clac_orientation_hist_2(Mat& image, Point pt, float scale, int n, float* hist) { Mat output_image; //使用 sobel 滤波器计算的图像的梯度幅度图像 sobelfilter(image, output_image); //使用 sobel 滤波器求高斯差分图像的梯度幅度图像 int radius = cvRound(ORI_RADIUS * scale); //特征点邻域半径(3*1.5*scale) int len = (2 * radius + 1) * (2 * radius + 1); //特征点邻域像素总个数(最大值) float sigma = ORI_SIG_FCTR * scale; //特征点邻域高斯权重标准差(1.5*scale) float exp_scale = -1.f / (2 * sigma * sigma); //权重的指数部分 //使用AutoBuffer分配一段内存,这里多出4个空间的目的是为了方便后面平滑直方图的需要 AutoBuffer<float> buffer((4 * len) + n + 4); //X保存水平差分,Y保存数值差分,Mag保存梯度幅度,Ori保存梯度角度,W保存高斯权重 float* X = buffer, * Y = buffer + len, * Mag = Y, * Ori = Y + len, * W = Ori + len; float* temp_hist = W + len + 2; //临时保存直方图数据 for (int i = 0; i < n; ++i) temp_hist[i] = 0.f; //数据清零 //计算邻域像素的水平差分和竖直差分 int k = 0; for (int i = -radius; i < radius; ++i) { int y = pt.y + i; //邻域点的纵坐标,行 if (y <= 0 || y >= output_image.rows - 1) continue; for (int j = -radius; j < radius; ++j) { int x = pt.x + j; //邻域点的纵坐标,列 if (x <= 0 || x >= output_image.cols - 1) continue; //float dx = image.at<float>(y, x + 1) - image.at<float>(y, x - 1); //水平差分 float dx = output_image.at<float>(y - 1, x + 1) - output_image.at<float>(y - 1, x - 1) + 2 * output_image.at<float>(y, x + 1) - 2 * output_image.at<float>(y, x - 1) + output_image.at<float>(y + 1, x + 1) - output_image.at<float>(y + 1, x - 1); float dy = output_image.at<float>(y + 1, x - 1) - output_image.at<float>(y - 1, x - 1) + 2 * output_image.at<float>(y + 1, x) - 2 * output_image.at<float>(y - 1, x) + output_image.at<float>(y + 1, x + 1) - output_image.at<float>(y - 1, x + 1); /*float dx = image.at<float>(y - 1, x + 1) - image.at<float>(y - 1, x - 1) + 2 * image.at<float>(y, x + 1) - 2 * image.at<float>(y, x - 1) + image.at<float>(y + 1, x + 1) - image.at<float>(y + 1, x - 1); float dy = image.at<float>(y + 1, x - 1) - image.at<float>(y - 1, x - 1) + 2 * image.at<float>(y + 1, x) - 2 * image.at<float>(y - 1, x) + image.at<float>(y + 1, x + 1) - image.at<float>(y - 1, x + 1);*/ //保存水平差分和竖直差分及其对应的权重 X[k] = dx; Y[k] = dy; W[k] = (i * i + j * j) * exp_scale; ++k; } } len = k; //邻域内特征点的个数 cv::hal::exp(W, W, len); //计算邻域内所有像素的高斯权重 cv::hal::fastAtan2(Y, X, Ori, len, true); //计算邻域内所有像素的梯度方向,角度范围0-360度 cv::hal::magnitude32f(X, Y, Mag, len); //计算邻域内所有像素的梯度幅度,计算的是数学意义上的梯度 //遍历邻域的像素 for (int i = 0; i < len; ++i) { int bin = cvRound((n / 360.f) * Ori[i]); //利用像素的梯度方向,约束bin的范围在[0,(n-1)] //像素点梯度方向为360度时,和0°一样 if (bin >= n) bin = bin - n; if (bin < 0) bin = bin + n; temp_hist[bin] = temp_hist[bin] + Mag[i] * W[i]; //统计邻域内像素各个方向在梯度直方图的幅值(加权后的幅值) } //平滑直方图 temp_hist[-1] = temp_hist[n - 1]; temp_hist[-2] = temp_hist[n - 2]; temp_hist[n] = temp_hist[0]; temp_hist[n + 1] = temp_hist[1]; for (int i = 0; i < n; ++i) { hist[i] = (temp_hist[i - 2] + temp_hist[i + 2]) * (1.f / 16.f) + (temp_hist[i - 1] + temp_hist[i + 1]) * (4.f / 16.f) + temp_hist[i] * (6.f / 16.f); } //获得直方图中最大值 float max_value = hist[0]; for (int i = 1; i < n; ++i) { if (hist[i] > max_value) max_value = hist[i]; } return max_value; } /******************该函数计算特征点主方向,用于LOGH版本*********************/ /*amplit表示特征点所在层的梯度幅度,即输入图像对应像素点的梯度存在了对应位置 orient表示特征点所在层的梯度方向,0-360度 point表示特征点坐标 scale表示特征点的所在层的尺度 hist表示生成的直方图 n表示主方向直方图bin个数 */ static float calc_orient_hist(const Mat& amplit, const Mat& orient, Point pt, float scale, float* hist, int n) { //暂且认为是只进行了下采样,没有进行高斯模糊 int num_row = amplit.rows; int num_col = amplit.cols; Point point(cvRound(pt.x), cvRound(pt.y)); //int radius = cvRound(SAR_SIFT_FACT_RADIUS_ORI * scale); int radius = cvRound(6 * scale); radius = min(radius, min(num_row / 2, num_col / 2)); float gauss_sig = 2 * scale; //高斯加权标准差 float exp_temp = -1.f / (2 * gauss_sig * gauss_sig); //权重指数部分 //邻域区域 int radius_x_left = point.x - radius; int radius_x_right = point.x + radius; int radius_y_up = point.y - radius; int radius_y_down = point.y + radius; //防止越界 if (radius_x_left < 0) radius_x_left = 0; if (radius_x_right > num_col - 1) radius_x_right = num_col - 1; if (radius_y_up < 0) radius_y_up = 0; if (radius_y_down > num_row - 1) radius_y_down = num_row - 1; //此时特征点周围矩形区域相对于本矩形区域的中心 int center_x = point.x - radius_x_left; int center_y = point.y - radius_y_up; //矩形区域的边界,计算高斯权值 Range x_rng(-(point.x - radius_x_left), radius_x_right - point.x); Range y_rng(-(point.y - radius_y_up), radius_y_down - point.y); Mat gauss_weight; gauss_weight.create(y_rng.end - y_rng.start + 1, x_rng.end - x_rng.start + 1, CV_32FC1); //求各个像素点的高斯权重 for (int i = y_rng.start; i <= y_rng.end; ++i) { float* ptr_gauss = gauss_weight.ptr<float>(i - y_rng.start); for (int j = x_rng.start; j <= x_rng.end; ++j) ptr_gauss[j - x_rng.start] = exp((i * i + j * j) * exp_temp); } //索引特征点周围的像素梯度幅度,梯度方向 Mat sub_amplit = amplit(Range(radius_y_up, radius_y_down + 1), Range(radius_x_left, radius_x_right + 1)); Mat sub_orient = orient(Range(radius_y_up, radius_y_down + 1), Range(radius_x_left, radius_x_right + 1)); //Mat W = sub_amplit.mul(gauss_weight); //加入高斯权重,计算高斯权重时,正确匹配点对反而变少了 Mat W = sub_amplit; //没加高斯权重,梯度幅值 //计算直方图 AutoBuffer<float> buffer(n + 4); float* temp_hist = buffer + 2; for (int i = 0; i < n; ++i) temp_hist[i] = 0.f; for (int i = 0; i < sub_orient.rows; i++) { float* ptr_1 = W.ptr<float>(i); float* ptr_2 = sub_orient.ptr<float>(i); for (int j = 0; j < sub_orient.cols; j++) { if (((i - center_y) * (i - center_y) + (j - center_x) * (j - center_x)) < radius * radius) { int bin = cvRound(ptr_2[j] * n / 360.f); if (bin > n) bin = bin - n; if (bin < 0) bin = bin + n; temp_hist[bin] += ptr_1[j]; } } } //平滑直方图,可以防止突变 temp_hist[-1] = temp_hist[n - 1]; temp_hist[-2] = temp_hist[n - 2]; temp_hist[n] = temp_hist[0]; temp_hist[n + 1] = temp_hist[1]; for (int i = 0; i < n; ++i) { hist[i] = (temp_hist[i - 2] + temp_hist[i + 2]) * (1.f / 16.f) + (temp_hist[i - 1] + temp_hist[i + 1]) * (4.f / 16.f) + temp_hist[i] * (6.f / 16.f); } //获得直方图中最大值 float max_value = hist[0]; for (int i = 1; i < n; ++i) { if (hist[i] > max_value) max_value = hist[i]; } return max_value; } /****************************该函数精确定位特征点位置(x,y,scale),用于后面特征点的检测*************************/ /*功能:确定特征点的位置,并通过主曲率消除边缘相应点,该版本是简化版 dog_pry表示DOG金字塔 kpt表示精确定位后该特征点的信息 octave表示初始特征点所在的组 layer表示初始特征点所在的层 row表示初始特征点在图像中的行坐标 col表示初始特征点在图像中的列坐标 nOctaveLayers表示DOG金字塔每组中间层数,默认是3 contrastThreshold表示对比度阈值,默认是0.04 edgeThreshold表示边缘阈值,默认是10 sigma表示高斯尺度空间最底层图像尺度,默认是1.6*/ static bool adjust_local_extrema_1(const vector<vector<Mat>>& dog_pyr, KeyPoint& kpt, int octave, int& layer, int& row, int& col, int nOctaveLayers, float contrastThreshold, float edgeThreshold, float sigma) { float xi = 0, xr = 0, xc = 0; int i = 0; for (; i < MAX_INTERP_STEPS; ++i) //最大迭代次数 { const Mat& img = dog_pyr[octave][layer]; //当前层图像索引 const Mat& prev = dog_pyr[octave][layer - 1]; //之前层图像索引 const Mat& next = dog_pyr[octave][layer + 1]; //下一层图像索引 //特征点位置x方向,y方向,尺度方向的一阶偏导数 float dx = (img.at<float>(row, col + 1) - img.at<float>(row, col - 1)) * (1.f / 2.f); float dy = (img.at<float>(row + 1, col) - img.at<float>(row - 1, col)) * (1.f / 2.f); float dz = (next.at<float>(row, col) - prev.at<float>(row, col)) * (1.f / 2.f); //计算特征点位置二阶偏导数 float v2 = img.at<float>(row, col); float dxx = img.at<float>(row, col + 1) + img.at<float>(row, col - 1) - 2 * v2; float dyy = img.at<float>(row + 1, col) + img.at<float>(row - 1, col) - 2 * v2; float dzz = prev.at<float>(row, col) + next.at<float>(row, col) - 2 * v2; //计算特征点周围混合二阶偏导数 float dxy = (img.at<float>(row + 1, col + 1) + img.at<float>(row - 1, col - 1) - img.at<float>(row + 1, col - 1) - img.at<float>(row - 1, col + 1)) * (1.f / 4.f); float dxz = (next.at<float>(row, col + 1) + prev.at<float>(row, col - 1) - next.at<float>(row, col - 1) - prev.at<float>(row, col + 1)) * (1.f / 4.f); float dyz = (next.at<float>(row + 1, col) + prev.at<float>(row - 1, col) - next.at<float>(row - 1, col) - prev.at<float>(row + 1, col)) * (1.f / 4.f); Matx33f H(dxx, dxy, dxz, dxy, dyy, dyz, dxz, dyz, dzz); Vec3f dD(dx, dy, dz); Vec3f X = H.solve(dD, DECOMP_SVD); xc = -X[0]; //x方向偏移量 xr = -X[1]; //y方向偏移量 xi = -X[2]; //尺度方向偏移量 //如果三个方向偏移量都小于0.5,说明已经找到特征点准确位置 if (abs(xc) < 0.5f && abs(xr) < 0.5f && abs(xi) < 0.5f) break; //如果其中一个方向的偏移量过大,则删除该点 if (abs(xc) > (float)(INT_MAX / 3) || abs(xr) > (float)(INT_MAX / 3) || abs(xi) > (float)(INT_MAX / 3)) return false; col = col + cvRound(xc); row = row + cvRound(xr); layer = layer + cvRound(xi); //如果特征点定位在边界区域,同样也需要删除 if (layer<1 || layer>nOctaveLayers || col<IMG_BORDER || col>img.cols - IMG_BORDER || row<IMG_BORDER || row>img.rows - IMG_BORDER) return false; } //如果i=MAX_INTERP_STEPS,说明循环结束也没有满足条件,即该特征点需要被删除 if (i >= MAX_INTERP_STEPS) return false; /**************************再次删除低响应点(对比度较低的点)********************************/ //再次计算特征点位置x方向,y方向,尺度方向的一阶偏导数 //高对比度的特征对图像的变形是稳定的 { const Mat& img = dog_pyr[octave][layer]; const Mat& prev = dog_pyr[octave][layer - 1]; const Mat& next = dog_pyr[octave][layer + 1]; float dx = (img.at<float>(row, col + 1) - img.at<float>(row, col - 1)) * (1.f / 2.f); float dy = (img.at<float>(row + 1, col) - img.at<float>(row - 1, col)) * (1.f / 2.f); float dz = (next.at<float>(row, col) - prev.at<float>(row, col)) * (1.f / 2.f); Matx31f dD(dx, dy, dz); float t = dD.dot(Matx31f(xc, xr, xi)); float contr = img.at<float>(row, col) + t * 0.5f; //特征点响应 |D(x~)| 即对比度 //Low建议contr阈值是0.03,但是RobHess等建议阈值为0.04/nOctaveLayers if (abs(contr) < contrastThreshold / nOctaveLayers) //阈值设为0.03时特征点数量过多 return false; /******************************删除边缘响应比较强的点************************************/ //再次计算特征点位置二阶偏导数,获取特征点出的 Hessian 矩阵,主曲率通过 2X2 的 Hessian 矩阵求出 //一个定义不好的高斯差分算子的极值在横跨边缘的地方有较大的主曲率而在垂直边缘的方向有较小的主曲率 float v2 = img.at<float>(row, col); float dxx = img.at<float>(row, col + 1) + img.at<float>(row, col - 1) - 2 * v2; float dyy = img.at<float>(row + 1, col) + img.at<float>(row - 1, col) - 2 * v2; float dxy = (img.at<float>(row + 1, col + 1) + img.at<float>(row - 1, col - 1) - img.at<float>(row + 1, col - 1) - img.at<float>(row - 1, col + 1)) * (1.f / 4.f); float det = dxx * dyy - dxy * dxy; float trace = dxx + dyy; //主曲率和阈值的对比判定 if (det < 0 || (trace * trace * edgeThreshold >= det * (edgeThreshold + 1) * (edgeThreshold + 1))) return false; /*********到目前为止该特征的满足上面所有要求,保存该特征点信息***********/ kpt.pt.x = ((float)col + xc) * (1 << octave); //相对于最底层的图像的x坐标 kpt.pt.y = ((float)row + xr) * (1 << octave); //相对于最底层图像的y坐标 kpt.octave = octave + (layer << 8); //组号保存在低字节,层号保存在高字节 //相对于最底层图像的尺度 kpt.size = sigma * powf(2.f, (layer + xi) / nOctaveLayers) * (1 << octave); kpt.response = abs(contr); //特征点响应值(对比度) return true; } } /****************************该函数精确定位特征点位置(x,y,scale),用于后面特征点的检测*************************/ //该版本是 SIFT 原版,检测得到的特征点数量更多 static bool adjust_local_extrema_2(const vector<vector<Mat>>& dog_pyr, KeyPoint& kpt, int octave, int& layer, int& row, int& col, int nOctaveLayers, float contrastThreshold, float edgeThreshold, float sigma) { const float img_scale = 1.f / (255 * SIFT_FIXPT_SCALE); //SIFT_FIXPT_SCALE=48 const float deriv_scale = img_scale * 0.5f; const float second_deriv_scale = img_scale; const float cross_deriv_scale = img_scale * 0.25f; float xi = 0, xr = 0, xc = 0; int i = 0; for (; i < MAX_INTERP_STEPS; ++i) //最大迭代次数 { const Mat& img = dog_pyr[octave][layer]; //当前层图像索引 const Mat& prev = dog_pyr[octave][layer - 1]; //之前层图像索引 const Mat& next = dog_pyr[octave][layer + 1]; //下一层图像索引 //计算一阶偏导数,通过临近点差分求得 float dx = (img.at<float>(row, col + 1) - img.at<float>(row, col - 1)) * deriv_scale; float dy = (img.at<float>(row + 1, col) - img.at<float>(row - 1, col)) * deriv_scale; float dz = (next.at<float>(row, col) - prev.at<float>(row, col)) * deriv_scale; //计算特征点位置二阶偏导数 //float v2 = img.at<float>(row, col); float v2 = (float)img.at<float>(row, col) * 2.f; float dxx = (img.at<float>(row, col + 1) + img.at<float>(row, col - 1) - v2) * second_deriv_scale; float dyy = (img.at<float>(row + 1, col) + img.at<float>(row - 1, col) - v2) * second_deriv_scale; float dzz = (prev.at<float>(row, col) + next.at<float>(row, col) - v2) * second_deriv_scale; //计算特征点周围混合二阶偏导数 float dxy = (img.at<float>(row + 1, col + 1) + img.at<float>(row - 1, col - 1) - img.at<float>(row + 1, col - 1) - img.at<float>(row - 1, col + 1)) * cross_deriv_scale; float dxz = (next.at<float>(row, col + 1) + prev.at<float>(row, col - 1) - next.at<float>(row, col - 1) - prev.at<float>(row, col + 1)) * cross_deriv_scale; float dyz = (next.at<float>(row + 1, col) + prev.at<float>(row - 1, col) - next.at<float>(row - 1, col) - prev.at<float>(row + 1, col)) * cross_deriv_scale; Matx33f H(dxx, dxy, dxz, dxy, dyy, dyz, dxz, dyz, dzz); Vec3f dD(dx, dy, dz); Vec3f X = H.solve(dD, DECOMP_SVD); xc = -X[0]; //x方向偏移量 xr = -X[1]; //y方向偏移量 xi = -X[2]; //尺度方向偏移量 //如果三个方向偏移量都小于0.5,说明已经找到特征点准确位置 if (abs(xc) < 0.5f && abs(xr) < 0.5f && abs(xi) < 0.5f) break; //如果其中一个方向的偏移量过大,则删除该点 if (abs(xc) > (float)(INT_MAX / 3) || abs(xr) > (float)(INT_MAX / 3) || abs(xi) > (float)(INT_MAX / 3)) return false; col = col + cvRound(xc); row = row + cvRound(xr); layer = layer + cvRound(xi); //如果特征点定位在边界区域,同样也需要删除 if (layer<1 || layer>nOctaveLayers || col < IMG_BORDER || col >= img.cols - IMG_BORDER || row < IMG_BORDER || row >= img.rows - IMG_BORDER) return false; } //如果i=MAX_INTERP_STEPS,说明循环结束也没有满足条件,即该特征点需要被删除 if (i >= MAX_INTERP_STEPS) return false; /**************************再次删除低响应点(对比度较低的点)********************************/ //再次计算特征点位置x方向,y方向,尺度方向的一阶偏导数 //高对比度的特征对图像的变形是稳定的 const Mat& img = dog_pyr[octave][layer]; const Mat& prev = dog_pyr[octave][layer - 1]; const Mat& next = dog_pyr[octave][layer + 1]; float dx = (img.at<float>(row, col + 1) - img.at<float>(row, col - 1)) * deriv_scale; float dy = (img.at<float>(row + 1, col) - img.at<float>(row - 1, col)) * deriv_scale; float dz = (next.at<float>(row, col) - prev.at<float>(row, col)) * deriv_scale; Matx31f dD(dx, dy, dz); float t = dD.dot(Matx31f(xc, xr, xi)); float contr = img.at<float>(row, col) + t * 0.5f; //特征点响应 |D(x~)| 即对比度 //Low建议contr阈值是0.03,但是RobHess等建议阈值为0.04/nOctaveLayers if (abs(contr) < contrastThreshold / nOctaveLayers) //阈值设为0.03时特征点数量过多 return false; /******************************删除边缘响应比较强的点************************************/ //再次计算特征点位置二阶偏导数,获取特征点出的 Hessian 矩阵,主曲率通过 2X2 的 Hessian 矩阵求出 //一个定义不好的高斯差分算子的极值在横跨边缘的地方有较大的主曲率而在垂直边缘的方向有较小的主曲率 float v2 = (float)img.at<float>(row, col) * 2.f; float dxx = (img.at<float>(row, col + 1) + img.at<float>(row, col - 1) - v2) * second_deriv_scale; float dyy = (img.at<float>(row + 1, col) + img.at<float>(row - 1, col) - v2) * second_deriv_scale; float dxy = (img.at<float>(row + 1, col + 1) + img.at<float>(row - 1, col - 1) - img.at<float>(row + 1, col - 1) - img.at<float>(row - 1, col + 1)) * cross_deriv_scale; float det = dxx * dyy - dxy * dxy; float trace = dxx + dyy; //主曲率和阈值的对比判定 if (det <= 0 || (trace * trace * edgeThreshold >= det * (edgeThreshold + 1) * (edgeThreshold + 1))) return false; /*********保存该特征点信息***********/ kpt.pt.x = ((float)col + xc) * (1 << octave); //高斯金字塔坐标根据组数扩大相应的倍数 kpt.pt.y = ((float)row + xr) * (1 << octave); // SIFT 描述子 kpt.octave = octave + (layer << 8) + (cvRound((xi + 0.5) * 255) << 16); //特征点被检测出时所在的金字塔组 kpt.size = sigma * powf(2.f, (layer + xi) / nOctaveLayers) * (1 << octave) * 2; //关键点邻域直径 kpt.response = abs(contr); //特征点响应值(对比度) return true; } /************该函数在DOG金字塔上进行特征点检测,特征点精确定位,删除低对比度点,删除边缘响应较大点**********/ /*dog_pyr表示高斯金字塔 原始 SIFT 算子 gauss_pyr表示高斯金字塔 keypoints表示检测到的特征点*/ void mySift::find_scale_space_extrema(const vector<vector<Mat>>& dog_pyr, const vector<vector<Mat>>& gauss_pyr, vector<KeyPoint>& keypoints) { int nOctaves = (int)dog_pyr.size(); //子八度个数 //Low文章建议thresholdThreshold是0.03,Rob Hess等人使用0.04/nOctaveLayers作为阈值 float threshold = (float)(contrastThreshold / nOctaveLayers); const int n = ORI_HIST_BINS; //n=36 float hist[n]; KeyPoint kpt; keypoints.clear(); //先清空keypoints for (int i = 0; i < nOctaves; ++i) //对于每一组 { for (int j = 1; j <= nOctaveLayers; ++j) //对于组内每一层 { const Mat& curr_img = dog_pyr[i][j]; //当前层 const Mat& prev_img = dog_pyr[i][j - 1]; //上一层 const Mat& next_img = dog_pyr[i][j + 1]; //下一层 int num_row = curr_img.rows; int num_col = curr_img.cols; //获得当前组图像的大小 size_t step = curr_img.step1(); //一行元素所占字节数 //遍历每一个尺度层中的有效像素,像素值 for (int r = IMG_BORDER; r < num_row - IMG_BORDER; ++r) { const float* curr_ptr = curr_img.ptr<float>(r); //指向的是第 r 行的起点,返回的是 float 类型的像素值 const float* prev_ptr = prev_img.ptr<float>(r - 1); const float* next_ptr = next_img.ptr<float>(r + 1); for (int c = IMG_BORDER; c < num_col - IMG_BORDER; ++c) { float val = curr_ptr[c]; //当前中心点响应值 //开始检测特征点 if (abs(val) > threshold && ((val > 0 && val >= curr_ptr[c - 1] && val >= curr_ptr[c + 1] && val >= curr_ptr[c - step - 1] && val >= curr_ptr[c - step] && val >= curr_ptr[c - step + 1] && val >= curr_ptr[c + step - 1] && val >= curr_ptr[c + step] && val >= curr_ptr[c + step + 1] && val >= prev_ptr[c] && val >= prev_ptr[c - 1] && val >= prev_ptr[c + 1] && val >= prev_ptr[c - step - 1] && val >= prev_ptr[c - step] && val >= prev_ptr[c - step + 1] && val >= prev_ptr[c + step - 1] && val >= prev_ptr[c + step] && val >= prev_ptr[c + step + 1] && val >= next_ptr[c] && val >= next_ptr[c - 1] && val >= next_ptr[c + 1] && val >= next_ptr[c - step - 1] && val >= next_ptr[c - step] && val >= next_ptr[c - step + 1] && val >= next_ptr[c + step - 1] && val >= next_ptr[c + step] && val >= next_ptr[c + step + 1]) || (val < 0 && val <= curr_ptr[c - 1] && val <= curr_ptr[c + 1] && val <= curr_ptr[c - step - 1] && val <= curr_ptr[c - step] && val <= curr_ptr[c - step + 1] && val <= curr_ptr[c + step - 1] && val <= curr_ptr[c + step] && val <= curr_ptr[c + step + 1] && val <= prev_ptr[c] && val <= prev_ptr[c - 1] && val <= prev_ptr[c + 1] && val <= prev_ptr[c - step - 1] && val <= prev_ptr[c - step] && val <= prev_ptr[c - step + 1] && val <= prev_ptr[c + step - 1] && val <= prev_ptr[c + step] && val <= prev_ptr[c + step + 1] && val <= next_ptr[c] && val <= next_ptr[c - 1] && val <= next_ptr[c + 1] && val <= next_ptr[c - step - 1] && val <= next_ptr[c - step] && val <= next_ptr[c - step + 1] && val <= next_ptr[c + step - 1] && val <= next_ptr[c + step] && val <= next_ptr[c + step + 1]))) { //++numKeys; //获得特征点初始行号,列号,组号,组内层号 int octave = i, layer = j, r1 = r, c1 = c; if (!adjust_local_extrema_1(dog_pyr, kpt, octave, layer, r1, c1, nOctaveLayers, (float)contrastThreshold, (float)edgeThreshold, (float)sigma)) { continue; //如果该初始点不满足条件,则不保存改点 } float scale = kpt.size / float(1 << octave); //该特征点相对于本组的尺度 //max_hist值对应的方向为主方向 float max_hist = clac_orientation_hist(gauss_pyr[octave][layer], Point(c1, r1), scale, n, hist); //大于mag_thr值对应的方向为辅助方向 float mag_thr = max_hist * ORI_PEAK_RATIO; //主峰值 80% 的方向作为辅助方向 //遍历直方图中的 36 个bin for (int i = 0; i < n; ++i) { int left = i > 0 ? i - 1 : n - 1; int right = i < n - 1 ? i + 1 : 0; //创建新的特征点,大于主峰值 80% 的方向,赋值给该特征点,作为一个新的特征点;即有多个特征点,位置、尺度相同,方向不同 if (hist[i] > hist[left] && hist[i] > hist[right] && hist[i] >= mag_thr) { float bin = i + 0.5f * (hist[left] - hist[right]) / (hist[left] + hist[right] - 2 * hist[i]); bin = bin < 0 ? n + bin : bin >= n ? bin - n : bin; kpt.angle = (360.f / n) * bin; //原始 SIFT 算子使用的特征点的主方向0-360度 keypoints.push_back(kpt); //保存该特征点 } } } } } } } //cout << "初始满足要求特征点个数是: " << numKeys << endl; } /************该函数在DOG金字塔上进行特征点检测,特征点精确定位,删除低对比度点,删除边缘响应较大点**********/ //对特征点进行方向的细化 + 增加更多的主方向版本 ——此时细化是对最后要给关键点进行赋值时的细化 //还可以考虑直接对方向直方图进行细化 void mySift::find_scale_space_extrema1(const vector<vector<Mat>>& dog_pyr, vector<vector<Mat>>& gauss_pyr, vector<KeyPoint>& keypoints) { int nOctaves = (int)dog_pyr.size(); //子八度个数 //Low文章建议thresholdThreshold是0.03,Rob Hess等人使用0.04/nOctaveLayers作为阈值 float threshold = (float)(contrastThreshold / nOctaveLayers); const int n = ORI_HIST_BINS; //n=36 float hist[n]; KeyPoint kpt; vector<Mat> amplit; //存放高斯差分金字塔每一层的梯度幅度图像 vector<Mat> orient; //存放高斯差分金字塔每一层的梯度方向图像 keypoints.clear(); //先清空keypoints for (int i = 0; i < nOctaves; ++i) //对于每一组 { for (int j = 1; j <= nOctaveLayers; ++j) //对于组内每一层 { const Mat& curr_img = dog_pyr[i][j]; //当前层 const Mat& prev_img = dog_pyr[i][j - 1]; //上一层 const Mat& next_img = dog_pyr[i][j + 1]; //下一层 int num_row = curr_img.rows; int num_col = curr_img.cols; //获得当前组图像的大小 size_t step = curr_img.step1(); //一行元素所占字节数 //遍历每一个尺度层中的有效像素,像素值 for (int r = IMG_BORDER; r < num_row - IMG_BORDER; ++r) { const float* curr_ptr = curr_img.ptr<float>(r); //指向的是第 r 行的起点,返回的是 float 类型的像素值 const float* prev_ptr = prev_img.ptr<float>(r); const float* next_ptr = next_img.ptr<float>(r); for (int c = IMG_BORDER; c < num_col - IMG_BORDER; ++c) { float val = curr_ptr[c]; //当前中心点响应值 //开始检测特征点 if (abs(val) > threshold && ((val > 0 && val >= curr_ptr[c - 1] && val >= curr_ptr[c + 1] && val >= curr_ptr[c - step - 1] && val >= curr_ptr[c - step] && val >= curr_ptr[c - step + 1] && val >= curr_ptr[c + step - 1] && val >= curr_ptr[c + step] && val >= curr_ptr[c + step + 1] && val >= prev_ptr[c] && val >= prev_ptr[c - 1] && val >= prev_ptr[c + 1] && val >= prev_ptr[c - step - 1] && val >= prev_ptr[c - step] && val >= prev_ptr[c - step + 1] && val >= prev_ptr[c + step - 1] && val >= prev_ptr[c + step] && val >= prev_ptr[c + step + 1] && val >= next_ptr[c] && val >= next_ptr[c - 1] && val >= next_ptr[c + 1] && val >= next_ptr[c - step - 1] && val >= next_ptr[c - step] && val >= next_ptr[c - step + 1] && val >= next_ptr[c + step - 1] && val >= next_ptr[c + step] && val >= next_ptr[c + step + 1]) || (val < 0 && val <= curr_ptr[c - 1] && val <= curr_ptr[c + 1] && val <= curr_ptr[c - step - 1] && val <= curr_ptr[c - step] && val <= curr_ptr[c - step + 1] && val <= curr_ptr[c + step - 1] && val <= curr_ptr[c + step] && val <= curr_ptr[c + step + 1] && val <= prev_ptr[c] && val <= prev_ptr[c - 1] && val <= prev_ptr[c + 1] && val <= prev_ptr[c - step - 1] && val <= prev_ptr[c - step] && val <= prev_ptr[c - step + 1] && val <= prev_ptr[c + step - 1] && val <= prev_ptr[c + step] && val <= prev_ptr[c + step + 1] && val <= next_ptr[c] && val <= next_ptr[c - 1] && val <= next_ptr[c + 1] && val <= next_ptr[c - step - 1] && val <= next_ptr[c - step] && val <= next_ptr[c - step + 1] && val <= next_ptr[c + step - 1] && val <= next_ptr[c + step] && val <= next_ptr[c + step + 1]))) { //++numKeys; //获得特征点初始行号,列号,组号,组内层号 int octave = i, layer = j, r1 = r, c1 = c, nums = i * nOctaves + j; if (!adjust_local_extrema_2(dog_pyr, kpt, octave, layer, r1, c1, nOctaveLayers, (float)contrastThreshold, (float)edgeThreshold, (float)sigma)) { continue; //如果该初始点不满足条件,则不保存改点 } float scale = kpt.size / float(1 << octave); //该特征点相对于本组的尺度 //计算梯度幅度和梯度方向 //amplit_orient(curr_img, amplit, orient, scale, nums); //max_hist值对应的方向为主方向 float max_hist = clac_orientation_hist(gauss_pyr[octave][layer], Point(c1, r1), scale, n, hist); //float max_hist = calc_orient_hist(amplit[nums], orient[nums], Point2f(c1, r1), scale, hist, n); 大于mag_thr值对应的方向为辅助方向 //float mag_thr = max_hist * ORI_PEAK_RATIO; //主峰值 80% 的方向作为辅助方向 //增加更多的主方向,以增加特征点对梯度差异的鲁棒性 float sum = 0.0; //直方图对应的幅值之和 float mag_thr = 0.0; //判断是否为主方向的阈值 for (int i = 0; i < n; ++i) { sum += hist[i]; } mag_thr = 0.5 * (1.0 / 36) * sum; //遍历直方图中的 36 个bin for (int i = 0; i < n; ++i) { int left = i > 0 ? i - 1 : n - 1; int right = i < n - 1 ? i + 1 : 0; //创建新的特征点,大于主峰值 80% 的方向,赋值给该特征点,作为一个新的特征点;即有多个特征点,位置、尺度相同,方向不同 if (hist[i] > hist[left] && hist[i] > hist[right] && hist[i] >= mag_thr) { float bin = i + 0.5f * (hist[left] - hist[right]) / (hist[left] + hist[right] - 2 * hist[i]); bin = bin < 0 ? n + bin : bin >= n ? bin - n : bin; //修改的地方,特征点的主方向修改为了0-180度,相当于对方向做了一个细化 float angle = (360.f / n) * bin; if (angle >= 1 && angle <= 180) { kpt.angle = angle; } else if (angle > 180 && angle < 360) { kpt.angle = 360 - angle; } //kpt.angle = (360.f / n) * bin; //原始 SIFT 算子使用的特征点的主方向0-360度 keypoints.push_back(kpt); //保存该特征点 } } } } } } } //cout << "初始满足要求特征点个数是: " << numKeys << endl; } /*该函数生成matlab中的meshgrid函数*/ /*x_range表示x方向的范围 y_range表示y方向的范围 X表示生成的沿x轴变化的网格 Y表示生成沿y轴变换的网格 */ static void meshgrid(const Range& x_range, const Range& y_range, Mat& X, Mat& Y) { int x_start = x_range.start, x_end = x_range.end; int y_start = y_range.start, y_end = y_range.end; int width = x_end - x_start + 1; int height = y_end - y_start + 1; X.create(height, width, CV_32FC1); Y.create(height, width, CV_32FC1); for (int i = y_start; i <= y_end; i++) { float* ptr_1 = X.ptr<float>(i - y_start); for (int j = x_start; j <= x_end; ++j) ptr_1[j - x_start] = j * 1.0f; } for (int i = y_start; i <= y_end; i++) { float* ptr_2 = Y.ptr<float>(i - y_start); for (int j = x_start; j <= x_end; ++j) ptr_2[j - x_start] = i * 1.0f; } } /******************************计算一个特征点的描述子***********************************/ /*gauss_image表示特征点所在的高斯层 main_angle表示特征点的主方向,角度范围是0-360度 pt表示特征点在高斯图像上的坐标,相对与本组,不是相对于最底层 scale表示特征点所在层的尺度,相对于本组,不是相对于最底层 d表示特征点邻域网格宽度 n表示每个网格内像素梯度角度等分个数 descriptor表示生成的特征点的描述子*/ static void calc_sift_descriptor(const Mat& gauss_image, float main_angle, Point2f pt, float scale, int d, int n, float* descriptor) { Point ptxy(cvRound(pt.x), cvRound(pt.y)); //坐标取整 float cos_t = cosf(-main_angle * (float)(CV_PI / 180)); //把角度转化为弧度,计算主方向的余弦 float sin_t = sinf(-main_angle * (float)(CV_PI / 180)); //把角度转化为弧度,计算主方向的正弦 float bins_per_rad = n / 360.f; // n = 8 ,梯度直方图分为 8 个方向 float exp_scale = -1.f / (d * d * 0.5f); //权重指数部分 float hist_width = DESCR_SCL_FCTR * scale; //特征点邻域内子区域边长,子区域的边长 int radius = cvRound(hist_width * (d + 1) * sqrt(2) * 0.5f);//特征点邻域半径(d+1)*(d+1),四舍五入 int rows = gauss_image.rows, cols = gauss_image.cols; //当前高斯层行、列信息 //特征点邻域半径 radius = min(radius, (int)sqrt((double)rows * rows + cols * cols)); cos_t = cos_t / hist_width; sin_t = sin_t / hist_width; int len = (2 * radius + 1) * (2 * radius + 1); //邻域内总像素数,为后面动态分配内存使用 int histlen = (d + 2) * (d + 2) * (n + 2); //值为 360 AutoBuffer<float> buf(6 * len + histlen); //X保存水平差分,Y保存竖直差分,Mag保存梯度幅度,Angle保存特征点方向, W保存高斯权重 float* X = buf, * Y = buf + len, * Mag = Y, * Angle = Y + len, * W = Angle + len; float* RBin = W + len, * CBin = RBin + len, * hist = CBin + len; //首先清空直方图数据 for (int i = 0; i < d + 2; ++i) // i 对应 row { for (int j = 0; j < d + 2; ++j) // j 对应 col { for (int k = 0; k < n + 2; ++k) hist[(i * (d + 2) + j) * (n + 2) + k] = 0.f; } } //把邻域内的像素分配到相应子区域内,计算子区域内每个像素点的权重(子区域即 d*d 中每一个小方格) int k = 0; //实际上是在 4 x 4 的网格中找 16 个种子点,每个种子点都在子网格的正中心, //通过三线性插值对不同种子点间的像素点进行加权作用到不同的种子点上绘制方向直方图 for (int i = -radius; i < radius; ++i) // i 对应 y 行坐标 { for (int j = -radius; j < radius; ++j) // j 对应 x 列坐标 { float c_rot = j * cos_t - i * sin_t; //旋转后邻域内采样点的 x 坐标 float r_rot = j * sin_t + i * cos_t; //旋转后邻域内采样点的 y 坐标 //旋转后 5 x 5 的网格中的所有像素点被分配到 4 x 4 的网格中 float cbin = c_rot + d / 2 - 0.5f; //旋转后的采样点落在子区域的 x 坐标 float rbin = r_rot + d / 2 - 0.5f; //旋转后的采样点落在子区域的 y 坐标 int r = ptxy.y + i, c = ptxy.x + j; //ptxy是高斯金字塔中的坐标 //这里rbin,cbin范围是(-1,d) if (rbin > -1 && rbin < d && cbin>-1 && cbin < d && r>0 && r < rows - 1 && c>0 && c < cols - 1) { float dx = gauss_image.at<float>(r, c + 1) - gauss_image.at<float>(r, c - 1); float dy = gauss_image.at<float>(r + 1, c) - gauss_image.at<float>(r - 1, c); X[k] = dx; //邻域内所有像素点的水平差分 Y[k] = dy; //邻域内所有像素点的竖直差分 CBin[k] = cbin; //邻域内所有采样点落在子区域的 x 坐标 RBin[k] = rbin; //邻域内所有采样点落在子区域的 y 坐标 W[k] = (c_rot * c_rot + r_rot * r_rot) * exp_scale; //高斯权值的指数部分 ++k; } } } //计算采样点落在子区域的像素梯度幅度,梯度角度,和高斯权值 len = k; cv::hal::exp(W, W, len); //邻域内所有采样点落在子区域的像素的高斯权值 cv::hal::fastAtan2(Y, X, Angle, len, true); //邻域内所有采样点落在子区域的像素的梯度方向,角度范围是0-360度 cv::hal::magnitude(X, Y, Mag, len); //邻域内所有采样点落在子区域的像素的梯度幅度 //实际上是在 4 x 4 的网格中找 16 个种子点,每个种子点都在子网格的正中心, //通过三线性插值对不同种子点间的像素点进行加权作用到不同的种子点上绘制方向直方图 //计算每个特征点的特征描述子 for (k = 0; k < len; ++k) { float rbin = RBin[k], cbin = CBin[k]; //子区域内像素点坐标,rbin,cbin范围是(-1,d) 改进的地方,对方向进行了一个细化,也是为了增加对梯度差异的鲁棒性 //if (Angle[k] > 180 && Angle[k] < 360) // Angle[k] = 360 - Angle[k]; //子区域内像素点处理后的方向 float temp = Angle[k] - main_angle; /*if (temp > 180 && temp < 360) temp = 360 - temp;*/ float obin = temp * bins_per_rad; //指定方向的数量后,邻域内像素点对应的方向 float mag = Mag[k] * W[k]; //子区域内像素点乘以权值后的梯度幅值 int r0 = cvFloor(rbin); //ro取值集合是{-1,0,1,2,3},没太懂为什么? int c0 = cvFloor(cbin); //c0取值集合是{-1,0,1,2,3} int o0 = cvFloor(obin); rbin = rbin - r0; //子区域内像素点坐标的小数部分,用于线性插值,分配像素点的作用 cbin = cbin - c0; obin = obin - o0; //子区域方向的小数部分 //限制范围为梯度直方图横坐标[0,n),8 个方向直方图 if (o0 < 0) o0 = o0 + n; if (o0 >= n) o0 = o0 - n; //三线性插值用于计算落在两个子区域之间的像素对两个子区域的作用,并把其分配到对应子区域的8个方向上 //像素对应的信息通过加权分配给其周围的种子点,并把相应方向的梯度值进行累加 float v_r1 = mag * rbin; //第二行分配的值 float v_r0 = mag - v_r1; //第一行分配的值 float v_rc11 = v_r1 * cbin; //第二行第二列分配的值,右下角种子点 float v_rc10 = v_r1 - v_rc11; //第二行第一列分配的值,左下角种子点 float v_rc01 = v_r0 * cbin; //第一行第二列分配的值,右上角种子点 float v_rc00 = v_r0 - v_rc01; //第一行第一列分配的值,左上角种子点 //一个像素点的方向为每个种子点的两个方向做出贡献 float v_rco111 = v_rc11 * obin; //右下角种子点第二个方向上分配的值 float v_rco110 = v_rc11 - v_rco111; //右下角种子点第一个方向上分配的值 float v_rco101 = v_rc10 * obin; float v_rco100 = v_rc10 - v_rco101; float v_rco011 = v_rc01 * obin; float v_rco010 = v_rc01 - v_rco011; float v_rco001 = v_rc00 * obin; float v_rco000 = v_rc00 - v_rco001; //该像素所在网格的索引 int idx = ((r0 + 1) * (d + 2) + c0 + 1) * (n + 2) + o0; hist[idx] += v_rco000; hist[idx + 1] += v_rco001; hist[idx + n + 2] += v_rco010; hist[idx + n + 3] += v_rco011; hist[idx + (d + 2) * (n + 2)] += v_rco100; hist[idx + (d + 2) * (n + 2) + 1] += v_rco101; hist[idx + (d + 3) * (n + 2)] += v_rco110; hist[idx + (d + 3) * (n + 2) + 1] += v_rco111; } //由于圆周循环的特性,对计算以后幅角小于 0 度或大于 360 度的值重新进行调整,使 //其在 0~360 度之间 for (int i = 0; i < d; ++i) { for (int j = 0; j < d; ++j) { //类似于 hist[0][2][3] 第 0 行,第 2 列,种子点直方图中的第 3 个 bin int idx = ((i + 1) * (d + 2) + (j + 1)) * (n + 2); hist[idx] += hist[idx + n]; //hist[idx + 1] += hist[idx + n + 1];//opencv源码中这句话是多余的,hist[idx + n + 1]永远是0.0 for (k = 0; k < n; ++k) descriptor[(i * d + j) * n + k] = hist[idx + k]; } } //对描述子进行归一化 int lenght = d * d * n; float norm = 0; //计算特征描述向量的模值的平方 for (int i = 0; i < lenght; ++i) { norm = norm + descriptor[i] * descriptor[i]; } norm = sqrt(norm); //特征描述向量的模值 //此次归一化能去除光照的影响 for (int i = 0; i < lenght; ++i) { descriptor[i] = descriptor[i] / norm; } //阈值截断,去除特征描述向量中大于 0.2 的值,能消除非线性光照的影响(相机饱和度对某些放的梯度影响较大,对方向的影响较小) for (int i = 0; i < lenght; ++i) { descriptor[i] = min(descriptor[i], DESCR_MAG_THR); } //再次归一化,能够提高特征的独特性 norm = 0; for (int i = 0; i < lenght; ++i) { norm = norm + descriptor[i] * descriptor[i]; } norm = sqrt(norm); for (int i = 0; i < lenght; ++i) { descriptor[i] = descriptor[i] / norm; } } /*************************该函数计算每个特征点的特征描述子*****************************/ /*amplit表示特征点所在层的梯度幅度图像 orient表示特征点所在层的梯度角度图像 pt表示特征点的位置 scale表示特征点所在层的尺度 main_ori表示特征点的主方向,0-360度 d表示GLOH角度方向区间个数,默认是8, n表示每个网格内角度在0-360度之间等分个数,n默认是8 */ static void calc_gloh_descriptor(const Mat& amplit, const Mat& orient, Point2f pt, float scale, float main_ori, int d, int n, float* ptr_des) { Point point(cvRound(pt.x), cvRound(pt.y)); //特征点旋转方向余弦和正弦 float cos_t = cosf(-main_ori / 180.f * (float)CV_PI); float sin_t = sinf(-main_ori / 180.f * (float)CV_PI); int num_rows = amplit.rows; int num_cols = amplit.cols; int radius = cvRound(SAR_SIFT_RADIUS_DES * scale); radius = min(radius, min(num_rows / 2, num_cols / 2));//特征点邻域半径 int radius_x_left = point.x - radius; int radius_x_right = point.x + radius; int radius_y_up = point.y - radius; int radius_y_down = point.y + radius; //防止越界 if (radius_x_left < 0) radius_x_left = 0; if (radius_x_right > num_cols - 1) radius_x_right = num_cols - 1; if (radius_y_up < 0) radius_y_up = 0; if (radius_y_down > num_rows - 1) radius_y_down = num_rows - 1; //此时特征点周围本矩形区域的中心,相对于该矩形 int center_x = point.x - radius_x_left; int center_y = point.y - radius_y_up; //特征点周围区域内像素梯度幅度,梯度角度 Mat sub_amplit = amplit(Range(radius_y_up, radius_y_down + 1), Range(radius_x_left, radius_x_right + 1)); Mat sub_orient = orient(Range(radius_y_up, radius_y_down + 1), Range(radius_x_left, radius_x_right + 1)); //以center_x和center_y位中心,对下面矩形区域进行旋转 Range x_rng(-(point.x - radius_x_left), radius_x_right - point.x); Range y_rng(-(point.y - radius_y_up), radius_y_down - point.y); Mat X, Y; meshgrid(x_rng, y_rng, X, Y); Mat c_rot = X * cos_t - Y * sin_t; Mat r_rot = X * sin_t + Y * cos_t; Mat GLOH_angle, GLOH_amplit; phase(c_rot, r_rot, GLOH_angle, true);//角度在0-360度之间 GLOH_amplit = c_rot.mul(c_rot) + r_rot.mul(r_rot);//为了加快速度,没有计算开方 //三个圆半径平方 float R1_pow = (float)radius * radius;//外圆半径平方 float R2_pow = pow(radius * SAR_SIFT_GLOH_RATIO_R1_R2, 2.f);//中间圆半径平方 float R3_pow = pow(radius * SAR_SIFT_GLOH_RATIO_R1_R3, 2.f);//内圆半径平方 int sub_rows = sub_amplit.rows; int sub_cols = sub_amplit.cols; //开始构建描述子,在角度方向对描述子进行插值 int len = (d * 2 + 1) * n; AutoBuffer<float> hist(len); for (int i = 0; i < len; ++i)//清零 hist[i] = 0; for (int i = 0; i < sub_rows; ++i) { float* ptr_amplit = sub_amplit.ptr<float>(i); float* ptr_orient = sub_orient.ptr<float>(i); float* ptr_GLOH_amp = GLOH_amplit.ptr<float>(i); float* ptr_GLOH_ang = GLOH_angle.ptr<float>(i); for (int j = 0; j < sub_cols; ++j) { if (((i - center_y) * (i - center_y) + (j - center_x) * (j - center_x)) < radius * radius) { float pix_amplit = ptr_amplit[j];//该像素的梯度幅度 float pix_orient = ptr_orient[j];//该像素的梯度方向 float pix_GLOH_amp = ptr_GLOH_amp[j];//该像素在GLOH网格中的半径位置 float pix_GLOH_ang = ptr_GLOH_ang[j];//该像素在GLOH网格中的位置方向 int rbin, cbin, obin; rbin = pix_GLOH_amp < R3_pow ? 0 : (pix_GLOH_amp > R2_pow ? 2 : 1);//rbin={0,1,2} cbin = cvRound(pix_GLOH_ang * d / 360.f); cbin = cbin > d ? cbin - d : (cbin <= 0 ? cbin + d : cbin);//cbin=[1,d] obin = cvRound(pix_orient * n / 360.f); obin = obin > n ? obin - n : (obin <= 0 ? obin + n : obin);//obin=[1,n] if (rbin == 0)//内圆 hist[obin - 1] += pix_amplit; else { int idx = ((rbin - 1) * d + cbin - 1) * n + n + obin - 1; hist[idx] += pix_amplit; } } } } //对描述子进行归一化 float norm = 0; for (int i = 0; i < len; ++i) { norm = norm + hist[i] * hist[i]; } norm = sqrt(norm); for (int i = 0; i < len; ++i) { hist[i] = hist[i] / norm; } //阈值截断 for (int i = 0; i < len; ++i) { hist[i] = min(hist[i], DESCR_MAG_THR); } //再次归一化 norm = 0; for (int i = 0; i < len; ++i) { norm = norm + hist[i] * hist[i]; } norm = sqrt(norm); for (int i = 0; i < len; ++i) { ptr_des[i] = hist[i] / norm; } } /******************************计算一个特征点的描述子—改进版***********************************/ static void improve_calc_sift_descriptor(const Mat& gauss_image, float main_angle, Point2f pt, float scale, int d, int n, float* descriptor) { int n1 = 16, n2 = 6, n3 = 4; Point ptxy(cvRound(pt.x), cvRound(pt.y)); //坐标取整 float cos_t = cosf(-main_angle * (float)(CV_PI / 180)); //计算主方向的余弦 float sin_t = sinf(-main_angle * (float)(CV_PI / 180)); //计算主方向的正弦 float bin_per_rad_1 = n1 / 360.f; //n=8 float bin_per_rad_2 = n2 / 360.f; //原理特征点部分阈值 float bin_per_rad_3 = n3 / 360.f; //原理特征点部分阈值 float exp_scale = -1.f / (d * d * 0.5f); //权重指数部分 float hist_width = DESCR_SCL_FCTR * scale; //子区域边长,子区域的面积也即采样像素点个数 int radius = cvRound(hist_width * (d + 1) * sqrt(2) * 0.5f);//特征点邻域半径(d+1)*(d+1) int rows = gauss_image.rows, cols = gauss_image.cols; //特征点邻域半径 radius = min(radius, (int)sqrt((double)rows * rows + cols * cols)); cos_t = cos_t / hist_width; sin_t = sin_t / hist_width; int len = (2 * radius + 1) * (2 * radius + 1); //邻域内总像素数 int histlen = (d + 2) * (d + 2) * (n1 + 2); AutoBuffer<float> buf(6 * len + histlen); //X保存水平差分,Y保存竖直差分,Mag保存梯度幅度,Angle保存特征点方向, W保存高斯权重 float* X = buf, * Y = buf + len, * Mag = Y, * Angle = Y + len, * W = Angle + len; float* X2 = buf, * Y2 = buf + len, * Mag2 = Y, * Angle2 = Y + len, * W2 = Angle + len; float* X3 = buf, * Y3 = buf + len, * Mag3 = Y, * Angle3 = Y + len, * W3 = Angle + len; float* RBin = W + len, * CBin = RBin + len, * hist = CBin + len; float* RBin2 = W + len, * CBin2 = RBin + len; float* RBin3 = W + len, * CBin3 = RBin + len; //首先清空直方图数据 for (int i = 0; i < d + 2; ++i) { for (int j = 0; j < d + 2; ++j) { for (int k = 0; k < n + 2; ++k) hist[(i * (d + 2) + j) * (n + 2) + k] = 0.f; } } //把邻域内的像素分配到相应子区域内,计算子区域内每个像素点的权重(子区域即 d*d 中每一个小方格) int k1 = 0, k2 = 0, k3 = 0; vector<int> v; //存放外邻域像素点对应的序号 for (int i = -radius; i < radius; ++i) { for (int j = -radius; j < radius; ++j) { float c_rot = j * cos_t - i * sin_t; //旋转后邻域内采样点的 x 坐标 float r_rot = j * sin_t + i * cos_t; //旋转后邻域内采样点的 y 坐标 float rbin = r_rot + d / 2 - 0.5f; //旋转后的采样点落在子区域的 y 坐标 float cbin = c_rot + d / 2 - 0.5f; //旋转后的采样点落在子区域的 x 坐标 int r = ptxy.y + i, c = ptxy.x + j; //ptxy是高斯金字塔中的坐标 //对离中心点近的部分进行操作 if (abs(i) < (radius / 3) && abs(j) < (radius / 3)) { //这里rbin,cbin范围是(-1,d) if (rbin > -1 && rbin < d && cbin>-1 && cbin < d && r>0 && r < rows - 1 && c>0 && c < cols - 1) { float dx = gauss_image.at<float>(r, c + 1) - gauss_image.at<float>(r, c - 1); float dy = gauss_image.at<float>(r + 1, c) - gauss_image.at<float>(r - 1, c); X[k1] = dx; //邻域内所有像素点的水平差分 Y[k1] = dy; //邻域内所有像素点的竖直差分 RBin[k1] = rbin; //邻域内所有采样点落在子区域的 y 坐标 CBin[k1] = cbin; //邻域内所有采样点落在子区域的 x 坐标 //高斯权值的指数部分 W[k1] = (c_rot * c_rot + r_rot * r_rot) * exp_scale; ++k1; } } //对离中心点远的部分进行操作 else if (abs(i) < (2 * radius / 3) && abs(i) > (radius / 3) && abs(j) < (2 * radius / 3) && abs(j) > (radius / 3)) { //这里rbin,cbin范围是(-1,d) if (rbin > -1 && rbin < d && cbin>-1 && cbin < d && r>0 && r < rows - 1 && c>0 && c < cols - 1) { float dx = gauss_image.at<float>(r, c + 1) - gauss_image.at<float>(r, c - 1); float dy = gauss_image.at<float>(r + 1, c) - gauss_image.at<float>(r - 1, c); X2[k2] = dx; //邻域内所有像素点的水平差分 Y2[k2] = dy; //邻域内所有像素点的竖直差分 RBin2[k2] = rbin; //邻域内所有采样点落在子区域的 y 坐标 CBin2[k2] = cbin; //邻域内所有采样点落在子区域的 x 坐标 //高斯权值的指数部分 W2[k2] = (c_rot * c_rot + r_rot * r_rot) * exp_scale; ++k2; } } else { //这里rbin,cbin范围是(-1,d) if (rbin > -1 && rbin < d && cbin>-1 && cbin < d && r>0 && r < rows - 1 && c>0 && c < cols - 1) { float dx = gauss_image.at<float>(r, c + 1) - gauss_image.at<float>(r, c - 1); float dy = gauss_image.at<float>(r + 1, c) - gauss_image.at<float>(r - 1, c); X3[k3] = dx; //邻域内所有像素点的水平差分 Y3[k3] = dy; //邻域内所有像素点的竖直差分 RBin3[k3] = rbin; //邻域内所有采样点落在子区域的 y 坐标 CBin3[k3] = cbin; //邻域内所有采样点落在子区域的 x 坐标 //高斯权值的指数部分 W3[k3] = (c_rot * c_rot + r_rot * r_rot) * exp_scale; ++k3; } } } } //两个区域内数组的合并拼接 for (int k = 0; k < k2; k++) { X[k1 + k] = X2[k]; Y[k1 + k] = Y2[k]; RBin[k1 + k] = RBin2[k]; CBin[k1 + k] = CBin2[k]; W[k1 + k] = W2[k]; } for (int k = 0; k < k3; k++) { X[k1 + k2 + k] = X3[k]; Y[k1 + k2 + k] = Y3[k]; RBin[k1 + k2 + k] = RBin3[k]; CBin[k1 + k2 + k] = CBin3[k]; W[k1 + k2 + k] = W3[k]; } //计算采样点落在子区域的像素梯度幅度,梯度角度,和高斯权值 len = k1 + k2 + k3; cv::hal::exp(W, W, len); //邻域内所有采样点落在子区域的像素的高斯权值 cv::hal::fastAtan2(Y, X, Angle, len, true); //邻域内所有采样点落在子区域的像素的梯度方向,角度范围是0-360度 cv::hal::magnitude(X, Y, Mag, len); //邻域内所有采样点落在子区域的像素的梯度幅度 //计算每个特征点的特征描述子 for (int k = 0; k < len; ++k) { float rbin = RBin[k], cbin = CBin[k]; //子区域内像素点坐标,rbin,cbin范围是(-1,d) //离特征点进的邻域 if (k < k1) { //子区域内像素点处理后的方向 float obin = (Angle[k] - main_angle) * bin_per_rad_1; float mag = Mag[k] * W[k]; //子区域内像素点乘以权值后的梯度幅值 int r0 = cvFloor(rbin); //ro取值集合是{-1,0,1,2,3},向下取整 int c0 = cvFloor(cbin); //c0取值集合是{-1,0,1,2,3} int o0 = cvFloor(obin); rbin = rbin - r0; //子区域内像素点坐标的小数部分,用于线性插值 cbin = cbin - c0; obin = obin - o0; //限制范围为梯度直方图横坐标[0,n) if (o0 < 0) o0 = o0 + n1; if (o0 >= n1) o0 = o0 - n1; //三线性插值用于计算落在两个子区域之间的像素对两个子区域的作用,并把其分配到对应子区域的8个方向上 //使用三线性插值(即三维)方法,计算直方图 float v_r1 = mag * rbin; //第二行分配的值 float v_r0 = mag - v_r1; //第一行分配的值 float v_rc11 = v_r1 * cbin; //第二行第二列分配的值 float v_rc10 = v_r1 - v_rc11; //第二行第一列分配的值 float v_rc01 = v_r0 * cbin; //第一行第二列分配的值 float v_rc00 = v_r0 - v_rc01; float v_rco111 = v_rc11 * obin; //第二行第二列第二个方向上分配的值,每个采样点去邻近两个方向 float v_rco110 = v_rc11 - v_rco111; //第二行第二列第一个方向上分配的值 float v_rco101 = v_rc10 * obin; float v_rco100 = v_rc10 - v_rco101; float v_rco011 = v_rc01 * obin; float v_rco010 = v_rc01 - v_rco011; float v_rco001 = v_rc00 * obin; float v_rco000 = v_rc00 - v_rco001; //该像素所在网格的索引 int idx = ((r0 + 1) * (d + 2) + c0 + 1) * (n1 + 2) + o0; hist[idx] += v_rco000; hist[idx + 1] += v_rco001; hist[idx + n1 + 2] += v_rco010; hist[idx + n1 + 3] += v_rco011; hist[idx + (d + 2) * (n1 + 2)] += v_rco100; hist[idx + (d + 2) * (n1 + 2) + 1] += v_rco101; hist[idx + (d + 3) * (n1 + 2)] += v_rco110; hist[idx + (d + 3) * (n1 + 2) + 1] += v_rco111; } //离特征点远的邻域 else if (k >= k1 && k < k2) { //子区域内像素点处理后的方向 float obin = (Angle[k] - main_angle) * bin_per_rad_2; float mag = Mag[k] * W[k]; //子区域内像素点乘以权值后的梯度幅值 int r0 = cvFloor(rbin); //ro取值集合是{-1,0,1,2,3},向下取整 int c0 = cvFloor(cbin); //c0取值集合是{-1,0,1,2,3} int o0 = cvFloor(obin); rbin = rbin - r0; //子区域内像素点坐标的小数部分,用于线性插值 cbin = cbin - c0; obin = obin - o0; //限制范围为梯度直方图横坐标[0,n) if (o0 < 0) o0 = o0 + n2; if (o0 >= n1) o0 = o0 - n2; //三线性插值用于计算落在两个子区域之间的像素对两个子区域的作用,并把其分配到对应子区域的8个方向上 //使用三线性插值(即三维)方法,计算直方图 float v_r1 = mag * rbin; //第二行分配的值 float v_r0 = mag - v_r1; //第一行分配的值 float v_rc11 = v_r1 * cbin; //第二行第二列分配的值 float v_rc10 = v_r1 - v_rc11; //第二行第一列分配的值 float v_rc01 = v_r0 * cbin; //第一行第二列分配的值 float v_rc00 = v_r0 - v_rc01; float v_rco111 = v_rc11 * obin; //第二行第二列第二个方向上分配的值,每个采样点去邻近两个方向 float v_rco110 = v_rc11 - v_rco111; //第二行第二列第一个方向上分配的值 float v_rco101 = v_rc10 * obin; float v_rco100 = v_rc10 - v_rco101; float v_rco011 = v_rc01 * obin; float v_rco010 = v_rc01 - v_rco011; float v_rco001 = v_rc00 * obin; float v_rco000 = v_rc00 - v_rco001; //该像素所在网格的索引 int idx = ((r0 + 1) * (d + 2) + c0 + 1) * (n2 + 2) + o0; hist[idx] += v_rco000; hist[idx + 1] += v_rco001; hist[idx + n2 + 2] += v_rco010; hist[idx + n2 + 3] += v_rco011; hist[idx + (d + 2) * (n2 + 2)] += v_rco100; hist[idx + (d + 2) * (n2 + 2) + 1] += v_rco101; hist[idx + (d + 3) * (n2 + 2)] += v_rco110; hist[idx + (d + 3) * (n2 + 2) + 1] += v_rco111; } else { //子区域内像素点处理后的方向 float obin = (Angle[k] - main_angle) * bin_per_rad_3; float mag = Mag[k] * W[k]; //子区域内像素点乘以权值后的梯度幅值 int r0 = cvFloor(rbin); //ro取值集合是{-1,0,1,2,3},向下取整 int c0 = cvFloor(cbin); //c0取值集合是{-1,0,1,2,3} int o0 = cvFloor(obin); rbin = rbin - r0; //子区域内像素点坐标的小数部分,用于线性插值 cbin = cbin - c0; obin = obin - o0; //限制范围为梯度直方图横坐标[0,n) if (o0 < 0) o0 = o0 + n3; if (o0 >= n1) o0 = o0 - n3; //三线性插值用于计算落在两个子区域之间的像素对两个子区域的作用,并把其分配到对应子区域的8个方向上 //使用三线性插值(即三维)方法,计算直方图 float v_r1 = mag * rbin; //第二行分配的值 float v_r0 = mag - v_r1; //第一行分配的值 float v_rc11 = v_r1 * cbin; //第二行第二列分配的值 float v_rc10 = v_r1 - v_rc11; //第二行第一列分配的值 float v_rc01 = v_r0 * cbin; //第一行第二列分配的值 float v_rc00 = v_r0 - v_rc01; float v_rco111 = v_rc11 * obin; //第二行第二列第二个方向上分配的值,每个采样点去邻近两个方向 float v_rco110 = v_rc11 - v_rco111; //第二行第二列第一个方向上分配的值 float v_rco101 = v_rc10 * obin; float v_rco100 = v_rc10 - v_rco101; float v_rco011 = v_rc01 * obin; float v_rco010 = v_rc01 - v_rco011; float v_rco001 = v_rc00 * obin; float v_rco000 = v_rc00 - v_rco001; //该像素所在网格的索引 int idx = ((r0 + 1) * (d + 2) + c0 + 1) * (n3 + 2) + o0; hist[idx] += v_rco000; hist[idx + 1] += v_rco001; hist[idx + n3 + 2] += v_rco010; hist[idx + n3 + 3] += v_rco011; hist[idx + (d + 2) * (n3 + 2)] += v_rco100; hist[idx + (d + 2) * (n3 + 2) + 1] += v_rco101; hist[idx + (d + 3) * (n3 + 2)] += v_rco110; hist[idx + (d + 3) * (n3 + 2) + 1] += v_rco111; } } //由于圆周循环的特性,对计算以后幅角小于 0 度或大于 360 度的值重新进行调整,使 //其在 0~360 度之间 for (int i = 0; i < d; ++i) { for (int j = 0; j < d; ++j) { int idx = ((i + 1) * (d + 2) + (j + 1)) * (n + 2); hist[idx] += hist[idx + n]; //hist[idx + 1] += hist[idx + n + 1];//opencv源码中这句话是多余的,hist[idx + n + 1]永远是0.0 for (int k = 0; k < n; ++k) descriptor[(i * d + j) * n + k] = hist[idx + k]; } } //对描述子进行归一化 int lenght = d * d * n; float norm = 0; //计算特征描述向量的模值的平方 for (int i = 0; i < lenght; ++i) { norm = norm + descriptor[i] * descriptor[i]; } norm = sqrt(norm); //特征描述向量的模值 //此次归一化能去除光照的影响 for (int i = 0; i < lenght; ++i) { descriptor[i] = descriptor[i] / norm; } //阈值截断,去除特征描述向量中大于 0.2 的值,能消除非线性光照的影响(相机饱和度对某些放的梯度影响较大,对方向的影响较小) for (int i = 0; i < lenght; ++i) { descriptor[i] = min(descriptor[i], DESCR_MAG_THR); } //再次归一化,能够提高特征的独特性 norm = 0; for (int i = 0; i < lenght; ++i) { norm = norm + descriptor[i] * descriptor[i]; } norm = sqrt(norm); for (int i = 0; i < lenght; ++i) { descriptor[i] = descriptor[i] / norm; } } /********************************该函数计算所有特征点特征描述子***************************/ /*gauss_pyr表示高斯金字塔 keypoints表示特征点、 descriptors表示生成的特征点的描述子*/ void mySift::calc_sift_descriptors(const vector<vector<Mat>>& gauss_pyr, vector<KeyPoint>& keypoints, Mat& descriptors, const vector<Mat>& amplit, const vector<Mat>& orient) { int d = DESCR_WIDTH; //d=4,特征点邻域网格个数是d x d int n = DESCR_HIST_BINS; //n=8,每个网格特征点梯度角度等分为8个方向 descriptors.create(keypoints.size(), d * d * n, CV_32FC1); //分配空间 for (size_t i = 0; i < keypoints.size(); ++i) //对于每一个特征点 { int octaves, layer; //得到特征点所在的组号,层号 octaves = keypoints[i].octave & 255; layer = (keypoints[i].octave >> 8) & 255; //得到特征点相对于本组的坐标,不是最底层 Point2f pt(keypoints[i].pt.x / (1 << octaves), keypoints[i].pt.y / (1 << octaves)); float scale = keypoints[i].size / (1 << octaves); //得到特征点相对于本组的尺度 float main_angle = keypoints[i].angle; //特征点主方向 //计算该点的描述子 calc_sift_descriptor(gauss_pyr[octaves][layer], main_angle, pt, scale, d, n, descriptors.ptr<float>((int)i)); //improve_calc_sift_descriptor(gauss_pyr[octaves][layer], main_angle, pt, scale, d, n, descriptors.ptr<float>((int)i)); //calc_gloh_descriptor(amplit[octaves], orient[octaves], pt, scale, main_angle, d, n, descriptors.ptr<float>((int)i)); if (double_size)//如果图像尺寸扩大一倍 { keypoints[i].pt.x = keypoints[i].pt.x / 2.f; keypoints[i].pt.y = keypoints[i].pt.y / 2.f; } } } /*************该函数构建SAR_SIFT尺度空间*****************/ /*image表示输入的原始图像 sar_harris_fun表示尺度空间的Sar_harris函数 amplit表示尺度空间像素的梯度幅度 orient表示尺度空间像素的梯度方向 */ void mySift::build_sar_sift_space(const Mat& image, vector<Mat>& sar_harris_fun, vector<Mat>& amplit, vector<Mat>& orient) { //转换输入图像格式 Mat gray_image; if (image.channels() != 1) cvtColor(image, gray_image, CV_RGB2GRAY); else gray_image = image; //把图像转换为0-1之间的浮点数据 Mat float_image; double ratio = pow(2, 1.0 / 3.0); //相邻两层的尺度比,默认是2^(1/3) //在这里转换为0-1之间的浮点数据和转换为0-255之间的浮点数据,效果是一样的 //gray_image.convertTo(float_image, CV_32FC1, 1.f / 255.f, 0.f);//转换为0-1之间 gray_image.convertTo(float_image, CV_32FC1, 1, 0.f); //转换为0-255之间的浮点数 //分配内存 sar_harris_fun.resize(Mmax); amplit.resize(Mmax); orient.resize(Mmax); for (int i = 0; i < Mmax; ++i) { float scale = (float)sigma * (float)pow(ratio, i); //获得当前层的尺度 int radius = cvRound(2 * scale); Mat kernel; roewa_kernel(radius, scale, kernel); //四个滤波模板生成 Mat W34 = Mat::zeros(2 * radius + 1, 2 * radius + 1, CV_32FC1); Mat W12 = Mat::zeros(2 * radius + 1, 2 * radius + 1, CV_32FC1); Mat W14 = Mat::zeros(2 * radius + 1, 2 * radius + 1, CV_32FC1); Mat W23 = Mat::zeros(2 * radius + 1, 2 * radius + 1, CV_32FC1); kernel(Range(radius + 1, 2 * radius + 1), Range::all()).copyTo(W34(Range(radius + 1, 2 * radius + 1), Range::all())); kernel(Range(0, radius), Range::all()).copyTo(W12(Range(0, radius), Range::all())); kernel(Range::all(), Range(radius + 1, 2 * radius + 1)).copyTo(W14(Range::all(), Range(radius + 1, 2 * radius + 1))); kernel(Range::all(), Range(0, radius)).copyTo(W23(Range::all(), Range(0, radius))); //滤波 Mat M34, M12, M14, M23; double eps = 0.00001; filter2D(float_image, M34, CV_32FC1, W34, Point(-1, -1), eps); filter2D(float_image, M12, CV_32FC1, W12, Point(-1, -1), eps); filter2D(float_image, M14, CV_32FC1, W14, Point(-1, -1), eps); filter2D(float_image, M23, CV_32FC1, W23, Point(-1, -1), eps); //计算水平梯度和竖直梯度 Mat Gx, Gy; log((M14) / (M23), Gx); log((M34) / (M12), Gy); //计算梯度幅度和梯度方向 magnitude(Gx, Gy, amplit[i]); phase(Gx, Gy, orient[i], true); //构建sar-Harris矩阵 //Mat Csh_11 = log(scale)*log(scale)*Gx.mul(Gx); //Mat Csh_12 = log(scale)*log(scale)*Gx.mul(Gy); //Mat Csh_22 = log(scale)*log(scale)*Gy.mul(Gy); Mat Csh_11 = scale * scale * Gx.mul(Gx); Mat Csh_12 = scale * scale * Gx.mul(Gy); Mat Csh_22 = scale * scale * Gy.mul(Gy);//此时阈值为0.8 //Mat Csh_11 = Gx.mul(Gx); //Mat Csh_12 = Gx.mul(Gy); //Mat Csh_22 = Gy.mul(Gy);//此时阈值为0.8/100 //高斯权重 float gauss_sigma = sqrt(2.f) * scale; int size = cvRound(3 * gauss_sigma); Size kern_size(2 * size + 1, 2 * size + 1); GaussianBlur(Csh_11, Csh_11, kern_size, gauss_sigma, gauss_sigma); GaussianBlur(Csh_12, Csh_12, kern_size, gauss_sigma, gauss_sigma); GaussianBlur(Csh_22, Csh_22, kern_size, gauss_sigma, gauss_sigma); /*Mat gauss_kernel;//自定义圆形高斯核 gauss_circle(size, gauss_sigma, gauss_kernel); filter2D(Csh_11, Csh_11, CV_32FC1, gauss_kernel); filter2D(Csh_12, Csh_12, CV_32FC1, gauss_kernel); filter2D(Csh_22, Csh_22, CV_32FC1, gauss_kernel);*/ Mat Csh_21 = Csh_12; //构建sar_harris函数 Mat temp_add = Csh_11 + Csh_22; double d = 0.04; //sar_haiirs函数表达式中的任意参数,默认是0.04 sar_harris_fun[i] = Csh_11.mul(Csh_22) - Csh_21.mul(Csh_12) - (float)d * temp_add.mul(temp_add); } } /***************该函数计算所有特征点的特征向量*************/ /*amplit表示尺度空间像素幅度 orient表示尺度空间像素梯度角度 keys表示检测到的特征点 descriptors表示特征点描述子向量,【M x N】,M表示描述子个数,N表示描述子维度 */ void mySift::calc_gloh_descriptors(const vector<Mat>& amplit, const vector<Mat>& orient, const vector<KeyPoint>& keys, Mat& descriptors) { int d = SAR_SIFT_GLOH_ANG_GRID; //d=4或者d=8 int n = SAR_SIFT_DES_ANG_BINS; //n=8默认 int num_keys = (int)keys.size(); int grids = 2 * d + 1; //descriptors.create(num_keys, grids * n, CV_32FC1); descriptors.create(num_keys, grids * n, CV_32FC1); for (int i = 0; i < num_keys; ++i) { int octaves = keys[i].octave & 255; //特征点所在层 float* ptr_des = descriptors.ptr<float>(i); float scale = keys[i].size / (1 << octaves); //得到特征点相对于本组的尺度; //特征点所在层的尺度 float main_ori = keys[i].angle; //特征点主方向 //得到特征点相对于本组的坐标,不是最底层 Point2f point(keys[i].pt.x / (1 << octaves), keys[i].pt.y / (1 << octaves)); cout << "layer=" << octaves << endl; cout << "scale=" << scale << endl; //计算该特征点的特征描述子 calc_gloh_descriptor(amplit[octaves], orient[octaves], point, scale, main_ori, d, n, ptr_des); } } //特征点检测和特征点描述把整个 SIFT 算子都涵盖在内了// /******************************特征点检测*********************************/ /*image表示输入的图像 gauss_pyr表示生成的高斯金字塔 dog_pyr表示生成的高斯差分DOG金字塔 keypoints表示检测到的特征点 vector<float>& cell_contrast 用于存放一个单元格中所有特征点的对比度 vector<float>& cell_contrasts用于存放一个尺度层中所有单元格中特征点的对比度 vector<vector<vector<float>>>& all_cell_contrasts用于存放所有尺度层中所有单元格的对比度 vector<vector<float>>& average_contrast用于存放所有尺度层中多有单元格的平均对比度*/ void mySift::detect(const Mat& image, vector<vector<Mat>>& gauss_pyr, vector<vector<Mat>>& dog_pyr, vector<KeyPoint>& keypoints, vector<vector<vector<float>>>& all_cell_contrasts, vector<vector<float>>& average_contrast, vector<vector<int>>& n_cells, vector<int>& num_cell, vector<vector<int>>& available_n, vector<int>& available_num, vector<KeyPoint>& final_keypoints, vector<KeyPoint>& Final_keypoints, vector<KeyPoint>& Final_Keypoints) { if (image.empty() || image.depth() != CV_8U) CV_Error(CV_StsBadArg, "输入图像为空,或者图像深度不是CV_8U"); //计算高斯金字塔组数 int nOctaves; nOctaves = num_octaves(image); //生成高斯金字塔第一层图像 Mat init_gauss; create_initial_image(image, init_gauss); //生成高斯尺度空间图像 build_gaussian_pyramid(init_gauss, gauss_pyr, nOctaves); //生成高斯差分金字塔(DOG金字塔,or LOG金字塔) build_dog_pyramid(dog_pyr, gauss_pyr); //在DOG金字塔上检测特征点 find_scale_space_extrema1(dog_pyr, gauss_pyr, keypoints); //原始 SIFT 算子 // UR_SIFT,仅仅是检测特征点,并未对不理想的点进行筛选 /*UR_SIFT_find_scale_space_extrema(dog_pyr, gauss_pyr, keypoints, all_cell_contrasts, average_contrast, n_cells, num_cell, available_n, available_num, final_keypoints, Final_keypoints, Final_Keypoints, N);*/ //如果指定了特征点个数,并且设定的设置小于默认检测的特征点个数 if (nfeatures != 0 && nfeatures < (int)keypoints.size()) { //特征点响应值(即对比度,对比度越小越不稳定)从大到小排序 sort(keypoints.begin(), keypoints.end(), [](const KeyPoint& a, const KeyPoint& b) {return a.response > b.response; }); //删除点多余的特征点 keypoints.erase(keypoints.begin() + nfeatures, keypoints.end()); } } /**********************特征点描述*******************/ /*gauss_pyr表示高斯金字塔 keypoints表示特征点集合 descriptors表示特征点的描述子*/ void mySift::comput_des(const vector<vector<Mat>>& gauss_pyr, vector<KeyPoint>& final_keypoints, const vector<Mat>& amplit, const vector<Mat>& orient, Mat& descriptors) { calc_sift_descriptors(gauss_pyr, final_keypoints, descriptors, amplit, orient); }
3、 myMatch.h
#pragma once #include<opencv2\core\core.hpp> #include<opencv2\features2d\features2d.hpp> #include<vector> #include<string> #include<iostream> const double dis_ratio1 = 0.75; //最近邻和次近邻距离比阈值,就目前测试来看 dis_ratio = 0.75 时正确匹配的数量相对较多 const double dis_ratio2 = 0.8; const double dis_ratio3 = 0.9; const float ransac_error = 1.5; //ransac算法误差阈值 const double FSC_ratio_low = 0.8; const double FSC_ratio_up = 1; const int pointsCount = 9; // k均值聚类数据点个数 const int clusterCount = 3; // k均值聚类质心的个数 enum DIS_CRIT { Euclidean = 0, COS }; //距离度量准则 using namespace std; using namespace cv; class myMatch { public: myMatch(); ~myMatch(); //该函数根据正确的匹配点对,计算出图像之间的变换关系 Mat LMS(const Mat& match1_xy, const Mat& match2_xy, string model, float& rmse); //改进版LMS超定方程 Mat improve_LMS(const Mat& match1_xy, const Mat& match2_xy, string model, float& rmse); //该函数删除错误的匹配点对 Mat ransac(const vector<Point2f>& points_1, const vector<Point2f>& points_2, string model, float threshold, vector<bool>& inliers, float& rmse); //绘制棋盘图像 void mosaic_map(const Mat& image_1, const Mat& image_2, Mat& chessboard_1, Mat& chessboard_2, Mat& mosaic_image, int width); //该函数把两幅配准后的图像进行融合镶嵌 void image_fusion(const Mat& image_1, const Mat& image_2, const Mat T, Mat& fusion_image, Mat& matched_image); //该函数进行描述子的最近邻和次近邻匹配 void match_des(const Mat& des_1, const Mat& des_2, vector<vector<DMatch>>& dmatchs, DIS_CRIT dis_crite); //建立尺度直方图、ROM 直方图 void scale_ROM_Histogram(const vector<DMatch>& matches, float* scale_hist, float* ROM_hist, int n); //该函数删除错误匹配点对,并进行配准 Mat match(const Mat& image_1, const Mat& image_2, const vector<vector<DMatch>>& dmatchs, vector<KeyPoint> keys_1, vector<KeyPoint> keys_2, string model, vector<DMatch>& right_matchs, Mat& matched_line, vector<DMatch>& init_matchs); };
4、 myMatch.cpp
#include"myMatch.h" #include<opencv2\imgproc\types_c.h> #include<opencv2\imgproc\imgproc.hpp> #include<opencv2\highgui\highgui.hpp> #include<opencv2\features2d\features2d.hpp> //特征提取 #include<algorithm> #include<vector> #include<cmath> #include<opencv.hpp> #include <numeric> //用于容器元素求和 using namespace std; using namespace cv; //using namespace gpu; RNG rng(100); myMatch::myMatch() { } /********该函数根据正确的匹配点对,计算出图像之间的变换关系********/ /*注意:输入几个点都能计算对应的 x 矩阵,(2N,8)*(8,1)=(2N,1) match1_xy表示参考图像特征点坐标集合,[M x 2]矩阵,M表示特征的个数 match2_xy表示待配准图像特征点集合,[M x 2]矩阵,M表示特征点集合 model表示变换类型,“相似变换”,"仿射变换","透视变换" rmse表示均方根误差 返回值为计算得到的3 x 3矩阵参数 */ Mat myMatch::LMS(const Mat& match1_xy, const Mat& match2_xy, string model, float& rmse) { if (match1_xy.rows != match2_xy.rows) CV_Error(CV_StsBadArg, "LMS模块输入特征点对个数不一致!"); if (!(model == string("affine") || model == string("similarity") || model == string("perspective") || model == string("projective"))) CV_Error(CV_StsBadArg, "LMS模块图像变换类型输入错误!"); const int N = match1_xy.rows; //特征点个数 Mat match2_xy_trans, match1_xy_trans; //特征点坐标转置 transpose(match1_xy, match1_xy_trans); //矩阵转置(2,M) transpose(match2_xy, match2_xy_trans); Mat change = Mat::zeros(3, 3, CV_32FC1); //变换矩阵 //A*X=B,接下来部分仿射变换和透视变换一样,如果特征点个数是M,则A=[2*M,6]矩阵 //A=[x1,y1,0,0,1,0;0,0,x1,y1,0,1;.....xn,yn,0,0,1,0;0,0,xn,yn,0,1],应该是改版过的 Mat A = Mat::zeros(2 * N, 6, CV_32FC1); for (int i = 0; i < N; ++i) { A.at<float>(2 * i, 0) = match2_xy.at<float>(i, 0);//x A.at<float>(2 * i, 1) = match2_xy.at<float>(i, 1);//y A.at<float>(2 * i, 4) = 1.f; A.at<float>(2 * i + 1, 2) = match2_xy.at<float>(i, 0); A.at<float>(2 * i + 1, 3) = match2_xy.at<float>(i, 1); A.at<float>(2 * i + 1, 5) = 1.f; } //如果特征点个数是M,那个B=[2*M,1]矩阵 //B=[u1,v1,u2,v2,.....,un,vn] Mat B; B.create(2 * N, 1, CV_32FC1); for (int i = 0; i < N; ++i) { B.at<float>(2 * i, 0) = match1_xy.at<float>(i, 0); //x B.at<float>(2 * i + 1, 0) = match1_xy.at<float>(i, 1);//y } //如果是仿射变换 if (model == string("affine")) { Vec6f values; solve(A, B, values, DECOMP_QR); change = (Mat_<float>(3, 3) << values(0), values(1), values(4), values(2), values(3), values(5), +0.0f, +0.0f, 1.0f); Mat temp_1 = change(Range(0, 2), Range(0, 2)); //尺度和旋转量 Mat temp_2 = change(Range(0, 2), Range(2, 3)); //平移量 Mat match2_xy_change = temp_1 * match2_xy_trans + repeat(temp_2, 1, N); Mat diff = match2_xy_change - match1_xy_trans; //求差 pow(diff, 2.f, diff); rmse = (float)sqrt(sum(diff)(0) * 1.0) / N; //sum输出是各个通道的和, / N 初始实在括号里面 } //如果是透视变换 else if (model == string("perspective")) { /*透视变换模型 [u'*w,v'*w, w]'=[u,v,w]' = [a1, a2, a5; a3, a4, a6; a7, a8, 1] * [x, y, 1]' [u',v']'=[x,y,0,0,1,0,-u'x, -u'y; 0, 0, x, y, 0, 1, -v'x,-v'y] * [a1, a2, a3, a4, a5, a6, a7, a8]' 即,Y = A*X */ //构造 A 矩阵的后两列 Mat A2; A2.create(2 * N, 2, CV_32FC1); for (int i = 0; i < N; ++i) { A2.at<float>(2 * i, 0) = match1_xy.at<float>(i, 0) * match2_xy.at<float>(i, 0) * (-1.f); A2.at<float>(2 * i, 1) = match1_xy.at<float>(i, 0) * match2_xy.at<float>(i, 1) * (-1.f); A2.at<float>(2 * i + 1, 0) = match1_xy.at<float>(i, 1) * match2_xy.at<float>(i, 0) * (-1.f); A2.at<float>(2 * i + 1, 1) = match1_xy.at<float>(i, 1) * match2_xy.at<float>(i, 1) * (-1.f); } Mat A1; //完成的 A 矩阵,(8,8) A1.create(2 * N, 8, CV_32FC1); A.copyTo(A1(Range::all(), Range(0, 6))); A2.copyTo(A1(Range::all(), Range(6, 8))); Mat values; //values中存放的是待求的8个参数 solve(A1, B, values, DECOMP_QR); //(2N,8)*(8,1)=(2N,1)好像本身就有点超定矩阵的意思 change.at<float>(0, 0) = values.at<float>(0); change.at<float>(0, 1) = values.at<float>(1); change.at<float>(0, 2) = values.at<float>(4); change.at<float>(1, 0) = values.at<float>(2); change.at<float>(1, 1) = values.at<float>(3); change.at<float>(1, 2) = values.at<float>(5); change.at<float>(2, 0) = values.at<float>(6); change.at<float>(2, 1) = values.at<float>(7); change.at<float>(2, 2) = 1.f; Mat temp1 = Mat::ones(1, N, CV_32FC1); Mat temp2; temp2.create(3, N, CV_32FC1); match2_xy_trans.copyTo(temp2(Range(0, 2), Range::all())); temp1.copyTo(temp2(Range(2, 3), Range::all())); Mat match2_xy_change = change * temp2; //待配准图像中的特征点在参考图中的映射结果 Mat match2_xy_change_12 = match2_xy_change(Range(0, 2), Range::all()); //float* temp_ptr = match2_xy_change.ptr<float>(2); float* temp_ptr = match2_xy_change.ptr<float>(2); for (int i = 0; i < N; ++i) { float div_temp = temp_ptr[i]; match2_xy_change_12.at<float>(0, i) = match2_xy_change_12.at<float>(0, i) / div_temp; match2_xy_change_12.at<float>(1, i) = match2_xy_change_12.at<float>(1, i) / div_temp; } Mat diff = match2_xy_change_12 - match1_xy_trans; pow(diff, 2, diff); rmse = (float)sqrt(sum(diff)(0) * 1.0) / N; //sum输出是各个通道的和,rmse为输入点的均方误差 } //如果是相似变换 else if (model == string("similarity")) { /*[x, y, 1, 0; y, -x, 0, 1] * [a, b, c, d]'=[u,v]*/ Mat A3; A3.create(2 * N, 4, CV_32FC1); for (int i = 0; i < N; ++i) { A3.at<float>(2 * i, 0) = match2_xy.at<float>(i, 0); A3.at<float>(2 * i, 1) = match2_xy.at<float>(i, 1); A3.at<float>(2 * i, 2) = 1.f; A3.at<float>(2 * i, 3) = 0.f; A3.at<float>(2 * i + 1, 0) = match2_xy.at<float>(i, 1); A3.at<float>(2 * i + 1, 1) = match2_xy.at<float>(i, 0) * (-1.f); A3.at<float>(2 * i + 1, 2) = 0.f; A3.at<float>(2 * i + 1, 3) = 1.f; } Vec4f values; solve(A3, B, values, DECOMP_QR); change = (Mat_<float>(3, 3) << values(0), values(1), values(2), values(1) * (-1.0f), values(0), values(3), +0.f, +0.f, 1.f); Mat temp_1 = change(Range(0, 2), Range(0, 2));//尺度和旋转量 Mat temp_2 = change(Range(0, 2), Range(2, 3));//平移量 Mat match2_xy_change = temp_1 * match2_xy_trans + repeat(temp_2, 1, N); Mat diff = match2_xy_change - match1_xy_trans;//求差 pow(diff, 2, diff); rmse = (float)sqrt(sum(diff)(0) * 1.0) / N;//sum输出是各个通道的和 } return change; } /********改进版LMS超定方程********/ Mat myMatch::improve_LMS(const Mat& match1_xy, const Mat& match2_xy, string model, float& rmse) { if (match1_xy.rows != match2_xy.rows) CV_Error(CV_StsBadArg, "LMS模块输入特征点对个数不一致!"); if (!(model == string("affine") || model == string("similarity") || model == string("perspective"))) CV_Error(CV_StsBadArg, "LMS模块图像变换类型输入错误!"); const int N = match1_xy.rows; //特征点个数 Mat match2_xy_trans, match1_xy_trans; //特征点坐标转置 transpose(match1_xy, match1_xy_trans); //矩阵转置(2,M) transpose(match2_xy, match2_xy_trans); Mat change = Mat::zeros(3, 3, CV_32FC1); //变换矩阵 //A*X=B,接下来部分仿射变换和透视变换一样,如果特征点个数是M,则A=[2*M,6]矩阵 //A=[x1,y1,0,0,1,0;0,0,x1,y1,0,1;.....xn,yn,0,0,1,0;0,0,xn,yn,0,1],应该是改版过的 Mat A = Mat::zeros(2 * N, 6, CV_32FC1); for (int i = 0; i < N; ++i) { A.at<float>(2 * i, 0) = match2_xy.at<float>(i, 0);//x A.at<float>(2 * i, 1) = match2_xy.at<float>(i, 1);//y A.at<float>(2 * i, 4) = 1.f; //A.at<float>(2 * i, 0) = match2_xy.at<float>(i, 0);//x //A.at<float>(2 * i, 1) = match2_xy.at<float>(i, 1);//y //A.at<float>(2 * i, 2) = 1.f; A.at<float>(2 * i + 1, 2) = match2_xy.at<float>(i, 0); A.at<float>(2 * i + 1, 3) = match2_xy.at<float>(i, 1); A.at<float>(2 * i + 1, 5) = 1.f; /*A.at<float>(2 * i + 1, 3) = match2_xy.at<float>(i, 0); A.at<float>(2 * i + 1, 4) = match2_xy.at<float>(i, 1); A.at<float>(2 * i + 1, 5) = 1.f;*/ } //如果特征点个数是M,那个B=[2*M,1]矩阵 //B=[u1,v1,u2,v2,.....,un,vn] Mat B; B.create(2 * N, 1, CV_32FC1); for (int i = 0; i < N; ++i) { B.at<float>(2 * i, 0) = match1_xy.at<float>(i, 0); //x B.at<float>(2 * i + 1, 0) = match1_xy.at<float>(i, 1);//y } //如果是仿射变换 if (model == string("affine")) { Vec6f values; solve(A, B, values, DECOMP_QR); change = (Mat_<float>(3, 3) << values(0), values(1), values(4), values(2), values(3), values(5), +0.0f, +0.0f, 1.0f); Mat temp_1 = change(Range(0, 2), Range(0, 2));//尺度和旋转量 Mat temp_2 = change(Range(0, 2), Range(2, 3));//平移量 Mat match2_xy_change = temp_1 * match2_xy_trans + repeat(temp_2, 1, N); Mat diff = match2_xy_change - match1_xy_trans;//求差 pow(diff, 2.f, diff); rmse = (float)sqrt(sum(diff)(0) * 1.0 / N);//sum输出是各个通道的和 } //如果是透视变换 else if (model == string("perspective")) { /*透视变换模型 [u'*w,v'*w, w]'=[u,v,w]' = [a1, a2, a5; a3, a4, a6; a7, a8, 1] * [x, y, 1]' [u',v']'=[x,y,0,0,1,0,-u'x, -u'y; 0, 0, x, y, 0, 1, -v'x,-v'y] * [a1, a2, a3, a4, a5, a6, a7, a8]' 即,Y = A*X */ //构造 A 矩阵的后两列 Mat A2; A2.create(2 * N, 2, CV_32FC1); for (int i = 0; i < N; ++i) { A2.at<float>(2 * i, 0) = match1_xy.at<float>(i, 0) * match2_xy.at<float>(i, 0) * (-1.f); A2.at<float>(2 * i, 1) = match1_xy.at<float>(i, 0) * match2_xy.at<float>(i, 1) * (-1.f); A2.at<float>(2 * i + 1, 0) = match1_xy.at<float>(i, 1) * match2_xy.at<float>(i, 0) * (-1.f); A2.at<float>(2 * i + 1, 1) = match1_xy.at<float>(i, 1) * match2_xy.at<float>(i, 1) * (-1.f); } Mat A1; //完整的 A 矩阵,(8,8) A1.create(2 * N, 8, CV_32FC1); A.copyTo(A1(Range::all(), Range(0, 6))); A2.copyTo(A1(Range::all(), Range(6, 8))); Mat AA1, balance; Mat evects, evals; transpose(A1, AA1); //求矩阵 A1 的转置 balance = AA1 * A1; double a[8][8]; for (int i = 0; i < 8; i++) { for (int j = 0; j < 8; j++) { a[i][j] = balance.at<float>(i, j); } } //构造输入矩阵 CvMat SrcMatrix = cvMat(8, 8, CV_32FC1, a); double b[8][8] = { {0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0}, {0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0}, {0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0}, {0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0}, {0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0}, {0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0}, {0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0}, {0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0} }; // 构造输出特征向量矩阵,特征向量按行存储,且与特征值相对应 CvMat ProVector = cvMat(8, 8, CV_32FC1, b); // 构造输出特征值矩阵,特征值按降序配列 double c[8] = { 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 }; CvMat ProValue = cvMat(8, 1, CV_32FC1, c); //求特征向量,最后一行特征向量即对应的 H 矩阵的参数 cvEigenVV(&SrcMatrix, &ProVector, &ProValue, 1.0e-6F); 输出特征向量矩阵 //for (int i = 0; i < 2; i++) //{ // for (int j = 0; j < 2; j++) // printf("%f\t", cvmGet(&ProVector, i, j)); // printf("\n"); //} //把计算得到的最小特征值对应的特征变量赋给 H 矩阵 change.at<float>(0, 0) = cvmGet(&ProVector, 7, 0); change.at<float>(0, 1) = cvmGet(&ProVector, 7, 1); change.at<float>(0, 2) = cvmGet(&ProVector, 7, 2); change.at<float>(1, 0) = cvmGet(&ProVector, 7, 3); change.at<float>(1, 1) = cvmGet(&ProVector, 7, 4); change.at<float>(1, 2) = cvmGet(&ProVector, 7, 5); change.at<float>(2, 0) = cvmGet(&ProVector, 7, 6); change.at<float>(2, 1) = cvmGet(&ProVector, 7, 7); change.at<float>(2, 2) = 1.f; Mat temp1 = Mat::ones(1, N, CV_32FC1); Mat temp2; //存放处理后的特征点(x,y,1)T temp2.create(3, N, CV_32FC1); match2_xy_trans.copyTo(temp2(Range(0, 2), Range::all())); temp1.copyTo(temp2(Range(2, 3), Range::all())); Mat match2_xy_change = change * temp2; //待配准图像中的特征点在参考图中的映射结果 Mat match2_xy_change_12 = match2_xy_change(Range(0, 2), Range::all()); //float* temp_ptr = match2_xy_change.ptr<float>(2); float* temp_ptr = match2_xy_change.ptr<float>(2); for (int i = 0; i < N; ++i) { float div_temp = temp_ptr[i]; match2_xy_change_12.at<float>(0, i) = match2_xy_change_12.at<float>(0, i) / div_temp; match2_xy_change_12.at<float>(1, i) = match2_xy_change_12.at<float>(1, i) / div_temp; } Mat diff = match2_xy_change_12 - match1_xy_trans; pow(diff, 2, diff); rmse = (float)sqrt(sum(diff)(0) * 1.0 / N); //sum输出是各个通道的和,rmse为输入点的均方误差 } //如果是相似变换 else if (model == string("similarity")) { /*[x, y, 1, 0; y, -x, 0, 1] * [a, b, c, d]'=[u,v]*/ Mat A3; A3.create(2 * N, 4, CV_32FC1); for (int i = 0; i < N; ++i) { A3.at<float>(2 * i, 0) = match2_xy.at<float>(i, 0); A3.at<float>(2 * i, 1) = match2_xy.at<float>(i, 1); A3.at<float>(2 * i, 2) = 1.f; A3.at<float>(2 * i, 3) = 0.f; A3.at<float>(2 * i + 1, 0) = match2_xy.at<float>(i, 1); A3.at<float>(2 * i + 1, 1) = match2_xy.at<float>(i, 0) * (-1.f); A3.at<float>(2 * i + 1, 2) = 0.f; A3.at<float>(2 * i + 1, 3) = 1.f; } Vec4f values; solve(A3, B, values, DECOMP_QR); change = (Mat_<float>(3, 3) << values(0), values(1), values(2), values(1) * (-1.0f), values(0), values(3), +0.f, +0.f, 1.f); Mat temp_1 = change(Range(0, 2), Range(0, 2));//尺度和旋转量 Mat temp_2 = change(Range(0, 2), Range(2, 3));//平移量 Mat match2_xy_change = temp_1 * match2_xy_trans + repeat(temp_2, 1, N); Mat diff = match2_xy_change - match1_xy_trans;//求差 pow(diff, 2, diff); rmse = (float)sqrt(sum(diff)(0) * 1.0) / N;//sum输出是各个通道的和 } return change; } /*********************该函数删除错误的匹配点对****************************/ /*points_1表示参考图像上匹配的特征点位置 points_2表示待配准图像上匹配的特征点位置 model表示变换模型,“similarity”,"affine",“perspective” threshold表示内点阈值 inliers表示points_1和points_2中对应的点对是否是正确匹配,如果是,对应元素值为1,否则为0 rmse表示最后所有正确匹配点对计算出来的误差 返回一个3 x 3矩阵,表示待配准图像到参考图像的变换矩阵 */ Mat myMatch::ransac(const vector<Point2f>& points_1, const vector<Point2f>& points_2, string model, float threshold, vector<bool>& inliers, float& rmse) { if (points_1.size() != points_2.size()) CV_Error(CV_StsBadArg, "ransac模块输入特征点数量不一致!"); if (!(model == string("affine") || model == string("similarity") || model == string("perspective") || model == string("projective"))) CV_Error(CV_StsBadArg, "ransac模块图像变换类型输入错误!"); const size_t N = points_1.size(); //特征点对数,size_t 类型常用来保存一个数据的大小,通常为整形,可移植性强 int n; //相当于不同模型对应的标签 size_t max_iteration, iterations; //确定最大迭代次数,就目前来看此法过于简单粗暴,可以使用自适应迭代次数法 if (model == string("similarity")) { n = 2; max_iteration = N * (N - 1) / 2; } else if (model == string("affine")) { n = 3; max_iteration = N * (N - 1) * (N - 2) / (2 * 3); } else if (model == string("perspective")) { n = 4; max_iteration = N * (N - 1) * (N - 2) / (2 * 3); } if (max_iteration > 800) iterations = 800; else iterations = max_iteration; //取出保存在points_1和points_2中的点坐标,保存在Mat矩阵中,方便处理 Mat arr_1, arr_2; //arr_1,和arr_2是一个[3 x N]的矩阵,每一列表示一个点坐标,第三行全是1 arr_1.create(3, N, CV_32FC1); arr_2.create(3, N, CV_32FC1); //获取矩阵每一行的首地址 float* p10 = arr_1.ptr<float>(0), * p11 = arr_1.ptr<float>(1), * p12 = arr_1.ptr<float>(2); float* p20 = arr_2.ptr<float>(0), * p21 = arr_2.ptr<float>(1), * p22 = arr_2.ptr<float>(2); //把特征点放到矩阵中 for (size_t i = 0; i < N; ++i) { p10[i] = points_1[i].x; p11[i] = points_1[i].y; p12[i] = 1.f; p20[i] = points_2[i].x; p21[i] = points_2[i].y; p22[i] = 1.f; } Mat rand_mat; //特征点索引 rand_mat.create(1, n, CV_32SC1); int* p = rand_mat.ptr<int>(0); Mat sub_arr1, sub_arr2; //存放随机挑选的特征点 sub_arr1.create(n, 2, CV_32FC1); sub_arr2.create(n, 2, CV_32FC1); Mat T; //待配准图像到参考图像的变换矩阵 int most_consensus_num = 0; //当前最优一致集个数初始化为0 vector<bool> right; right.resize(N); inliers.resize(N); for (size_t i = 0; i < iterations; ++i) //迭代次数 { //随机选择n个不同的点对,不同的模型每次随机选择的个数不同 while (1) { randu(rand_mat, 0, N - 1); //随机生成n个范围在[0,N-1]之间的数,作为获取特征点的索引 //保证这n个点坐标不相同 if (n == 2 && p[0] != p[1] && (p10[p[0]] != p10[p[1]] || p11[p[0]] != p11[p[1]]) && (p20[p[0]] != p20[p[1]] || p21[p[0]] != p21[p[1]])) break; if (n == 3 && p[0] != p[1] && p[0] != p[2] && p[1] != p[2] && (p10[p[0]] != p10[p[1]] || p11[p[0]] != p11[p[1]]) && (p10[p[0]] != p10[p[2]] || p11[p[0]] != p11[p[2]]) && (p10[p[1]] != p10[p[2]] || p11[p[1]] != p11[p[2]]) && (p20[p[0]] != p20[p[1]] || p21[p[0]] != p21[p[1]]) && (p20[p[0]] != p20[p[2]] || p21[p[0]] != p21[p[2]]) && (p20[p[1]] != p20[p[2]] || p21[p[1]] != p21[p[2]])) break; if (n == 4 && p[0] != p[1] && p[0] != p[2] && p[0] != p[3] && p[1] != p[2] && p[1] != p[3] && p[2] != p[3] && (p10[p[0]] != p10[p[1]] || p11[p[0]] != p11[p[1]]) && (p10[p[0]] != p10[p[2]] || p11[p[0]] != p11[p[2]]) && (p10[p[0]] != p10[p[3]] || p11[p[0]] != p11[p[3]]) && (p10[p[1]] != p10[p[2]] || p11[p[1]] != p11[p[2]]) && (p10[p[1]] != p10[p[3]] || p11[p[1]] != p11[p[3]]) && (p10[p[2]] != p10[p[3]] || p11[p[2]] != p11[p[3]]) && (p20[p[0]] != p20[p[1]] || p21[p[0]] != p21[p[1]]) && (p20[p[0]] != p20[p[2]] || p21[p[0]] != p21[p[2]]) && (p20[p[0]] != p20[p[3]] || p21[p[0]] != p21[p[3]]) && (p20[p[1]] != p20[p[2]] || p21[p[1]] != p21[p[2]]) && (p20[p[1]] != p20[p[3]] || p21[p[1]] != p21[p[3]]) && (p20[p[2]] != p20[p[3]] || p21[p[2]] != p21[p[3]])) break; } //提取出n个点对 for (int i = 0; i < n; ++i) { sub_arr1.at<float>(i, 0) = p10[p[i]]; sub_arr1.at<float>(i, 1) = p11[p[i]]; sub_arr2.at<float>(i, 0) = p20[p[i]]; sub_arr2.at<float>(i, 1) = p21[p[i]]; } //根据这n个点对,计算初始变换矩阵 T T = LMS(sub_arr1, sub_arr2, model, rmse); int consensus_num = 0; //当前一致集(内点)个数 if (model == string("perspective")) { //变换矩阵计算待配准图像特征点在参考图像中的映射点 Mat match2_xy_change = T * arr_2; //arr_2 中存放的是待配准图像的特征点 (3,N), //match2_xy_change(Range(0, 2), Range::all())意思是提取 match2_xy_change 的 0、1 行,所有的列 Mat match2_xy_change_12 = match2_xy_change(Range(0, 2), Range::all()); //获取 match2_xy_change 第二行首地址 float* temp_ptr = match2_xy_change.ptr<float>(2); for (size_t i = 0; i < N; ++i) { float div_temp = temp_ptr[i]; //match2_xy_change 第二行第 i 列值,除以 div_temp ,是为了保证第三行为 1,和原始坐标相对应 match2_xy_change_12.at<float>(0, i) = match2_xy_change_12.at<float>(0, i) / div_temp; match2_xy_change_12.at<float>(1, i) = match2_xy_change_12.at<float>(1, i) / div_temp; } //计算待配准图像特征点在参考图像中的映射点与参考图像中对应点的距离 Mat diff = match2_xy_change_12 - arr_1(Range(0, 2), Range::all()); pow(diff, 2, diff); //第一行和第二行求和,即两点间距离的平方 Mat add = diff(Range(0, 1), Range::all()) + diff(Range(1, 2), Range::all()); float* p_add = add.ptr<float>(0); //遍历所有距离,如果小于阈值,则认为是局内点 for (size_t i = 0; i < N; ++i) { if (p_add[i] < threshold) //初始 p_add[i] { right[i] = true; ++consensus_num; } else right[i] = false; } } else if (model == string("affine") || model == string("similarity")) { Mat match2_xy_change = T * arr_2; //计算在参考图像中的映射坐标 Mat diff = match2_xy_change - arr_1; pow(diff, 2, diff); //第一行和第二行求和,计算特征点间的距离的平方 Mat add = diff(Range(0, 1), Range::all()) + diff(Range(1, 2), Range::all()); float* p_add = add.ptr<float>(0); for (size_t i = 0; i < N; ++i) { //如果小于阈值 if (p_add[i] < threshold) { right[i] = true; ++consensus_num; } else right[i] = false; } } //判断当前一致集是否是优于之前最优一致集,并更新当前最优一致集个数 if (consensus_num > most_consensus_num) { most_consensus_num = consensus_num; //把正确匹配的点赋予标签 1 for (size_t i = 0; i < N; ++i) inliers[i] = right[i]; } } //删除重复点对 for (size_t i = 0; i < N - 1; ++i) { for (size_t j = i + 1; j < N; ++j) { if (inliers[i] && inliers[j]) { if (p10[i] == p10[j] && p11[i] == p11[j] && p20[i] == p20[j] && p21[i] == p21[j]) { inliers[j] = false; --most_consensus_num; } } } } //迭代结束,获得最优一致集合,根据这些最优一致集合计算出最终的变换关系 T Mat consensus_arr1, consensus_arr2; //经过迭代后最终确认正确匹配的点 consensus_arr1.create(most_consensus_num, 2, CV_32FC1); consensus_arr2.create(most_consensus_num, 2, CV_32FC1); int k = 0; for (size_t i = 0; i < N; ++i) { if (inliers[i]) { consensus_arr1.at<float>(k, 0) = p10[i]; consensus_arr1.at<float>(k, 1) = p11[i]; consensus_arr2.at<float>(k, 0) = p20[i]; consensus_arr2.at<float>(k, 1) = p21[i]; ++k; } } int num_ransac = (model == string("similarity") ? 2 : (model == string("affine") ? 3 : 4)); if (k < num_ransac) CV_Error(CV_StsBadArg, "ransac模块删除错误点对后剩下正确点对个数不足以计算出变换关系矩阵!"); //利用迭代后正确匹配点计算变换矩阵,为什么不是挑选 n 个点计算变换矩阵 T = LMS(consensus_arr1, consensus_arr2, model, rmse); return T; } /********************该函数生成两幅图的棋盘网格图*************************/ /*image_1表示参考图像 image_2表示配准后的待配准图像 chessboard_1表示image_1的棋盘图像 chessboard_2表示image_2的棋盘图像 mosaic_image表示image_1和image_2的镶嵌图像 width表示棋盘网格大小 */ void myMatch::mosaic_map(const Mat& image_1, const Mat& image_2, Mat& chessboard_1, Mat& chessboard_2, Mat& mosaic_image, int width) { if (image_1.size != image_2.size) CV_Error(CV_StsBadArg, "mosaic_map模块输入两幅图大小必须一致!"); //生成image_1的棋盘网格图 chessboard_1 = image_1.clone(); int rows_1 = chessboard_1.rows; int cols_1 = chessboard_1.cols; int row_grids_1 = cvFloor((double)rows_1 / width); //行方向网格个数 int col_grids_1 = cvFloor((double)cols_1 / width); //列方向网格个数 //指定区域像素赋值为零,便形成了棋盘图 //第一幅图,第一行 2、4、6 像素值赋值零;第一幅图与第二幅图零像素位置交叉,以便两幅图交叉显示 for (int i = 0; i < row_grids_1; i = i + 2) { for (int j = 1; j < col_grids_1; j = j + 2) { Range range_x(j * width, (j + 1) * width); Range range_y(i * width, (i + 1) * width); chessboard_1(range_y, range_x) = 0; } } for (int i = 1; i < row_grids_1; i = i + 2) { for (int j = 0; j < col_grids_1; j = j + 2) { Range range_x(j * width, (j + 1) * width); Range range_y(i * width, (i + 1) * width); chessboard_1(range_y, range_x) = 0; } } //生成image_2的棋盘网格图 chessboard_2 = image_2.clone(); int rows_2 = chessboard_2.rows; int cols_2 = chessboard_2.cols; int row_grids_2 = cvFloor((double)rows_2 / width);//行方向网格个数 int col_grids_2 = cvFloor((double)cols_2 / width);//列方向网格个数 //第二幅图,第一行 1、3、5 像素值赋值零 for (int i = 0; i < row_grids_2; i = i + 2) { for (int j = 0; j < col_grids_2; j = j + 2) { Range range_x(j * width, (j + 1) * width); Range range_y(i * width, (i + 1) * width); chessboard_2(range_y, range_x) = 0; } } for (int i = 1; i < row_grids_2; i = i + 2) { for (int j = 1; j < col_grids_2; j = j + 2) { Range range_x(j * width, (j + 1) * width); Range range_y(i * width, (i + 1) * width); chessboard_2(range_y, range_x) = 0; } } //两个棋盘图进行叠加,显示配准效果 mosaic_image = chessboard_1 + chessboard_2; } /*该函数对输入图像指定位置像素进行中值滤波,消除边缘拼接阴影*/ /*image表示输入的图像 position表示需要进行中值滤波的位置 */ inline void median_filter(Mat& image, const vector<vector<int>>& pos) { int channels = image.channels(); switch (channels) { case 1://单通道 for (auto beg = pos.cbegin(); beg != pos.cend(); ++beg) { int i = (*beg)[0];//y int j = (*beg)[1];//x uchar& pix_val = image.at<uchar>(i, j); vector<uchar> pixs; for (int row = -1; row <= 1; ++row) { for (int col = -1; col <= 1; ++col) { if (i + row >= 0 && i + row < image.rows && j + col >= 0 && j + col < image.cols) { pixs.push_back(image.at<uchar>(i + row, j + col)); } } } //排序 std::sort(pixs.begin(), pixs.end()); pix_val = pixs[pixs.size() / 2]; } break; case 3://3通道 for (auto beg = pos.cbegin(); beg != pos.cend(); ++beg) { int i = (*beg)[0];//y int j = (*beg)[1];//x Vec3b& pix_val = image.at<Vec3b>(i, j); vector<cv::Vec3b> pixs; for (int row = -1; row <= 1; ++row) { for (int col = -1; col <= 1; ++col) { if (i + row >= 0 && i + row < image.rows && j + col >= 0 && j + col < image.cols) { pixs.push_back(image.at<Vec3b>(i + row, j + col)); } } } //排序 std::sort(pixs.begin(), pixs.end(), [pix_val](const Vec3b& a, const Vec3b& b)->bool { return sum((a).ddot(a))[0] < sum((b).ddot(b))[0]; }); pix_val = pixs[pixs.size() / 2]; } break; default:break; } } /***************该函数把配准后的图像进行融合*****************/ /*该函数功能主要是来对图像进行融合,以显示配准的效果 *image_1表示参考图像 *image_2表示待配准图像 *T表示待配准图像到参考图像的转换矩阵 *fusion_image表示参考图像和待配准图像融合后的图像 *mosaic_image表示参考图像和待配准图像融合镶嵌后的图像,镶嵌图形是为了观察匹配效果 *matched_image表示把待配准图像进行配准后的结果 */ void myMatch::image_fusion(const Mat& image_1, const Mat& image_2, const Mat T, Mat& fusion_image, Mat& matched_image) { //有关depth()的理解,详解:https://blog.csdn.net/datouniao1/article/details/113524784 if (!(image_1.depth() == CV_8U && image_2.depth() == CV_8U)) CV_Error(CV_StsBadArg, "image_fusion模块仅支持uchar类型图像!"); if (image_1.channels() == 4 || image_2.channels() == 4) CV_Error(CV_StsBadArg, "image_fusion模块仅仅支持单通道或者3通道图像"); int rows_1 = image_1.rows, cols_1 = image_1.cols; int rows_2 = image_2.rows, cols_2 = image_2.cols; int channel_1 = image_1.channels(); int channel_2 = image_2.channels(); //可以对:彩色-彩色、彩色-灰色、灰色-彩色、灰色-灰色的配准 Mat image_1_temp, image_2_temp; if (channel_1 == 3 && channel_2 == 3) { image_1_temp = image_1; image_2_temp = image_2; } else if (channel_1 == 1 && channel_2 == 3) { image_1_temp = image_1; cvtColor(image_2, image_2_temp, CV_RGB2GRAY); //颜色空间转换,把彩色图转化为灰度图 } else if (channel_1 == 3 && channel_2 == 1) { cvtColor(image_1, image_1_temp, CV_RGB2GRAY); image_2_temp = image_2; } else if (channel_1 == 1 && channel_2 == 1) { image_1_temp = image_1; image_2_temp = image_2; } //创建一个(3,3)float 矩阵 Mat_ 是一个模板类 Mat T_temp = (Mat_<float>(3, 3) << 1, 0, cols_1, 0, 1, rows_1, 0, 0, 1); Mat T_1 = T_temp * T; //对参考图像和待配准图像进行变换 Mat trans_1, trans_2;//same type as image_2_temp trans_1 = Mat::zeros(3 * rows_1, 3 * cols_1, image_1_temp.type()); //创建扩大后的矩阵 image_1_temp.copyTo(trans_1(Range(rows_1, 2 * rows_1), Range(cols_1, 2 * cols_1))); //把image_1_temp中的数据复制到扩大后矩阵的对应位置 warpPerspective(image_2_temp, trans_2, T_1, Size(3 * cols_1, 3 * rows_1), INTER_LINEAR, 0, Scalar::all(0)); /*功能:把image_2_temp投射到一个新的视平面,即变形 *image_2_temp为输入矩阵 *trans_2为输出矩阵,尺寸和输入矩阵大小一致 *T_1为变换矩阵,(3, 3)矩阵用于透视变换, (2, 2)用于线性变换, (1, 1)用于平移 *warpPerspective函数功能是进行透视变换,T_1是(3,3)的透视变换矩阵 *int flags=INTER_LINEAR为输出图像的插值方法 *int borderMode=BORDER_CONSTANT,0 为图像边界的填充方式 *const Scalar& borderValue=Scalar():边界的颜色设置,一般默认是0,Scalar::all(0)对影像边界外进行填充*/ //使用简单的均值法进行图像融合 Mat trans = trans_2.clone(); //把经过透视变换的image_2复制给trans int nRows = rows_1; int nCols = cols_1; int len = nCols; bool flag_1 = false; bool flag_2 = false; vector<vector<int>> positions; //保存边缘位置坐标 switch (image_1_temp.channels()) { case 1: //如果图像1是单通道的 for (int i = 0; i < nRows; ++i) { uchar* ptr_1 = trans_1.ptr<uchar>(i + rows_1); //访问trans_1中的指定行像素值 uchar* ptr = trans.ptr<uchar>(i + rows_1); //访问trans 中的指定行像素值 for (int j = 0; j < nCols; ++j) { if (ptr[j + len] == 0 && ptr_1[j + len] != 0) //非重合区域 { flag_1 = true; if (flag_2) //表明从重合区域过度到了非重合区域 { for (int p = -1; p <= 1; ++p) //保存边界3x3区域像素 { for (int q = -1; q <= 1; ++q) { vector<int> pos; pos.push_back(i + rows_1 + p); pos.push_back(j + cols_1 + q); positions.push_back(pos);//保存边缘位置坐标 } } flag_2 = false; } ptr[j + len] = ptr_1[j + len]; } else//对于重合区域 { flag_2 = true; if (flag_1)//表明从非重合区域过度到了重合区域 { for (int p = -1; p <= 1; ++p)//保存边界3x3区域像素 { for (int q = -1; q <= 1; ++q) { vector<int> pos; pos.push_back(i + rows_1 + p); pos.push_back(j + cols_1 + q); positions.push_back(pos);//保存边缘位置坐标 } } flag_1 = false; } ptr[j + len] = saturate_cast<uchar>(((float)ptr[j + len] + (float)ptr_1[j + len]) / 2); } } } break; case 3: //如果图像是三通道的 len = len * image_1_temp.channels(); //3倍的列数 for (int i = 0; i < nRows; ++i) { uchar* ptr_1 = trans_1.ptr<uchar>(i + rows_1); //访问trans_1中的指定行像素值 uchar* ptr = trans.ptr<uchar>(i + rows_1); //访问trans 中的指定行像素值 for (int j = 0; j < nCols; ++j) { int nj = j * image_1_temp.channels(); //若两张影像对应列像素值不同(3通道),则是非重合区,该过程仅仅是为了使配准后的影像进行融合,而非配准 if (ptr[nj + len] == 0 && ptr[nj + len + 1] == 0 && ptr[nj + len + 2] == 0 && ptr_1[nj + len] != 0 && ptr_1[nj + len + 1] != 0 && ptr_1[nj + len + 2] != 0) { flag_1 = true; if (flag_2) //表明从重合区域过度到了非重合区域 { for (int p = -1; p <= 1; ++p) //保存边界3x3区域像素 { for (int q = -1; q <= 1; ++q) { vector<int> pos; pos.push_back(i + rows_1 + p); pos.push_back(j + cols_1 + q); positions.push_back(pos); //保存边缘位置坐标 } } flag_2 = false; } ptr[nj + len] = ptr_1[nj + len]; ptr[nj + len + 1] = ptr_1[nj + len + 1]; ptr[nj + len + 2] = ptr_1[nj + len + 2]; } else { //对于重合区域 flag_2 = true; if (flag_1) //表明从非重合区域过度到了重合区域 { for (int p = -1; p <= 1; ++p) //保存边界3x3区域像素 { for (int q = -1; q <= 1; ++q) { vector<int> pos; pos.push_back(i + rows_1 + p); pos.push_back(j + cols_1 + q); positions.push_back(pos); //保存边缘位置坐标 } } flag_1 = false; } ptr[nj + len] = saturate_cast<uchar>(((float)ptr[nj + len] + (float)ptr_1[nj + len]) / 2); ptr[nj + len + 1] = saturate_cast<uchar>(((float)ptr[nj + len + 1] + (float)ptr_1[nj + len + 1]) / 2); ptr[nj + len + 2] = saturate_cast<uchar>(((float)ptr[nj + len + 2] + (float)ptr_1[nj + len + 2]) / 2); } } } break; default:break; } //根据获取的边缘区域的坐标,对边缘像素进行中值滤波,消除边缘效应 median_filter(trans, positions); //删除多余的区域 Mat left_up = T_1 * (Mat_<float>(3, 1) << 0, 0, 1); //左上角 Mat left_down = T_1 * (Mat_<float>(3, 1) << 0, rows_2 - 1, 1); //左下角 Mat right_up = T_1 * (Mat_<float>(3, 1) << cols_2 - 1, 0, 1); //右上角 Mat right_down = T_1 * (Mat_<float>(3, 1) << cols_2 - 1, rows_2 - 1, 1); //右下角 //对于透视变换,需要除以一个因子 left_up = left_up / left_up.at<float>(2, 0); left_down = left_down / left_down.at<float>(2, 0); right_up = right_up / right_up.at<float>(2, 0); right_down = right_down / right_down.at<float>(2, 0); //计算x,y坐标的范围 float temp_1 = min(left_up.at<float>(0, 0), left_down.at<float>(0, 0)); float temp_2 = min(right_up.at<float>(0, 0), right_down.at<float>(0, 0)); float min_x = min(temp_1, temp_2); temp_1 = max(left_up.at<float>(0, 0), left_down.at<float>(0, 0)); temp_2 = max(right_up.at<float>(0, 0), right_down.at<float>(0, 0)); float max_x = max(temp_1, temp_2); temp_1 = min(left_up.at<float>(1, 0), left_down.at<float>(1, 0)); temp_2 = min(right_up.at<float>(1, 0), right_down.at<float>(1, 0)); float min_y = min(temp_1, temp_2); temp_1 = max(left_up.at<float>(1, 0), left_down.at<float>(1, 0)); temp_2 = max(right_up.at<float>(1, 0), right_down.at<float>(1, 0)); float max_y = max(temp_1, temp_2); int X_min = max(cvFloor(min_x), 0); int X_max = min(cvCeil(max_x), 3 * cols_1 - 1); int Y_min = max(cvFloor(min_y), 0); int Y_max = min(cvCeil(max_y), 3 * rows_1 - 1); if (X_min > cols_1) X_min = cols_1; if (X_max < 2 * cols_1 - 1) X_max = 2 * cols_1 - 1; if (Y_min > rows_1) Y_min = rows_1; if (Y_max < 2 * rows_1 - 1) Y_max = 2 * rows_1 - 1; //提取有价值区域 Range Y_range(Y_min, Y_max + 1); Range X_range(X_min, X_max + 1); fusion_image = trans(Y_range, X_range); matched_image = trans_2(Y_range, X_range); Mat ref_matched = trans_1(Y_range, X_range); //生成棋盘网格图像 /*Mat chessboard_1, chessboard_2; mosaic_map(trans_1(Y_range, X_range), trans_2(Y_range, X_range), chessboard_1, chessboard_2, mosaic_image, 100);*/ /*cv::imwrite(".\\image_save\\参考图像棋盘图像.jpg", chessboard_1); cv::imwrite(".\\image_save\\待配准图像棋盘图像.jpg", chessboard_2);*/ cv::imwrite(".\\image_save\\配准后的参考图像.jpg", ref_matched); cv::imwrite(".\\image_save\\配准后的待配准图像.jpg", matched_image); } /******该函数计算参考图像一个描述子和待配准图像所有描述子的欧式距离,并获得最近邻和次近邻距离,以及对应的索引*/ /*sub_des_1表示参考图像的一个描述子 *des_2表示待配准图像描述子 *num_des_2值待配准图像描述子个数 *dims_des指的是描述子维度 *dis保存最近邻和次近邻距离 *idx保存最近邻和次近邻索引 */ inline void min_dis_idx(const float* ptr_1, const Mat& des_2, int num_des2, int dims_des, float dis[2], int idx[2]) { float min_dis1 = 1000, min_dis2 = 2000; int min_idx1, min_idx2; for (int j = 0; j < num_des2; ++j) { const float* ptr_des_2 = des_2.ptr<float>(j); float cur_dis = 0; for (int k = 0; k < dims_des; ++k) { float diff = ptr_1[k] - ptr_des_2[k]; cur_dis += diff * diff; } if (cur_dis < min_dis1) { min_dis1 = cur_dis; min_idx1 = j; } else if (cur_dis >= min_dis1 && cur_dis < min_dis2) { min_dis2 = cur_dis; min_idx2 = j; } } dis[0] = sqrt(min_dis1); dis[1] = sqrt(min_dis2); idx[0] = min_idx1; idx[1] = min_idx2; } /*加速版本的描述子匹配,返回匹配点候选集函数,该加速版本比前面版本速度提升了3倍*/ void myMatch::match_des(const Mat& des_1, const Mat& des_2, vector<vector<DMatch>>& dmatchs, DIS_CRIT dis_crite) { int num_des_1 = des_1.rows; int num_des_2 = des_2.rows; int dims_des = des_1.cols; vector<DMatch> match(2); //对于参考图像上的每一点,和待配准图像进行匹配 if (dis_crite == 0) //欧几里得距离 { for (int i = 0; i < num_des_1; ++i) //对于参考图像中的每个描述子 { const float* ptr_des_1 = des_1.ptr<float>(i); float dis[2]; int idx[2]; min_dis_idx(ptr_des_1, des_2, num_des_2, dims_des, dis, idx); match[0].queryIdx = i; match[0].trainIdx = idx[0]; match[0].distance = dis[0]; match[1].queryIdx = i; match[1].trainIdx = idx[1]; match[1].distance = dis[1]; dmatchs.push_back(match); } } else if (dis_crite == 1)//cos距离 { Mat trans_des2; transpose(des_2, trans_des2); double aa = (double)getTickCount(); Mat mul_des = des_1 * trans_des2; //gemm(des_1, des_2, 1, Mat(), 0, mul_des, GEMM_2_T); double time1 = ((double)getTickCount() - aa) / getTickFrequency(); cout << "cos距离中矩阵乘法花费时间: " << time1 << "s" << endl; for (int i = 0; i < num_des_1; ++i) { float max_cos1 = -1000, max_cos2 = -2000; int max_idx1, max_idx2; float* ptr_1 = mul_des.ptr<float>(i); for (int j = 0; j < num_des_2; ++j) { float cur_cos = ptr_1[j]; if (cur_cos > max_cos1) { max_cos1 = cur_cos; max_idx1 = j; } else if (cur_cos <= max_cos1 && cur_cos > max_cos2) { max_cos2 = cur_cos; max_idx2 = j; } } match[0].queryIdx = i; match[0].trainIdx = max_idx1; match[0].distance = acosf(max_cos1); match[1].queryIdx = i; match[1].trainIdx = max_idx2; match[1].distance = acosf(max_cos2); dmatchs.push_back(match); } } } /*******************建立尺度直方图、ROM 直方图************************/ void myMatch::scale_ROM_Histogram(const vector<DMatch>& matches, float* scale_hist, float* ROM_hist, int n) { int len = matches.size(); //使用AutoBuffer分配一段内存,这里多出4个空间的目的是为了方便后面平滑直方图的需要 AutoBuffer<float> buffer((4 * len) + n + 4); //X保存水平差分,Y保存数值差分,Mag保存梯度幅度,Ori保存梯度角度,W保存高斯权重 float* X = buffer, * Y = buffer + len, * Mag = Y, * Ori = Y + len, * W = Ori + len; float* temp_hist = W + len + 2; //临时保存直方图数据 for (int i = 0; i < n; ++i) temp_hist[i] = 0.f; //数据清零 } /*******************该函数删除错误匹配点对,并完成匹配************************/ /*image_1表示参考图像, image_2表示待配准图像 dmatchs表示最近邻和次近邻匹配点对 keys_1表示参考图像特征点集合 keys_2表示待配准图像特征点集合 model表示变换模型 right_matchs表示参考图像和待配准图像正确匹配点对 matched_line表示在参考图像和待配准图像上绘制连接线 该函数返回变换模型参数 */ Mat myMatch::match(const Mat& image_1, const Mat& image_2, const vector<vector<DMatch>>& dmatchs, vector<KeyPoint> keys_1, vector<KeyPoint> keys_2, string model, vector<DMatch>& right_matchs, Mat& matched_line, vector<DMatch>& init_matchs) { //获取初始匹配的关键点的位置 vector<Point2f> point_1, point_2; for (size_t i = 0; i < dmatchs.size(); ++i) //增加一个一个0.8,正确匹配点数增加2 { double dis_1 = dmatchs[i][0].distance; //distance对应的是特征点描述符的欧式距离 double dis_2 = dmatchs[i][1].distance; //比率测试筛选误匹配点(初步筛选),如果满足则认为是有候选集中有正确匹配点 if ((dis_1 / dis_2) < dis_ratio3) //最近邻和次近邻距离比阈值 { //queryIdx、trainIdx和distance是DMatch类中的一些属性 //pt是KeyPoint类中的成员,对应关键点的坐标 point_1.push_back(keys_1[dmatchs[i][0].queryIdx].pt); //queryIdx对应的是特征描述子的下标,也是对应特征点的下标 point_2.push_back(keys_2[dmatchs[i][0].trainIdx].pt); //trainIdx对应的是特征描述子的下标,也是对应特征点的下标 init_matchs.push_back(dmatchs[i][0]); //保存正确的dmatchs } } cout << "距离比之后初始匹配点对个数是: " << init_matchs.size() << endl; int min_pairs = (model == string("similarity") ? 2 : (model == string("affine") ? 3 : 4)); if (point_1.size() < min_pairs) CV_Error(CV_StsBadArg, "match模块距离比阶段匹配特征点个数不足!"); //使用ransac算法再次对匹配点进行筛选,然后使用最后确定的匹配点计算变换矩阵的参数 vector<bool> inliers; //存放的是 bool 类型的数据,对应特征点 float rmse; //homography是一个(3,3)矩阵,是待配准影像到参考影像的变换矩阵,初始误差阈值是 1.5 Mat homography = ransac(point_1, point_2, model, 1.5, inliers, rmse); //提取出处正确匹配点对 int i = 0; vector<Point2f> point_11, point_22; vector<DMatch>::iterator itt = init_matchs.begin(); for (vector<bool>::iterator it = inliers.begin(); it != inliers.end(); ++it, ++itt) { if (*it) //如果是正确匹配点对 { right_matchs.push_back(*itt); // init_matchs 中匹配点对的存储顺序和 point_1 中特征点的存储顺序是一一对应的 point_11.push_back(point_1.at(i)); point_22.push_back(point_2.at(i)); } ++i; } cout << "使用RANSAC删除错误点对,且返回正确匹配个数: " << right_matchs.size() << endl; cout << "误差rmse: " << rmse << endl; //绘制初始匹配点对连线图,此时初始匹配指的是经过 KnnMatch 筛选后的匹配 Mat initial_matched; //输出矩阵,类似画布 drawMatches(image_1, keys_1, image_2, keys_2, init_matchs, initial_matched, Scalar(255, 0, 255), Scalar(0, 255, 0), vector<char>()); //该函数用于绘制特征点并对匹配的特征点进行连线 imwrite(".\\image_save\\初始匹配点对.jpg", initial_matched); //保存图片,第一个颜色控制连线,第二个颜色控制特征点 //绘制正确匹配点对连线图 drawMatches(image_1, keys_1, image_2, keys_2, right_matchs, matched_line, Scalar(255, 0, 255), Scalar(0, 255, 0), vector<char>()); imwrite(".\\image_save\\正确匹配点对.jpg", matched_line); //保存和显示检测到的特征点 Mat keys_image_1, keys_image_2; //输出矩阵,类似画布 drawKeypoints(image_1, keys_1, keys_image_1, Scalar(0, 255, 0)); //该函数用于绘制图像中的特征点 drawKeypoints(image_2, keys_2, keys_image_2, Scalar(0, 255, 0)); imwrite(".\\image_save\\参考图像检测到的特征点.jpg", keys_image_1); imwrite(".\\image_save\\待配准图像检测到的.jpg", keys_image_2); return homography; } myMatch::~myMatch() { }
5、 myDisplay.h
#pragma #include<iostream> #include<opencv2\core\core.hpp> #include<opencv2\features2d\features2d.hpp> #include<vector> using namespace cv; using namespace std; class myDisplay { public: myDisplay() {} void mosaic_pyramid(const vector<vector<Mat>>& pyramid, Mat& pyramid_image, int nOctaceLayers, string str); void write_mosaic_pyramid(const vector<vector<Mat>>& gauss_pyr_1, const vector<vector<Mat>>& dog_pyr_1, const vector<vector<Mat>>& gauss_pyr_2, const vector<vector<Mat>>& dog_pyr_2, int nOctaveLayers); //没用到,后文没有对其进行定义 void write_keys_image(vector<KeyPoint>& keypoints_1, vector<KeyPoint>& keypoints_2, const Mat& image_1, const Mat& image_2, Mat& image_1_keys, Mat& image_2_keys, bool double_size); };
6、 myDisplay.cpp
#include "myDisplay.h" #include<opencv2\highgui\highgui.hpp> #include<vector> #include<sstream> /***************************该函数把金字塔放在一副图像上(包络高斯金字塔和DOG金字塔)******************/ /*pyramid表示高斯金字塔或者DOG金字塔 pyramid_image表示生成的金字塔图像 nOctaveLayers表示每组中间层数,默认是3 str表示是高斯金字塔还是DOG金字塔 */ void myDisplay::mosaic_pyramid(const vector<vector<Mat>>& pyramid, Mat& pyramid_image, int nOctaceLayers, string str) { //获得每组金字塔图像大小 vector<vector<int>> temp_size; for (auto beg = pyramid.cbegin(); beg != pyramid.cend(); ++beg) { vector<int> temp_vec; int rows = (*beg)[0].rows; int cols = (*beg)[0].cols; temp_vec.push_back(rows); temp_vec.push_back(cols); temp_size.push_back(temp_vec); } //计算最后生成的金字塔图像pyramid_image的大小 int total_rows = 0, total_cols = 0; for (auto beg = temp_size.begin(); beg != temp_size.end(); ++beg) { total_rows = total_rows + (*beg)[0];//获取行大小 if (beg == temp_size.begin()) { if (str == string("高斯金字塔")) total_cols = (nOctaceLayers + 3) * ((*beg)[1]);//获取列大小 else if (str == string("DOG金字塔")) total_cols = (nOctaceLayers + 2) * ((*beg)[1]);//获取列大小 } } pyramid_image.create(total_rows, total_cols, CV_8UC1); int i = 0, accumulate_row = 0; for (auto beg = pyramid.cbegin(); beg != pyramid.cend(); ++beg, ++i) { int accumulate_col = 0; accumulate_row = accumulate_row + temp_size[i][0]; for (auto it = (*beg).cbegin(); it != (*beg).cend(); ++it) { accumulate_col = accumulate_col + temp_size[i][1]; Mat temp(pyramid_image, Rect(accumulate_col - temp_size[i][1], accumulate_row - temp_size[i][0], it->cols, it->rows)); Mat temp_it; normalize(*it, temp_it, 0, 255, NORM_MINMAX, CV_32FC1); convertScaleAbs(temp_it, temp_it, 1, 0); temp_it.copyTo(temp); } } } /**************************该函数保存拼接后的高斯金字塔和DOG金字塔图像**************************/ /*gauss_pyr_1表示参考高斯金字塔 dog_pyr_1表示参考DOG金字塔 gauss_pyr_2表示待配准高斯金字塔 dog_pyr_2表示待配准DOG金字塔 nOctaveLayers表示金字塔每组中间层数 */ void myDisplay::write_mosaic_pyramid(const vector<vector<Mat>>& gauss_pyr_1, const vector<vector<Mat>>& dog_pyr_1, const vector<vector<Mat>>& gauss_pyr_2, const vector<vector<Mat>>& dog_pyr_2, int nOctaveLayers) { //显示参考和待配准高斯金字塔图像 Mat gauss_image_1, gauss_image_2; mosaic_pyramid(gauss_pyr_1, gauss_image_1, nOctaveLayers, string("高斯金字塔")); mosaic_pyramid(gauss_pyr_2, gauss_image_2, nOctaveLayers, string("高斯金字塔")); imwrite(".\\image_save\\参考图像高斯金字塔.jpg", gauss_image_1); imwrite(".\\image_save\\待配准图像高斯金字塔.jpg", gauss_image_2); //显示参考和待配准DOG金字塔图像 Mat dog_image_1, dog_image_2; mosaic_pyramid(dog_pyr_1, dog_image_1, nOctaveLayers, string("DOG金字塔")); mosaic_pyramid(dog_pyr_2, dog_image_2, nOctaveLayers, string("DOG金字塔")); imwrite(".\\image_save\\参考图像DOG金字塔.jpg", dog_image_1); imwrite(".\\image_save\\待配准图像DOG金字塔.jpg", dog_image_2); }
7、 main.cpp
#include"mySift.h" #include"myDisplay.h" #include"myMatch.h" #include<direct.h> #include<opencv2\highgui\highgui.hpp> #include<opencv2\calib3d\calib3d.hpp> #include<opencv2\imgproc\imgproc.hpp> #include<fstream> #include<stdlib.h> #include<direct.h> int main(int argc, char* argv[]) { /********************** 1、读入数据 **********************/ Mat image_1 = imread("..\\test_data\\perspective_graf_1.ppm", -1); Mat image_2 = imread("..\\test_data\\perspective_graf_2.ppm", -1); string change_model = "perspective"; //affine为仿射变换,初始为perspective string folderPath = ".\\image_save"; _mkdir(folderPath.c_str()); double total_count_beg = (double)getTickCount(); //算法运行总时间开始计时 mySift sift_1(0, 3, 0.04, 10, 1.6, true); //类对象 /********************** 1、参考图像特征点检测和描述 **********************/ vector<vector<Mat>> gauss_pyr_1, dog_pyr_1; //高斯金字塔和高斯差分金字塔 vector<KeyPoint> keypoints_1; //特征点 vector<vector<vector<float>>> all_cell_contrasts_1; //所有尺度层中所有单元格的对比度 vector<vector<float>> average_contrast_1; //所有尺度层中多有单元格的平均对比度 vector<vector<int>> n_cells_1; //所有尺度层,每一尺度层中所有单元格所需特征数 vector<int> num_cell_1; //当前尺度层中所有单元格中的所需特征数 vector<vector<int>> available_n_1; //所有尺度层,每一尺度层中所有单元格可得到特征数 vector<int> available_num_1; //一个尺度层中所有单元格中可用特征数量 vector<KeyPoint> final_keypoints1; //第一次筛选结果 vector<KeyPoint> Final_keypoints1; //第二次筛选结果 vector<KeyPoint> Final_Keypoints1; //第三次筛选结果 Mat descriptors_1; //特征描述子 double detect_1 = (double)getTickCount(); //特征点检测运行时间开始计时 sift_1.detect(image_1, gauss_pyr_1, dog_pyr_1, keypoints_1, all_cell_contrasts_1, average_contrast_1, n_cells_1, num_cell_1, available_n_1, available_num_1, final_keypoints1, Final_keypoints1, Final_Keypoints1); //特征点检测 cout << "参考图像检测出的总特征数 =" << keypoints_1.size() << endl; //getTickFrequency() 返回值为一秒的计时周期数,二者比值为特征点检测时间 double detect_time_1 = ((double)getTickCount() - detect_1) / getTickFrequency(); cout << "参考图像特征点检测时间是: " << detect_time_1 << "s" << endl; double comput_1 = (double)getTickCount(); vector<Mat> sar_harris_fun_1; vector<Mat> amplit_1; vector<Mat> orient_1; sift_1.comput_des(gauss_pyr_1, keypoints_1, amplit_1, orient_1, descriptors_1); double comput_time_1 = ((double)getTickCount() - comput_1) / getTickFrequency(); cout << "参考图像特征点描述时间是: " << comput_time_1 << "s" << endl; /********************** 1、待配准图像特征点检测和描述 **********************/ vector<vector<Mat>> gauss_pyr_2, dog_pyr_2; vector<KeyPoint> keypoints_2; vector<vector<vector<float>>> all_cell_contrasts_2; //所有尺度层中所有单元格的对比度 vector<vector<float>> average_contrast_2; //所有尺度层中多有单元格的平均对比度 vector<vector<int>> n_cells_2; //所有尺度层,每一尺度层中所有单元格所需特征数 vector<int> num_cell_2; //当前尺度层中所有单元格中的所需特征数 vector<vector<int>> available_n_2; //所有尺度层,每一尺度层中的一个单元格可得到特征数 vector<int> available_num_2; //一个尺度层中所有单元格中可用特征数量 vector<KeyPoint> final_keypoints2; //第一次筛选结果 vector<KeyPoint> Final_keypoints2; //第二次筛选结果 vector<KeyPoint> Final_Keypoints2; //第三次筛选结果 Mat descriptors_2; double detect_2 = (double)getTickCount(); sift_1.detect(image_2, gauss_pyr_2, dog_pyr_2, keypoints_2, all_cell_contrasts_2, average_contrast_2, n_cells_2, num_cell_2, available_n_2, available_num_2, final_keypoints2, Final_keypoints2, Final_Keypoints2); cout << "待配准图像检测出的总特征数 =" << keypoints_2.size() << endl; double detect_time_2 = ((double)getTickCount() - detect_2) / getTickFrequency(); cout << "待配准图像特征点检测时间是: " << detect_time_2 << "s" << endl; double comput_2 = (double)getTickCount(); vector<Mat> sar_harris_fun_2; vector<Mat> amplit_2; vector<Mat> orient_2; sift_1.comput_des(gauss_pyr_2, keypoints_2, amplit_2, orient_2, descriptors_2); double comput_time_2 = ((double)getTickCount() - comput_2) / getTickFrequency(); cout << "待配准特征点描述时间是: " << comput_time_2 << "s" << endl; /********************** 1、最近邻与次近邻距离比匹配,两幅影像进行配准 **********************/ myMatch mymatch; double match_time = (double)getTickCount(); //影像配准计时开始 //knnMatch函数是DescriptorMatcher类的成员函数,FlannBasedMatcher是DescriptorMatcher的子类 Ptr<DescriptorMatcher> matcher1 = new FlannBasedMatcher; Ptr<DescriptorMatcher> matcher2 = new FlannBasedMatcher; vector<vector<DMatch>> dmatchs; //vector<DMatch>中存放的是一个描述子可能匹配的候选集 vector<vector<DMatch>> dmatch1; vector<vector<DMatch>> dmatch2; matcher1->knnMatch(descriptors_1, descriptors_2, dmatchs, 2); cout << "距离比之前初始匹配点对个数是:" << dmatchs.size() << endl; Mat matched_lines; //同名点连线 vector<DMatch> init_matchs, right_matchs; //用于存放正确匹配的点 //该函数返回的是空间映射模型参数 Mat homography = mymatch.match(image_1, image_2, dmatchs, keypoints_1, keypoints_2, change_model, right_matchs, matched_lines, init_matchs); double match_time_2 = ((double)getTickCount() - match_time) / getTickFrequency(); cout << "特征点匹配花费时间是: " << match_time_2 << "s" << endl; cout << change_model << "变换矩阵是:" << endl; cout << homography << endl; /********************** 1、把正确匹配点坐标写入文件中 **********************/ ofstream ofile; ofile.open(".\\position.txt"); //创建文件 for (size_t i = 0; i < right_matchs.size(); ++i) { ofile << keypoints_1[right_matchs[i].queryIdx].pt << " " << keypoints_2[right_matchs[i].trainIdx].pt << endl; } /********************** 1、图像融合 **********************/ double fusion_beg = (double)getTickCount(); Mat fusion_image, mosaic_image, regist_image; mymatch.image_fusion(image_1, image_2, homography, fusion_image, regist_image); imwrite(".\\image_save\\融合后的图像.jpg", fusion_image); double fusion_time = ((double)getTickCount() - fusion_beg) / getTickFrequency(); cout << "图像融合花费时间是: " << fusion_time << "s" << endl; double total_time = ((double)getTickCount() - total_count_beg) / getTickFrequency(); cout << "总花费时间是: " << total_time << "s" << endl; /********************** 1、生成棋盘图,显示配准结果 **********************/ //生成棋盘网格图像 Mat chessboard_1, chessboard_2, mosaic_images; Mat image1 = imread(".\\image_save\\配准后的参考图像.jpg", -1); Mat image2 = imread(".\\image_save\\配准后的待配准图像.jpg", -1); mymatch.mosaic_map(image1, image2, chessboard_1, chessboard_2, mosaic_images, 50); imwrite(".\\image_save\\参考图像棋盘图像.jpg", chessboard_1); imwrite(".\\image_save\\待配准图像棋盘图像.jpg", chessboard_2); imwrite(".\\image_save\\两幅棋盘图像叠加.jpg", mosaic_images); return 0; }
四、SIFT 算法实验结果
1、实验数据
说明:该数据是从网上下载的两个时相影像,左侧为旧时相影像,右侧为新时相影像;
2、实验结果
(1)特征点检测结果
(2)最终匹配结果
(3)配准效果
说明:把两张图像交叉重叠在一起,可以观察其配准效果;
五、配置说明
该结果是在 VS2019 + opencv4.4.0 + Debug x64 环境下跑出来的;
如果感觉复制麻烦,完整工程见:SIFT算法C++代码实现-C/C++文档类资源-CSDN下载