作者认为现存的针对地点识别(visual place recognition)的一般方法——特征提取和匹配,均存在计算量较大的问题。人类在地点识别过程中,往往只会记住突出的特征(salient features)。受这个事实的启发,作者提出在频域上的突出特征检测和重定位方法。
建立离线视觉词袋,如在FABMAP中使用SURF特征建立词袋。DBoW2使用了类似的方法建立词袋,但使用了ORB描述子。
建立在线词袋的,如HBST,虽然速度上有较大提升,但内存消耗很大。
作者开源了代码,因此我感觉原理结合代码会更好理解,而且作者的代码也比较简洁和通俗易懂,不会占用过多篇幅。
作者在突出特征检测上使用的是log-spectral residual的方法,这个方法是《Saliency detection: A spectral residual approach》提出的方法。对于一个输入图像 I I I,假定 ( ⋅ ) ^ \hat{(\cdot)} (⋅)^标记为傅里叶变换。 A ( I ^ ) , P ( I ^ ) A(\hat{I}), P(\hat{I}) A(I^),P(I^)分别表示这个图像的幅值和相位。那么定义log-spectral residual为:
L ( I ^ ) = l o g ( A ( I ^ ) ) R ( I ^ ) = e x p ( L ( I ^ ) − f a v e ∗ L ( I ^ ) ) \begin{aligned} L(\hat{I}) &= log(A(\hat{I})) \\ R(\hat{I}) &= exp(L(\hat{I}) - f_{ave} * L(\hat{I})) \end{aligned} L(I^)R(I^)=log(A(I^))=exp(L(I^)−fave∗L(I^))
其中, f a v e f_{ave} fave为均值滤波器, ∗ * ∗为相关操作(cross-correlation)。于是突出特征图为:
M = G σ ∗ F − 1 ( R ( I ^ ) ⋅ e x p ( j ⋅ P ( I ^ ) ) ) M = G_\sigma * F^{-1}(R(\hat{I}) \cdot exp(j \cdot P(\hat{I}))) M=Gσ∗F−1(R(I^)⋅exp(j⋅P(I^)))
得到这个特征图后,就可以通过图像处理的连通性分析,找出突出特征的位置和boundding box。结合代码来看看:
// 一个MAT的数组,第一个放实部,第二个放虚部
cv::Mat planes[] = { cv::Mat_<float>(image.clone()), cv::Mat::zeros(image.size(), CV_32F) };
cv::Mat complexImg;
cv::merge(planes, 2, complexImg);
cv::dft(complexImg, complexImg); // 做傅里叶变换,存到complexImg中
cv::split(complexImg, planes); // 分成实部和虚部
cv::Mat mag, logmag, smooth, spectralResidual;
cv::magnitude(planes[0], planes[1], mag); // 计算幅值A(I)
cv::log(mag, logmag); // => log(sqrt(Re(DFT(I))^2 + Im(DFT(I))^2))
cv::boxFilter(logmag, smooth, -1, cv::Size(average_filter_size, average_filter_size)); // 对log幅值均值滤波
cv::subtract(logmag, smooth, spectralResidual);
cv::exp(spectralResidual, spectralResidual);
// 最终得到 M = exp(log(A(I)) - f_ave * log(A(I)))
// real part
planes[0] = planes[0].mul(spectralResidual) / mag;
// imaginary part
planes[1] = planes[1].mul(spectralResidual) / mag;
// 上面两句下面note解释。
cv::merge(planes, 2, complexImg);
cv::dft(complexImg, complexImg, cv::DFT_INVERSE | cv::DFT_SCALE); // 傅里叶反变换
cv::split(complexImg, planes);
cv::magnitude(planes[0], planes[1], mag);
cv::multiply(mag, mag, mag); // 获得最终特征图
cv::GaussianBlur(mag, mag, cv::Size(5, 5), 8, 8); // 高斯滤波
note:
上面两句是怎么和公式对应的呢?
R ( I ^ ) ⋅ e x p ( j ⋅ P ( I ^ ) ) = R ( I ^ ) ⋅ A ( I ^ ) e j ⋅ P ( I ^ ) / A ( I ^ ) = R ( I ^ ) ⋅ ( R e ( I ^ ) + I m ( I ^ ) ) / A ( I ^ ) = R ( I ^ ) ⋅ R e ( I ^ ) / A ( I ^ ) + R ( I ^ ) ⋅ I m ( I ^ ) / A ( I ^ ) \begin{aligned} R(\hat{I}) \cdot exp(j\cdot P(\hat{I})) &= R(\hat{I}) \cdot A(\hat{I}) e^{j\cdot P(\hat{I})} / A(\hat{I}) \\ &= R(\hat{I}) \cdot (Re(\hat{I}) + Im(\hat{I})) / A(\hat{I}) \\ &= R(\hat{I}) \cdot Re(\hat{I}) / A(\hat{I}) + R(\hat{I}) \cdot Im(\hat{I}) / A(\hat{I}) \end{aligned} R(I^)⋅exp(j⋅P(I^))=R(I^)⋅A(I^)ej⋅P(I^)/A(I^)=R(I^)⋅(Re(I^)+Im(I^))/A(I^)=R(I^)⋅Re(I^)/A(I^)+R(I^)⋅Im(I^)/A(I^)
于是得到上两句公式!!
初步检测到特征图后,作者需要对特征图进行筛选。为此作者设计了两个指标:
其中,x是提取的特征区域, x ‾ \overline{x} x是这个区域的平均像素,C是canny边缘滤波器,S为这个区域像素的个数。看看代码:
cv::Mat object; // 特征区域
image_float(cv::Rect(x, y, w, h)).copyTo(object);
// check contrast density
float mean = cv::sum(object).val[0] / (w*h); // 像素均值
float cov = 0.0;
for (int p = 0;p < h;p++) {
for (int q = 0;q < w;q++) {
cov += fabs(object.at<float>(p, q) - mean);
}
}
cov = cov / (w*h);
// 这里用了绝对值距离,并除以了像素总数,和论文貌似不符?
if (cov < min_cov)
continue;
// check egde complexity
cv::Mat edge;
cv::Laplacian(object, edge, CV_32F, 3);
float edge_cov = 0;
// 这里用了拉式算子检测边缘,边缘响应强度大于50就算作检测到边缘
for (int p = 0;p < h;p++) {
for (int q = 0;q < w;q++) {
if(abs(edge.at<float>(p, q))>50)
edge_cov += 1;
}
}
edge_cov = edge_cov / (w*h);
if (edge_cov < min_edge_cov)
continue;
作者使用了核相关滤波(kernel cross-correlation)的方法进行两个突出特征块的匹配。此方法能够在频域直接进行信号匹配,而且具有位移、旋转和尺度不变性。其实相关滤波的方法在目标跟踪领域非常流行,如MOSSE、KCF和DSST这些算法都是使用相关滤波做的。
相关滤波的基本思想就是,对于 g = x ∗ h g = x * h g=x∗h( x x x为图像信号, h h h为滤波器, ∗ * ∗为相关操作),找到一个最佳的滤波器h,使得输出g是期望输出。而相关操作在频域上计算相当快。
规定一下符号: ( ⋅ ) ^ \hat{(\cdot)} (⋅)^为傅里叶变换, ∗ , ⨀ , h ∗ *, \bigodot, h^* ∗,⨀,h∗分别表示相关、按元素乘积和共轭。于是核相关定义为:
x和z文中说分别是候选(存在数据库)特征和当前特征,但我认为是相反的。在代码部分我会说明我的理由,暂时先按我的理解。公式中的 z i j z_{ij} zij表示特征块水平平移i个像素垂直平移j个像素,这是为了实现平移不变性的,正确的平移位置应该会有更大的响应。这个核函数是高斯核,代入高斯核后,这个 k z ( x ) k_z(x) kz(x)就可以通过下式得到:
不过这里为什么计算复杂度从平方变到对数这里我不太清楚。
现在知道 k z ( x ) k_z(x) kz(x)怎么计算,但还有最优滤波器 h ^ ∗ \hat{h}^* h^∗未知。这个滤波器可以看成是一个优化问题,使用平方和误差(SSE)进行优化:
从(10)式看到,这个滤波器有闭式解,因此可以在线求解,而不需要离线准备一个预训练的数据库。作者在求h的过程中,令x=z,而g则是使用了下式决定:
g ( x , y ) = { 1 , i f x = 0 , y = 0 0 , o t h e r w i s e g(x,y) = \left\{ \begin{aligned} 1 \ &, \ if \ \ x=0, y=0\\ 0 \ &, \ \ \ \ \ \ \ otherwise \end{aligned} \right. g(x,y)={1 0 , if x=0,y=0, otherwise
但代码好像也不是用这个g的,下面会说。
最后就是相似性评价,因为最后g的输出肯定不可能和期望输出一模一样,所以取g的最大响应作为相似性得分:
梳理一下步骤,看下图:
上面那一行,代表存在数据库的图片,里面有一个标志牌的突出特征图像块,做傅里叶变换后训练 h ^ ∗ \hat{h}^* h^∗,由闭式解(10)能求出这个突出特征对应的最优滤波器( k ^ z ( x ) \hat{k}_z(x) k^z(x)是由x=z计算的),存在数据库。下面那一行是当前图片,检测突出特征,傅里叶变换,然后计算 k ^ z ( x ) \hat{k}_z(x) k^z(x)(而这里x是当前突出特征块,z是数据库突出特征块),最后计算 k ^ z ( x ) \hat{k}_z(x) k^z(x)和 h ^ ∗ \hat{h}^* h^∗的乘积,傅里叶反变换后得到响应 g g g,在求其最大值得到相似性评分。
下面看看代码:
// 查询数据库
for (int i = 0;i < image_database.size();i++) {
// 先把目标特征resize到和数据库特征尺寸大小一样
cv::resize(cvimage, cvimage, cv::Size(image_database[i].salient_region.cols(), image_database[i].salient_region.rows()));
Eigen::Map<Eigen::ArrayXXf> image_eigen(&cvimage.at<float>(0, 0), cvimage.cols, cvimage.rows);;
// 计算相似性
float similarity = kcc_test(image_database[i].salient_region_fft, fft(image_eigen.transpose()), image_database[i].h_hat_star);
similarity_matrix(sr.frame_num, image_database[i].frame_num) = max(similarity_matrix(sr.frame_num, image_database[i].frame_num), similarity);
if (similarity > loop_threshold && abs(sr.frame_num- image_database[i].frame_num)> frame_space) {
result.push_back(Loop(sr.frame_num, image_database[i].frame_num, similarity));
}
if (max_similarity < similarity)
max_similarity = similarity;
}
上面做的事就是对于目标特征块cvimage,不断查询数据库,然后通过函数kcc_test计算相似性。下面看kcc_test函数:
ArrayXXf xy = ifft(image_train * image_test.conjugate()); // test里用的x和z是不同的
ArrayXXf xxyy = (xx + yy - 2 * xy) / N;
for (int i = 0;i < xxyy.rows();i++) {
for (int j = 0;j < xxyy.cols();j++) {
if (xxyy(i, j) < 0)
xxyy(i, j) = 0;
}
}
ArrayXXcf kxy_hat = fft((-1 / (sigma*sigma)*xxyy).exp());
// 以上计算\hat{k}_z(x)
ArrayXXf result = ifft(kxy_hat*h_hat_star);
// 这里这个h_hat_star(最优滤波器),是作为参数传进来的,是kcc_train函数训练出来的,每个数据库特征块都有一个独自的最优滤波器。
float max_num = 0.0;
// 求g的最大值作为相似性评分
for (int i = 0;i < xxyy.rows()/6;i++) {
for (int j = 0;j < xxyy.cols()/6;j++) {
if (result(i, j) > max_num)
max_num = result(i, j);
}
}
//std::cout << result.row(0) << std::endl;
return max_num;
下面看看kcc_train,这个函数计算耗时很少,因此可以在线计算:
int N = image_fft.rows()*image_fft.cols();
ArrayXXf gaussian_mask = ArrayXXf::Zero(image_fft.rows(), image_fft.cols());
// 这里为什么除以49我也不清楚
float row_sum = image_fft.rows()*image_fft.rows() / 49.0;
float col_sum = image_fft.cols()*image_fft.cols() / 49.0;
for (int i = 0;i < image_fft.rows();i++) {
for (int j = 0;j < image_fft.cols();j++) {
gaussian_mask(i,j) = exp(-((i+1)*(i+1)/ row_sum +(j+1)*(j+1)/ col_sum)/2);
}
}
ArrayXXcf g_hat = fft(gaussian_mask);
// 这里使用了帕森瓦尔定理,在时域图像的像素平方和等于在频域的平方和除以一个常数
float xx = image_fft.abs2().sum() / N; // Parseval's Theorem
ArrayXXf xy = ifft(image_fft * image_fft.conjugate());
// 这里train用到的x和z都是x
ArrayXXf xxyy = (xx + xx - 2 * xy) / N;
for (int i = 0;i < xxyy.rows();i++) {
for (int j = 0;j < xxyy.cols();j++) {
if (xxyy(i, j) < 0)
xxyy(i, j) = 0;
}
}
ArrayXXcf kxx_hat = fft((-1 / (sigma*sigma)*xxyy).exp());
h_hat_star = g_hat / (kxx_hat + lambda);
note:
这里 g ^ \hat{g} g^是通过对gaussian_mask的傅里叶变换得到的,然而gaussuan_mask的表达式为:
g ( x , y ) = e − ( x 2 r 2 + y 2 c 2 ) / 2 g(x,y) = e^{-(\frac{x^2}{r^2} + \frac{y^2}{c^2})/2} g(x,y)=e−(r2x2+c2y2)/2
虽然和原来的期望响应也有共同之处,就是在(0,0)处响应是最大的,但还是有所区别。
经过上面两个步骤得到的回环检测只是对于当前帧的候选回环,至于是不是真正的回环,还需要经过一个检验。检验的方法非常简单,当前帧和候选帧进行ORB特征提取和匹配,设定一个阈值,如果匹配点大于这个阈值就说明当前帧构成回环。
左、中、右分别是原图,特征检测图和在这基础上进行特征筛选的图。效果感觉一般般,确实有些显著的特征提取了出来。特征筛选后也有部分较差的特征块被去掉。
我使用了New College 数据集进行实验,但效果好像不是太好,我手动调了一下参数好像也不太行。
暂时搞不出来,在问作者中ing。。。
[1] Han Wang, Chen Wang, Lihua Xie.Online visual place recognition via saliency re-identification[J].International Conference on Intelligent Robots and Systems 2020 (IROS)