一、理论
文章看了以下博文:Sift中尺度空间、高斯金字塔、差分金字塔(DOG金字塔)、图像金字塔-CSDN博客
该文章对SIFT写的很详细,所以在这里我直接抄过来作为笔记。如果以后作者变为付费文章可以提醒我删除。
1.图像金字塔
图像金字塔是一种以多分辨率来解释图像的结构,通过对原始图像进行多尺度像素采样的方式,生成N个不同分辨率的图像。把具有最高级别分辨率的图像放在底部,以金字塔形状排列,往上是一系列像素(尺寸)逐渐降低的图像,一直到金字塔的顶部只包含一个像素点的图像,这就构成了传统意义上的图像金字塔。
获得图像金字塔一般包括二个步骤:
1. 利用低通滤波器平滑图像
2. 对平滑图像进行抽样(采样)
有两种采样方式——上采样(分辨率逐级升高)和下采样(分辨率逐级降低)
上采样:
下采样:
2.高斯金字塔
高斯金字塔式在Sift算子中提出来的概念,首先高斯金字塔并不是一个金字塔,而是有很多组(Octave)金字塔构成,并且每组金字塔都包含若干层(Interval)。
高斯金字塔构建过程:
1. 先将原图像扩大一倍之后作为高斯金字塔的第1组第1层,将第1组第1层图像经高斯卷积(其实就是高斯平滑或称高斯滤波)之后作为第1组金字塔的第2层,高斯卷积函数为:
对于参数σ,在Sift算子中取的是固定值1.6。
2. 将σ乘以一个比例系数k,等到一个新的平滑因子σ=k*σ,用它来平滑第1组第2层图像,结果图像作为第3层。
3. 如此这般重复,最后得到L层图像,在同一组中,每一层图像的尺寸都是一样的,只是平滑系数不一样。它们对应的平滑系数分别为:0,σ,kσ,k^2σ,k^3σ……k^(L-2)σ。
4. 将第1组倒数第三层图像作比例因子为2的降采样,得到的图像作为第2组的第1层,然后对第2组的第1层图像做平滑因子为σ的高斯平滑,得到第2组的第2层,就像步骤2中一样,如此得到第2组的L层图像,同组内它们的尺寸是一样的,对应的平滑系数分别为:0,σ,kσ,k^2σ,k^3σ……k^(L-2)σ。但是在尺寸方面第2组是第1组图像的一半。
这样反复执行,就可以得到一共O组,每组L层,共计O*L个图像,这些图像一起就构成了高斯金
字塔,结构如下:
在同一组内,不同层图像的尺寸是一样的,后一层图像的高斯平滑因子σ是前一层图像平滑因子的k倍;
在不同组内,后一组第一个图像是前一组倒数第三个图像的二分之一采样,图像大小是前一组的一半;
高斯金字塔图像效果如下,分别是第1组的4层和第2组的4层:
3.尺度空间
图像的尺度空间解决的问题是如何对图像在所有尺度下描述的问题。
在高斯金字塔中一共生成O组L层不同尺度的图像,这两个量合起来(O,L)就构成了高斯金字塔的尺度空间,也就是说以高斯金字塔的组O作为二维坐标系的一个坐标,不同层L作为另一个坐标,则给定的一组坐标(O,L)就可以唯一确定高斯金字塔中的一幅图像。
尺度空间的形象表述:
上图中尺度空间中k前的系数n表示的是第一组图像尺寸是当前组图像尺寸的n倍。
4.DOG金字塔
生成高斯金字塔的目的就是为了构建DOG金字塔。
DOG金字塔的第1组第1层是由高斯金字塔的第1组第2层减第1组第1层得到的。以此类推,逐组逐层生成每一个差分图像,所有差分图像构成差分金字塔。概括为DOG金字塔的第o组第l层图像是有高斯金字塔的第o组第l+1层减第o组第l层得到的。
DOG金字塔的构建可以用下图描术:
每一组在层数上,DOG金字塔比高斯金字塔少一层。后续Sift特征点的提取都是在DOG金字塔上进行的。
DOG金字塔的显示效果如下:
这些长得黑乎乎的图像就是差分金字塔的实际显示效果,只在第1组第1层差分图像上模糊可以看到一个轮廓。但其实这里边包含了大量特征点信息,只是我们人眼已经分辨不出来了。
下边对这些DOG图像进行归一化,可有很明显的看到差分图像所蕴含的特征,并且有一些特征是在不同模糊程度、不同尺度下都存在的,这些特征正是Sift所要提取的“稳定”特征:
二、算法代码
1.python代码
import cv2
from matplotlib import pyplot as plt
img1 = cv2.imread('box.png',0) # queryImage
img2 = cv2.imread('box_in_scene.png',0) # trainImage
# Initiate SIFT detector
sift = cv2.SIFT()
# find the keypoints and descriptors with SIFT
kp1, des1 = sift.detectAndCompute(img1,None)
kp2, des2 = sift.detectAndCompute(img2,None)
# BFMatcher with default params
bf = cv2.BFMatcher()
matches = bf.knnMatch(des1,des2,k=2)
# Apply ratio test
good = []
for m,n in matches:
if m.distance < 0.75*n.distance:
good.append([m])
# cv2.drawMatchesKnn expects list of lists as matches
img3 = cv2.drawMatchesKnn(img1,kp1,img2,kp2,good,flags=2)
plt.imshow(img3)
2.C++代码
#include <opencv2/opencv.hpp>
#include <iostream>
#include <opencv2/xfeatures2d.hpp>
void extracte_sift(cv::Mat input,std::vector<cv::KeyPoint> &keypoint,cv::Mat &descriptor){
cv::Ptr<cv::Feature2D> f2d = cv::xfeatures2d::SIFT::create();
f2d->detect(input,keypoint);
cv::Mat image_with_kp;
f2d->compute(input,keypoint,descriptor);
cv::drawKeypoints(input, keypoint, image_with_kp, cv::Scalar::all(-1),cv::DrawMatchesFlags::DRAW_RICH_KEYPOINTS);
cv::imwrite("sift"+std::to_string(keypoint.size())+".png",image_with_kp);
}
void match_two_image(cv::Mat image1,cv::Mat image2, std::vector<cv::KeyPoint> keypoint1,std::vector<cv::KeyPoint> keypoint2,cv::Mat descriptor1,cv::Mat descriptor2){
cv::FlannBasedMatcher matcher;
std::vector<cv::DMatch> matches, goodmatches;
matcher.match(descriptor1,descriptor2, matches);
cv::Mat good_matches_image;
double max_dist = 0; double min_dist = 1000;
for (int i = 0; i < descriptor1.rows; i++) {
if (matches[i].distance > max_dist) {
max_dist = matches[i].distance;
}
if (matches[i].distance < min_dist) {
min_dist = matches[i].distance;
}
}
for (int i = 0; i < descriptor1.rows; i++) {
if (matches[i].distance < 4 * min_dist) {
goodmatches.push_back(matches[i]);
}
}
cv::drawMatches(image1, keypoint1, image2, keypoint2,
goodmatches, good_matches_image, cv::Scalar::all(-1), cv::Scalar::all(-1),
std::vector<char>(), cv::DrawMatchesFlags::NOT_DRAW_SINGLE_POINTS);
cv::imwrite("good_matches_image.png",good_matches_image);
{
std::vector <cv::KeyPoint> RAN_KP1, RAN_KP2;
std::vector<cv::Point2f> keypoints1, keypoints2;
for (int i = 0; i < goodmatches.size(); i++) {
keypoints1.push_back(keypoint1[goodmatches[i].queryIdx].pt);
keypoints2.push_back(keypoint2[goodmatches[i].trainIdx].pt);
RAN_KP1.push_back(keypoint1[goodmatches[i].queryIdx]);
RAN_KP2.push_back(keypoint2[goodmatches[i].trainIdx]);
}
std::vector<uchar> RansacStatus;
cv::findFundamentalMat(keypoints1, keypoints2, RansacStatus, cv::FM_RANSAC);
std::vector <cv::KeyPoint> ransac_keypoints1, ransac_keypoints2;
std::vector <cv::DMatch> ransac_matches;
int index = 0;
for (size_t i = 0; i < goodmatches.size(); i++)
{
if (RansacStatus[i] != 0)
{
ransac_keypoints1.push_back(RAN_KP1[i]);
ransac_keypoints2.push_back(RAN_KP2[i]);
goodmatches[i].queryIdx = index;
goodmatches[i].trainIdx = index;
ransac_matches.push_back(goodmatches[i]);
index++;
}
}
cv::Mat after_ransac_sift_match;
cv::drawMatches(image1, ransac_keypoints1, image2, ransac_keypoints2,
ransac_matches, after_ransac_sift_match, cv::Scalar::all(-1), cv::Scalar::all(-1),
std::vector<char>(), cv::DrawMatchesFlags::NOT_DRAW_SINGLE_POINTS);
cv::imwrite("after_ransac_sift_match.png",after_ransac_sift_match);
}
}
int main(int argc, char *argv[])
{
cv::Mat image1 = cv::imread(argv[1]);
cv::Mat image2 = cv::imread(argv[2]);
std::vector<cv::KeyPoint> keypoint1,keypoint2;
cv::Mat descriptor1, descriptor2;
extracte_sift(image1,keypoint1,descriptor1);
extracte_sift(image2,keypoint2,descriptor2);
match_two_image(image1,image2,keypoint1,keypoint2,descriptor1,descriptor2);
return 0;
}
下面是另一个作者的理论解释,该解释更多的是上述理论的一些细节解释。在这里仍然是抄过来作为笔记,以便更深入的了解SIFT算法。博文地址如下:
SIFT特征匹配算法及代码(python)_sift算法那个python工具好-CSDN博客
1. SIFT(尺度不变特征变换)
David Lowe提出的SIFT(Scale-Invariant Feature Transform)是最成功的图像局部描述子之一。SIFT特征包括兴趣点检测器和描述子,其中SIFT描述子具有非常强的稳健性,这在很大程度上也是SIFT特征能够成功和流行的主要原因。SIFT特征对于尺度、旋转、亮度都具有不变性,下面会详细介绍其原理。
1.1 尺度空间的构建
图像的尺度空间是这幅图像在不同解析度下的表示。一幅图像可以产生几组(octave)图像,一组图像包括几层图像。构造尺度空间传统的方法即构造一个高斯金字塔,原始图像作为最底层,然后对图像进行高斯模糊再降采样(2倍)作为下一层图像(即尺度越大,图像越
模糊),循环迭代下去。
对图像进行尺度变换,以满足特征点的尺度不变性,保留图像轮廓和细节。
1.1.1 DOG算子
DoG(Difference of Gaussian)函数:
该函数在计算上只需相邻高斯平滑后图像相减,因此简化了计算。
1.1.2 DoG高斯差分金字塔
对应DOG算子,需构建DOG金字塔。
可以通过高斯差分图像看出图像上的像素值变化情况。(如果没有变化,也就没有特征。特征必须是变化尽可能多的点。)DOG图像描绘
的是目标的轮廓。
从图像中可看出越位于金字塔上层(差分越大)图像越模糊,但能得到图像的轮廓;反之,越处于金字塔底端(差分越小)图像的细节就越清晰。即保留了图像的轮廓和细节。
1.2 选定关键点
1.2.1 局部极值点
特征点是由DOG空间的局部极值点组成的。为了寻找DoG函数的极值点,每一个像素点要和它所有的相邻点比较,看其是否比它的图像域和尺度域的相邻点大或者小。如下图中检测特征点与自身尺度层中其余8个点和在其之上及之下的两个尺度层9个点进行比较,共26个点,图中标记‘x’的像素点的特征值若大于周围像素则可确定该点为该区域的特征点。
1.2.2 去除边缘响应点
由于DoG函数在图像边缘有较强的边缘响应,因此需要排除边缘响应。DoG函数的峰值点在边缘方向有较大的主曲率,而在垂直边缘的方
向有较小的主曲率。主曲率可以通过计算在该点位置尺度的2×2的Hessian矩阵得到,导数由采样点相邻差来估计:
Dxx 表示DOG金字塔中某一尺度的图像x方向求导两次。
D的主曲率和H的特征值成正比。令 α ,β为特征,则
该值在两特征值相等时达最小。Lowe论文中建议阈值T为1.2,即
时保留关键点,反之剔除。
1.3 关键点方向分配
通过尺度不变性求极值点,可以使其具有缩放不变的性质。而利用关键点邻域像素的梯度方向分布特性,可以为每个关键点指定方向参数方向,从而使描述子对图像旋转具有不变性通过求每个极值点的梯度来为极值点赋予方向。
像素点的梯度表示:
梯度幅值:
梯度方向:
1.3.1 方向直方图的生成
确定关键点的方向采用梯度直方图统计法,统计以关键点为原点,一定区域内的图像像素点对关键点方向生成所作的贡献。
1.3.2 关键点的主方向与辅方向
关键点主方向:极值点周围区域梯度直方图的主峰值也是特征点方向
关键点辅方向:在梯度方向直方图中,当存在另一个相当于主峰值80%能量的峰值时,则将这个方向认为是该关键点的辅方向。
这可以增强匹配的鲁棒性,Lowe的论文指出大概有15%关键点具有多方向,但这些点对匹配的稳定性至为关键。
1.4 生成特征点描述子
下图是一个SIFT描述子事例。其中描述子由2×2×8维向量表征,也即是2×2个8方向的方向直方图组成。左图的种子点由8×8单元组成。每一个小格都代表了特征点邻域所在的尺度空间的一个像素,箭头方向代表了像素梯度方向,箭头长度代表该像素的幅值。然后在4×4的窗口内计算8个方向的梯度方向直方图。绘制每个梯度方向的累加可形成一个种子点,如右图所示:一个特征点由4个种子点的信息所组成。
Lowe实验结果表明:描述子采用4×4×8=128维向量表征,综合效果最优(不变性与独特性)。
1.5 关键点匹配
分别对模板图(参考图,reference image)和实时图(观测图,observation image)建立关键点描述子集合。目标的识别是通过两点集内关键点描述子的比对来完成。具有128维的关键点描述子的相似性度量采用欧式距离。欧氏距离越短,代表两个特征点的匹配度越好。
关键点的匹配可以采用穷举法来完成,但是这样耗费的时间太多,一般都采用kd树的数据结构来完成搜索。搜索的内容是以目标图像的关键点为基准,搜索与目标图像的特征点最邻近的原图像特征点和次邻近的原图像特征点。