角点的位置校正也称亚像素角点检测算法,这个算法思想是基于经验与假设的。
(1)经验是:在角点所在的足够小的区域内的像素,要么在平坦区域(梯度为0),要么在角点所在的边缘上(梯度垂直于边缘,因为角点与边缘上的点连成的直线也垂直于梯度方向,这需要足够小的条件支持)。这导致如下一个公式成立,是角点领域内的任一像素点位置,是角点位置。该算法的就是根据这种理想的假设找到最佳角点位置。
(2)假设是:角点的精确位置应该角点附近,也就是领域满足足够小的假设条件。这样理想角点到邻域内任一点的连线才能拟合角点的某个边缘,从而直线才能垂直与这个像素的梯度方向。
注意:这个假设是调用cornerSubPix函数的时候取最佳参数的依据。
亚像素级的角点精准位置校正
-
亚像素级角点位置校正检测提供更高精度的坐标定位。强角点检测出来的角点位置实际存在偏移。就是从一个整数坐标,求出一个位置更加精准的坐标。该坐标因为计算的关系,最后的结果用小数表示。
-
这个算法是对已经存在的角点进行位置调整。
- 所以需要输入原图像,已经需要调整的角点。然后输出新的调整后的角点。
-
注意:
- 输入的角点,只需要坐标位置即可,不需要判定值。
OpenCV函数cornerSubPix的体验
- 函数定义
void cv::cornerSubPix(
InputArray image, // 输入图像
InputOutputArray corners, // 输入需要精细化的角点,输出精细化后的角点
// 类型:vector<cv::Point<float>>类型,也可以使用cv::Mat处理
Size winSize, // 亚像素的滑动窗体,就是高斯平滑窗体,使用半径
Size zeroZone, // 死区,在该区域不计算所有角点的求和。用来防止奇异点(没有可逆矩阵,因为算法采用的是最小二乘法)。
// 设置(-1,-1)表示没有这样的区域
TermCriteria criteria // 算法迭代的停止条件:一般俩个:(1)迭代到指定次数结束,(2)误差在可以接受范围内结束
)
-
TermCriteria类介绍
-
TermCriteria (int type, int maxCount, double epsilon)
- 三个public属性:type, maxCount,epsilon
- type是枚举类型 enmu Type:
- COUNT =1,
- MAX_ITER =COUNT,
- EPS =2
- 取值为三种形式:
- COUNT,
- EPS
- COUNT + EPS
- 使用方式记得使用命名空间与类前缀
- cv::TermCriteria::COUNT
- cv::TermCriteria::EPS
- cv::TermCriteria::COUNT + cv::TermCriteria::EPS
-
-
代码实现
#include <iostream>
#include <cmath>
#include <climits>
#include <opencv2/opencv.hpp>
/*
* 亚像素角点检测
*/
void call_opencv(){
cv::Mat img = cv::imread("corner.jpg");
cv::Mat img_src;
cv::cvtColor(img, img_src, cv::COLOR_BGR2GRAY); // OpenCV读取图像的颜色是BGR,灰度是8位图像
/*
* OpenCV标准的强角点检测算法
*/
// 1. 首先计算角点
cv::Mat corners;
// std::vector<cv::Point2f> corners;
cv::goodFeaturesToTrack(img_src, corners, 200, 0.01, 10, cv::noArray(), 9, true, 0.04);
std::cout << corners.dims << "," << corners.rows << "," << corners.cols << std::endl;
// 2. 然后计算精细化角点(就是焦点精细化)
cv::cornerSubPix(img_src, corners, cv::Size(3, 3), cv::Size(-1, -1), cv::TermCriteria(cv::TermCriteria::COUNT + cv::TermCriteria::EPS, 100, 0.001));
std::cout << corners.dims << "," << corners.rows << "," << corners.cols << std::endl;
for(int i = 0; i < corners.rows; i++){
cv::circle(img, corners.at<cv::Point2f>(i), 4, cv::Scalar(255, 0, 0, 255), 2, 8, 0);
}
cv::imwrite("corner_cv.jpg", img);
}
int main(int argc, char **argv){
call_opencv();
std::cout << "------------------------------" << std::endl;
return 0;
}
- 效果
亚像素角点位置精细校正算法
- 亚像素角点位置校正算法的目的:
- 对一个使用Harris或者Shi-tomasi算法检测出的角点,通过一个算法,得到精确的角点位置。见下面示意图。
- 红色是通过Harris算法或者Shi-Tomasi算法检测出来的角点;这些算法计算的位置不是特别准确(差分本身的性质决定的)
- 绿色点是我们需要找出来的精确角点位置。
- 对一个使用Harris或者Shi-tomasi算法检测出的角点,通过一个算法,得到精确的角点位置。见下面示意图。
-
算法的理论依据
- 精确的角点位置与周围邻域像素存在如下已知信息:
- 因为角点是边缘的交叉点,所以角点肯定在边缘上。
- 周围的像素点,要么在边缘上,要么在平坦区域。
- 精确的角点位置与周围邻域像素存在如下已知信息:
-
最精准的角点满足如下等式:
-
- 表示邻域像素的梯度(像素变化的方向)
- 是邻域像素与角点够的位置向量。
-
-
上述公式的分析
- 如果不在边缘,因为没有梯度变化,所以
- 如果在边缘上,根据梯度的描述,在足够小的范围内,,因为正交。(足够大的情况下,这个不成立的,但是因为我们在焦点周围查找最精准角点位置,这个假设条件是可以满足的)
- 但是因为计算缘故,上述公式不可能精确等于0从而存在误差,这个公式可以表述为:,这是所有领域点的误差的和。
-
问题的转换
- 位置最精准角点肯定是误差最小的q。问题就转换为求最小的q角点。
- 因为我们根据已知角点,来划分区域,则该区域是可能存在最精准位置的角点的。
- 如果已知角点与找到的最精准角点位置非常接近,则我们可以判定已知角点本身就是位置精准的角点。
- 根据上述描述,也可能不存在最精准位置的角点,这个我们可以
-
算法求解
- 设置,则
- 设置,则
-
算法说明
-
使用梯度的协方差矩阵表示
-
算法步骤
- 已知一个角点 ;
- 设置
- 使用位置得到一个邻域
- 根据上面公式得到
- 判定是否(1)或者(2)循环次数是否是最大次数或者(3)位置不在图像范围内或者(4)矩阵奇异,满足一个,则终止循环;否则,继续下一次循环:步骤3)
- 如果结束循环得到的角点位置为,则计算邻域窗口大小,则就是最精确角点位置,否则就是最精确角点位置。
-
提示:
- 这个计算量目测比较大。
- 上述算法没有考虑死区的设置,死区就是不参与计算。这个可以设置一个参数m来控制死区,死区参数设置为0
- 可以考虑是高斯核,与死区重叠那部分设置为0即可。(后面我们实现的算法不考虑死区了,如果发生奇异情况再说)
-
算法实现:
-
其中逆矩阵对二阶方阵来说使用如下公式:
-
其中的差分计算直接对整个图像计算(这样没有参与的像素也要计算,效率会低,但容易理解)
-
把取滑动窗口的函数单独封装
-
-
实现的核心代码代码
-
算法我们参考了OpenCV的源代码,但源代码写的晦涩难懂,效率是没有说的,依然使用连续内存块,用指针访问数据,这种方式主要是充分利用L2,L3缓存,可以参考我们写的关于GPU运算效率相关的主题。
-
获取窗口位置的函数
bool getROI(cv::Mat image, cv::Point2f center, cv::Size size, cv::Mat &roi){
// 感兴趣区域大小
int h = size.height;
int w = size.width;
// 计算中心坐标的整数值
int x = int(center.x + 0.5f);
int y = int(center.y + 0.5f);
// 计算ROI区域的左上角点
int lt_x = x - w / 2;
int lt_y = y - h / 2;
// ROI区域不包含在图像,则直接返回false,表示没有这个区域
if (lt_x < 0 || lt_y < 0 || (lt_x + w) >= image.cols || (lt_y + h) >= image.rows){
return false;
}
// 构造ROI区域的Rect描述
cv::Rect rect_roi(lt_x, lt_y, w, h);
roi = image(rect_roi).clone();
return true;
}
void yq_subpixel(cv::Mat image, cv::Mat &corners, cv::Size neighbor_size, int max_count, float epsilon){
// 构造一个Gauss核,大小为邻域大小, 因为getGaussianKernel函数返回的必须是方阵,所以下面我们自己编写一个
// 高斯函数的参数:方差窗体的半径, 均值为窗体中心
//搜索窗口大小
int w = neighbor_size.width * 2 + 1;
int h = neighbor_size.height * 2 + 1;
cv::Mat m_gauss = cv::Mat(h, w, CV_32FC1); // 浮点数float类型
for (int y = 0; y < h; y++){
for (int x = 0; x < w; x++){
float px = (float)(x - neighbor_size.width) / neighbor_size.width;
float py = (float)(y - neighbor_size.height) / neighbor_size.height;
float ex = exp(-px*px);
float ey = exp(-py*py);
m_gauss.at<float>(y, x) = (float)(ex*ey);
}
}
// 对每个角点进行精确位置校正
// 因为误差计算使用的是平方,所以参数的误差限制也平方
epsilon *= epsilon;
// 一次性把图像梯度计算好(可以考虑使用GPU计算)
cv::Mat I_x, I_y;
cv::Sobel(image, I_x, CV_32FC1, 1, 0, 3);
cv::Sobel(image, I_y, CV_32FC1, 0, 1, 3);
for(int i = 0; i < corners.rows; i++){
// 开始迭代找到精准位置点
// 下面五个量可以计算出新的q点
float a, b, c; // 协方差矩阵的四个元素,反对角线上的一样都是b
float c_x, c_y; // 协方差矩阵与邻域内点的内积后的向量的两个分量
float err; // 用来存放初始角点与新角点的距离误差,这是循环的终止条件
int n_iter; // 存放迭代的次数,这也是终止的判定条件
cv::Point2f init_corner = corners.at<cv::Point2f>(i); // 得到需要校正的角点
cv::Point2f new_corner = init_corner; // 用来存放新计算出来的角点,第一次就是初始角点,这个点用来决定滑动窗体的位置
n_iter = 0 ; // 初始化迭代次数
do{
// 使用角点的位置,与滑动窗体大小,取差分领域(两个)
cv::Mat window_x;
cv::Mat window_y;
// 取滑动窗口(没有就结束,意味着精确角点就是原角点)
if ( !getROI(I_x, new_corner, cv::Size(w, h), window_x)) break;
if ( !getROI(I_y, new_corner, cv::Size(w, h), window_y)) break;
// 计算M矩阵
a = b = c = 0.0f;
c_x = c_y = 0.0f;
for (int y = 0; y < h; y++){
for(int x = 0; x < w; x++){
// 取高斯值
float m = m_gauss.at<float>(y, x);
// 取梯度(差分)
float i_x = window_x.at<float>(y, x);
float i_y = window_y.at<float>(y, x);
// 计算IxIx , IxIy, IyIy
double i_xx = i_x * i_x * m;
double i_xy = i_x * i_y * m;
double i_yy = i_y * i_y * m;
a += i_xx;
b += i_xy;
c += i_yy;
// p_i的坐标(因为滑动窗口的中心点是初始角点位置,所以窗口内每个点的位置实际是相对位置,不是在原图像中的位置)
int p_i_x = x - neighbor_size.width;
int p_i_y = y - neighbor_size.height;
// 计算c_x,c_y
c_x += i_xx * p_i_x + i_xy * p_i_y;
c_y += i_xy * p_i_x + i_yy * p_i_y;
}
}
// 通过上面的循环,累加的M矩阵计算完毕
// 判定是否奇异(计算行列式的值是否接近0)
float det = a*c - b*b;
// 矩阵奇异就退出精确位置校正的循环
if (fabs(det) <= DBL_EPSILON * DBL_EPSILON)
break;
// 不奇异就计算逆矩阵(参考二阶方阵的逆矩阵计算公式)
double inv_a = c / det;
double inv_c = a / det;
double inv_b = -b / det;
// 最后计算出新的q角点位置(因为坐标以初始角点为中心,所以需要做平移)
cv::Point2f q;
q.x = (float)(new_corner.x + inv_a * c_x + inv_b * c_y);
q.y = (float)(new_corner.y + inv_b * c_x + inv_c * c_y);
// 计算误差
err = (q.x - new_corner.x)*(q.x - new_corner.x) + (q.y - new_corner.y)*(q.y - new_corner.y);
// 把新的点放入new_corner,开始下一次循环
new_corner = q;
// 迭代次数累加
n_iter ++;
// 判定新的角点位置是否在图像内(放在最后,前面一定要替换新角点,循环结束后,这种情况要做异常处理的,意味着角点位置还是原来的位置)
if (new_corner.x < 0 || new_corner.x >= image.cols || new_corner.y < 0 || new_corner.y >= image.rows){
break;
}
}while(n_iter < max_count && err > epsilon);
// 决定新的角点位置是否是合理的
if (fabs(new_corner.x - init_corner.x) > neighbor_size.width || fabs(new_corner.y - init_corner.y) > neighbor_size.height){
// 新的角点位置如果在窗体外面,则使用原来的角点位置(注意,图像外面的位置肯定在窗体外面)
new_corner = init_corner;
}
//保存算出的亚像素角点
corners.at<cv::Point2f>(i) = new_corner;
}
}
- 效果
- 计算的效果与OpenCV的计算效果一致。但我们的算法中明显牺牲计算效率,来换取对算法的理解。这里不贴图看效果,可以自己运行代码体验。完整代码见下面。
#include <iostream>
#include <cmath>
#include <climits>
#include <opencv2/opencv.hpp>
/*
* 亚像素角点检测
*/
void yq_subpixel(cv::Mat image, cv::Mat &corners, cv::Size neighbor_size, int max_count, float epsilon);
// 在图像取一个子区域
bool getROI(cv::Mat image, cv::Point2f center, cv::Size size, cv::Mat &roi){
// 感兴趣区域大小
int h = size.height;
int w = size.width;
// 计算中心坐标的整数值
int x = int(center.x + 0.5f);
int y = int(center.y + 0.5f);
// 计算ROI区域的左上角点
int lt_x = x - w / 2;
int lt_y = y - h / 2;
// ROI区域不包含在图像,则直接返回false,表示没有这个区域
if (lt_x < 0 || lt_y < 0 || (lt_x + w) >= image.cols || (lt_y + h) >= image.rows){
return false;
}
// 构造ROI区域的Rect描述
cv::Rect rect_roi(lt_x, lt_y, w, h);
roi = image(rect_roi).clone();
return true;
}
void call_opencv(){
cv::Mat img = cv::imread("corner.jpg");
cv::Mat img_src;
cv::cvtColor(img, img_src, cv::COLOR_BGR2GRAY); // OpenCV读取图像的颜色是BGR,灰度是8位图像
/*
* OpenCV标准的强角点检测算法
*/
// 1. 首先计算角点
cv::Mat corners;
// std::vector<cv::Point2f> corners;
cv::goodFeaturesToTrack(img_src, corners, 200, 0.01, 10, cv::noArray(), 9, true, 0.04);
std::cout << corners.dims << "," << corners.rows << "," << corners.cols << std::endl;
// 2. 然后计算精细化角点(就是焦点精细化)
cv::cornerSubPix(img_src, corners, cv::Size(3, 3), cv::Size(-1, -1), cv::TermCriteria(cv::TermCriteria::COUNT + cv::TermCriteria::EPS, 100, 0.001));
std::cout << corners.dims << "," << corners.rows << "," << corners.cols << std::endl;
for(int i = 0; i < corners.rows; i++){
cv::circle(img, corners.at<cv::Point2f>(i), 4, cv::Scalar(255, 0, 0, 255), 2, 8, 0);
}
cv::imwrite("corner_cv.jpg", img);
}
void call_subpix(){
cv::Mat img = cv::imread("corner.jpg");
cv::Mat img_src;
cv::cvtColor(img, img_src, cv::COLOR_BGR2GRAY); // OpenCV读取图像的颜色是BGR,灰度是8位图像
/*
* OpenCV标准的强角点检测算法
*/
// 1. 首先计算角点
cv::Mat corners;
// std::vector<cv::Point2f> corners;
cv::goodFeaturesToTrack(img_src, corners, 200, 0.01, 10, cv::noArray(), 9, true, 0.04);
std::cout << corners.dims << "," << corners.rows << "," << corners.cols << std::endl;
// 2. 然后计算精细化角点(就是焦点精细化)
yq_subpixel(img_src, corners, cv::Size(3, 3), 100, 0.001);
std::cout << corners.dims << "," << corners.rows << "," << corners.cols << std::endl;
for(int i = 0; i < corners.rows; i++){
cv::circle(img, corners.at<cv::Point2f>(i), 4, cv::Scalar(255, 0, 0, 255), 2, 8, 0);
}
cv::imwrite("corner_px.jpg", img);
}
int main(int argc, char **argv){
call_opencv();
std::cout << "------------------------------" << std::endl;
call_subpix();
return 0;
}
void yq_subpixel(cv::Mat image, cv::Mat &corners, cv::Size neighbor_size, int max_count, float epsilon){
// 构造一个Gauss核,大小为邻域大小, 因为getGaussianKernel函数返回的必须是方阵,所以下面我们自己编写一个
// 高斯函数的参数:方差窗体的半径, 均值为窗体中心
//搜索窗口大小
int w = neighbor_size.width * 2 + 1;
int h = neighbor_size.height * 2 + 1;
cv::Mat m_gauss = cv::Mat(h, w, CV_32FC1); // 浮点数float类型
for (int y = 0; y < h; y++){
for (int x = 0; x < w; x++){
float px = (float)(x - neighbor_size.width) / neighbor_size.width;
float py = (float)(y - neighbor_size.height) / neighbor_size.height;
float ex = exp(-px*px);
float ey = exp(-py*py);
m_gauss.at<float>(y, x) = (float)(ex*ey);
}
}
// 对每个角点进行精确位置校正
// 因为误差计算使用的是平方,所以参数的误差限制也平方
epsilon *= epsilon;
// 一次性把图像梯度计算好
cv::Mat I_x, I_y;
cv::Sobel(image, I_x, CV_32FC1, 1, 0, 3);
cv::Sobel(image, I_y, CV_32FC1, 0, 1, 3);
for(int i = 0; i < corners.rows; i++){
// 开始迭代找到精准位置点
// 下面五个量可以计算出新的q点
float a, b, c; // 协方差矩阵的四个元素,反对角线上的一样都是b
float c_x, c_y; // 协方差矩阵与邻域内点的内积后的向量的两个分量
float err; // 用来存放初始角点与新角点的距离误差,这是循环的终止条件
int n_iter; // 存放迭代的次数,这也是终止的判定条件
cv::Point2f init_corner = corners.at<cv::Point2f>(i); // 得到需要校正的角点
cv::Point2f new_corner = init_corner; // 用来存放新计算出来的角点,第一次就是初始角点,这个点用来决定滑动窗体的位置
n_iter = 0 ; // 初始化迭代次数
do{
// 使用角点的位置,与滑动窗体大小,取差分领域(两个)
cv::Mat window_x;
cv::Mat window_y;
// 取滑动窗口(没有就结束,意味着精确角点就是原角点)
if ( !getROI(I_x, new_corner, cv::Size(w, h), window_x)) break;
if ( !getROI(I_y, new_corner, cv::Size(w, h), window_y)) break;
// 计算M矩阵
a = b = c = 0.0f;
c_x = c_y = 0.0f;
for (int y = 0; y < h; y++){
for(int x = 0; x < w; x++){
// 取高斯值
float m = m_gauss.at<float>(y, x);
// 取梯度(差分)
float i_x = window_x.at<float>(y, x);
float i_y = window_y.at<float>(y, x);
// 计算IxIx , IxIy, IyIy
double i_xx = i_x * i_x * m;
double i_xy = i_x * i_y * m;
double i_yy = i_y * i_y * m;
a += i_xx;
b += i_xy;
c += i_yy;
// p_i的坐标(因为滑动窗口的中心点是初始角点位置,所以窗口内每个点的位置实际是相对位置,不是在原图像中的位置)
int p_i_x = x - neighbor_size.width;
int p_i_y = y - neighbor_size.height;
// 计算c_x,c_y
c_x += i_xx * p_i_x + i_xy * p_i_y;
c_y += i_xy * p_i_x + i_yy * p_i_y;
}
}
// 通过上面的循环,累加的M矩阵计算完毕
// 判定是否奇异(计算行列式的值是否接近0)
float det = a*c - b*b;
// 矩阵奇异就退出精确位置校正的循环
if (fabs(det) <= DBL_EPSILON * DBL_EPSILON)
break;
// 不奇异就计算逆矩阵(参考二阶方阵的逆矩阵计算公式)
double inv_a = c / det;
double inv_c = a / det;
double inv_b = -b / det;
// 最后计算出新的q角点位置(因为坐标以初始角点为中心,所以需要做平移)
cv::Point2f q;
q.x = (float)(new_corner.x + inv_a * c_x + inv_b * c_y);
q.y = (float)(new_corner.y + inv_b * c_x + inv_c * c_y);
// 计算误差
err = (q.x - new_corner.x)*(q.x - new_corner.x) + (q.y - new_corner.y)*(q.y - new_corner.y);
// 把新的点放入new_corner,开始下一次循环
new_corner = q;
// 迭代次数累加
n_iter ++;
// 判定新的角点位置是否在图像内(放在最后,前面一定要替换新角点,循环结束后,这种情况要做异常处理的,意味着角点位置还是原来的位置)
if (new_corner.x < 0 || new_corner.x >= image.cols || new_corner.y < 0 || new_corner.y >= image.rows){
break;
}
}while(n_iter < max_count && err > epsilon);
// 决定新的角点位置是否是合理的
if (fabs(new_corner.x - init_corner.x) > neighbor_size.width || fabs(new_corner.y - init_corner.y) > neighbor_size.height){
// 新的角点位置如果在窗体外面,则使用原来的角点位置(注意,图像外面的位置肯定在窗体外面)
new_corner = init_corner;
}
//保存算出的亚像素角点
corners.at<cv::Point2f>(i) = new_corner;
}
}
网友评论