CV03-02:Harris角点检测

2020-02-07  本文已影响0人  杨强AT南京

  疫情当前,不想说什么,就撸了这个技术文档,\color{red}{\texttt{Harris角点检测}}。这是特征检测的基础。本技术主题不是OpenCV的应用,而是解释角点检测算法从最质朴的像素变化思想到Harris算法。并使用OpenCV提供的数据结构,手工实现了一遍,效果还不错。下面是opencv与我们实现的对比:

实现对比
  值得一提的是Harris算法的精妙之处在于通过差分分布的方向来检测角点,通过PCA思维解决了传统Moravec算法的方向性,数学真是奇妙的东西。
  尽管本文使用的是C++实现Harris角点检测算法,但还是用python写了一个动画可视化代码,直观展式了Harris算法的响应值R与正定二次型矩阵的两个特征值的动态变化关系: R=\lambda_1 \lambda_2 - k(\lambda_1 + \lambda_2)^2

理解角点

从OpenCV的cornerHarris应用理解角点

  1. 函数定义
    void cv::cornerHarris (InputArray src, OutputArray dst, int blockSize, int ksize, double k, int borderType=BORDER_DEFAULT)
  1. 角点检测的数学模型:

    • \texttt{dst} (x,y) = \mathrm{det} M^{(x,y)} - k \cdot \left ( \mathrm{tr} M^{(x,y)} \right )^2
    • 该模型公式后面推导与解释。
  2. 参数说明

    • InputArray src,
      • 被检测特征的输入图像,必须是单通道CV_8U与浮点数图像。
    • OutputArray dst,
      • Harris角点检测计算输出,输出类型是CV_32FC1,大小与原图像一样。
    • int blockSize,
      • 角点检测算法中的一个窗口大小。是每次计算的部分区域,就是扫描区域。
    • int ksize,
      • 角点检测使用了差分算子,这是差分算子核的大小。该参数定义了角点检测的敏感度,其值必须介于3~31之间的奇数
    • double k,
      • 角点检测中一个自由因子;见上面公式。建议取值一般取0.04~0.06之间。
    • int borderType=BORDER_DEFAULT
  3. 一个简单的角点的直观例子

    • 把Harris的结果归一化保存为图像,可以直观看见角点的结果。
#include <iostream>
#include <opencv2/opencv.hpp>
/*
    通过调用cornerHarris函数,来直观认识什么是角点。
*/
int main(int argc, char **argv){
    // 1. 读取图像
    cv::Mat img_src = cv::imread("harris.jpg");
    // 2. 图像转换为单通道图像(灰度图像)
    cv::cvtColor(img_src, img_src, cv::COLOR_BGR2GRAY);  // OpenCV读取图像的颜色是BGR,灰度是8位图像
    cv::imwrite("harris_gray.jpg", img_src);
    // 3. Harris角点计算
    cv::Mat img_out;
    cv::cornerHarris(img_src, img_out, 5, 11, 0.04);
    // 4. 计算结构保存
    cv::imwrite("harris_out.jpg", img_out);
    return 0;
}

窗体为5,差分核为11,自由参数为0.4的Harris角点
  1. Harris角点检测算法中参数对检测的作用
使用参数调整,观察角点的计算结果

角点的常见类型

  1. 角点就是轮廓或者边缘的角点。

  2. 角点大致有如下5中交叉情形:

    • 其中的交叉角度可以变化。
角点常见的5中情形

角点的数学原理

角点检测算法朴素的思想

  1. 角点检测算法思想
    • 对图像中某个像素点检测周围点的像素变化,周围一共8个像素点。描述一个像素与周围像素的差异变化的数学表示非常简单:
      • \texttt{MO}(x,y) = \dfrac{1}{8} \sum \limits _{u=x-1} ^{x+1} \sum _{v=y-1} \limits ^{v=y+1} | f(u, v) - f(x,y) |
Moravec角点算法示意图

Moravec算子

  1. Moravec算子采用的灰度变化不是采用相邻像素,而是采用滑动窗口的方式。像素变化计算使用公式表示为:

    • V = \sum \limits _{i=1}^{9} (A_i - B_i) ^ 2
    • 滑动窗口采用如下示意图表示,示意图只说明了一个方向的灰度变化,实际上是8个方向的灰度计算。
  2. Moravec算子角点检测算法可以描述为:

    • 通过一个滑动的二值矩阵窗口寻找灰度变化的局部最大值。
      • 为了抑制敏感性,其中灰度变化通过一个阈值来过滤。具体阈值的计算见下面公式。
Moravec灰度变化计算示意图
  1. 灰度变化值计算:
    • 上面通过滑动窗口,得到8个灰度变化值,8个方向都有变化的才是角点,随意灰度变化值的计算公式为:(其中为了便于理解,简化了窗口中像素下标的表示方式,采用1-9顺序方式,实际程序实现的时候需要按照中心点为远点的坐标 表示方式)
      • V_{0,1}(x,y) = \sum \limits _{i=1}^{9} (A(i) - B_{0,1}(i))^2
      • V_{1,0}(x,y) = \sum \limits _{i=1}^{9} (A(i) - B_{1,0}(i))^2
      • V_{1,1}(x,y) = \sum \limits _{i=1}^{9} (A(i) - B_{1,1}(i))^2
      • V_{0,-1}(x,y) = \sum \limits _{i=1}^{9} (A(i) - B_{0,-1}(i))^2
      • V_{-1,0}(x,y) = \sum \limits _{i=1}^{9} (A(i) - B_{-1,0}(i))^2
      • V_{-1,-1}(x,y) = \sum \limits _{i=1}^{9} (A(i) - B_{-1, -1}(i))^2
      • V_{-1,1}(x,y) = \sum \limits _{i=1}^{9} (A(i) - B_{-1,1}(i))^2
      • V_{1,-1}(x,y) = \sum \limits _{i=1}^{9} (A(i) - B_{1,-1}(i))^2
  1. 角点检测:

    • 使用一个阈值,小于阈值的非角点,大于阈值的就是角点。
  2. 直观理解角点检测


    角点检测与灰度变化的关系
  3. Moravec角点检测法的几个特点:

    1. 对噪音敏感(见上图):可以增加滑动窗体降低。
    2. 方向性(8个方向上下左右,对角45度,其他方向检测不考虑),导致对旋转不具备重现性。
    3. 计算灰度变化,滑动窗口中所有像素贡献一样(按照概率思维,离计算点越远的像素贡献应该越小,应该使用不同的权重体现贡献)

Moravec角点检测算法实现

无滑动窗口角点检测

#include <iostream>
#include <cmath>
#include <opencv2/opencv.hpp>
/*
    1. 实现无滑动窗口角点检测算法
*/
// 无窗口角点检测
void moravec_nowindow(cv::Mat &src, cv::Mat &dst, float threshold);


int main(int argc, char **argv){
    cv::Mat img = cv::imread("corner.jpg");
    cv::Mat img_src;
    cv::cvtColor(img, img_src, cv::COLOR_BGR2GRAY);  // OpenCV读取图像的颜色是BGR,灰度是8位图像

    cv::Mat img_out;
    // 无窗口角点检测
    moravec_nowindow(img_src, img_out, 120.0f);
    cv::imwrite("harris_no.jpg", img_out); 
    // mark角点到原图
    cv::Mat img_mark;
    img.copyTo(img_mark);
    for(int y = 0; y < img_out.rows; y++){
        for(int x = 0; x < img_out.cols; x++){
            if(img_out.at<uchar>(y, x) == 255){
                cv::circle(img_mark, cv::Point(x, y), 4, cv::Scalar(255, 0, 0), 2, 8, 0);  // 中心点是x,y
            }
        }
    }    
    cv::imwrite("harris_no_mark.jpg", img_mark); 

    return 0;
}
void moravec_nowindow(cv::Mat  &src, cv::Mat &dst, float threshold){
    dst.create(src.rows, src.cols, CV_8UC1);
    for(int y = 0; y < dst.rows; y++){
        for(int x = 0; x < dst.cols; x++){
            float val = 0.0f;
            for(int i= y - 1; i <= y + 1; i++){
                for(int j= x - 1; j <= x + 1; j++){
                    if ( 0 <= i && i < dst.rows && 0 <= j && j < dst.cols){
                         val += abs(src.at<uchar>(y,x) - src.at<uchar>(i,j)); 
                    }
                }
            }
            val /= 8.0f;
            // std::cout << dst.at<float>(y, x) << std::endl;
            dst.at<uchar>(y, x) = val <= threshold ? 0 : 255;

        }
    }
}
无滑动窗口角点检测

滑动窗口的角点检测(Moravec角点检测)

#include <iostream>
#include <cmath>
#include <climits>

#include <opencv2/opencv.hpp>
/*
    实现Moravec滑动窗口角点检测算法
*/
// 滑动窗口角点检测
void moravec(cv::Mat &src, cv::Mat &dst, int block_size, float threshold);

int main(int argc, char **argv){
    cv::Mat img = cv::imread("corner.jpg");
    cv::Mat img_src;
    cv::cvtColor(img, img_src, cv::COLOR_BGR2GRAY);  // OpenCV读取图像的颜色是BGR,灰度是8位图像
    // OpenCV标准函数
    cv::Mat img_out;
    // cv::cornerHarris(img_src, img_out, 5, 31, 0.04);
    // cv::imwrite("harris_cv.jpg", img_out);

    // 无窗口角点检测
    moravec(img_src, img_out, 5, 45.0f);
    cv::imwrite("harris_mo.jpg", img_out); 
    // mark角点到原图
    cv::Mat img_mark;
    img.copyTo(img_mark);
    for(int y = 0; y < img_out.rows; y++){
        for(int x = 0; x < img_out.cols; x++){
            if(img_out.at<uchar>(y, x) == 255){
                cv::circle(img_mark, cv::Point(x, y), 4, cv::Scalar(255, 0, 0), 2, 8, 0);  // 中心点是x,y
            }
        }
    }    
    cv::imwrite("harris_mo_mark.jpg", img_mark);  
    return 0;
}

void moravec(cv::Mat  &src, cv::Mat &dst, int block_size, float threshold){
    // 滑动窗口半径
    int r = block_size / 2;   
    dst.create(src.rows, src.cols, CV_8UC1);
    for(int y = 0; y < dst.rows; y++){
        for(int x = 0; x < dst.cols; x++){
            // 定义8个滑动方向的像素灰度差
            float  results[8];
            float min_result = LLONG_MAX;   // 记录最小值
            //  1,  0 >> 左滑
            results[0] = 0.0f;
            for(int i = -r; i <= r; i++){
                for(int j = -r; j <= r; j++){
                    float w1 = 0.0f, w2 = 0.0f;
                    if(0 <= y + i && y + i < src.rows && 0 <= x + j && x + j < src.cols){
                        w1 = src.at<uchar>(y + i, x + j);
                    }
                    if(0 <= y + i && y + i < src.rows && 0 <= x + j + 1 && x + j + 1 < src.cols){
                        w2 = src.at<uchar>(y + i, x + j + 1);
                    }
                    results[0] += pow(w1 - w2, 2);
                }
            }
            min_result = min_result < results[0] ? min_result : results[0];
            // -1,  0 >> 右滑
            results[1] = 0.0f;
            for(int i = -r; i <= r; i++){
                for(int j = -r; j <= r; j++){
                    float w1 = 0.0f, w2 = 0.0f;
                    if(0 <= y + i && y + i < src.rows && 0 <= x + j && x + j < src.cols){
                        w1 = src.at<uchar>(y + i, x + j);
                    }
                    if(0 <= y + i && y + i < src.rows && 0 <= x + j - 1 && x + j - 1 < src.cols){
                        w2 = src.at<uchar>(y + i, x + j - 1);
                    }
                    results[1] += pow(w1 - w2, 2);
                }
            }
            min_result = min_result < results[1] ? min_result : results[1];
            //  0,  1 >> 下滑
            results[2] = 0.0f;
            for(int i = -r; i <= r; i++){
                for(int j = -r; j <= r; j++){
                    float w1 = 0.0f, w2 = 0.0f;
                    if(0 <= y + i && y + i < src.rows && 0 <= x + j && x + j < src.cols){
                        w1 = src.at<uchar>(y + i, x + j);
                    }
                    if(0 <= y + i + 1 && y + i + 1 < src.rows && 0 <= x + j  && x + j  < src.cols){
                        w2 = src.at<uchar>(y + i + 1, x + j);
                    }
                    results[2] += pow(w1 - w2, 2);
                }
            }
            min_result = min_result < results[2] ? min_result : results[2];
            //  0, -1 >> 上滑
            results[3] = 0.0f;
            for(int i = -r; i <= r; i++){
                for(int j = -r; j <= r; j++){
                    float w1 = 0.0f, w2 = 0.0f;
                    if(0 <= y + i && y + i < src.rows && 0 <= x + j && x + j < src.cols){
                        w1 = src.at<uchar>(y + i, x + j);
                    }
                    if(0 <= y + i - 1 && y + i - 1 < src.rows && 0 <= x + j  && x + j  < src.cols){
                        w2 = src.at<uchar>(y + i - 1, x + j);
                    }
                    results[3] += pow(w1 - w2, 2);
                }
            }
            min_result = min_result < results[3] ? min_result : results[3];
            // -1,-1 >> 左上
            results[4] = 0.0f;
            for(int i = -r; i <= r; i++){
                for(int j = -r; j <= r; j++){
                    float w1 = 0.0f, w2 = 0.0f;
                    if(0 <= y + i && y + i < src.rows && 0 <= x + j && x + j < src.cols){
                        w1 = src.at<uchar>(y + i, x + j);
                    }
                    if(0 <= y + i - 1 && y + i - 1 < src.rows && 0 <= x + j - 1  && x + j - 1 < src.cols){
                        w2 = src.at<uchar>(y + i - 1, x + j - 1);
                    }
                    results[4] += pow(w1 - w2, 2);
                }
            }
            min_result = min_result < results[4] ? min_result : results[4];
            //  1, -1 >> 右上
            results[5] = 0.0f;
            for(int i = -r; i <= r; i++){
                for(int j = -r; j <= r; j++){
                    float w1 = 0.0f, w2 = 0.0f;
                    if(0 <= y + i && y + i < src.rows && 0 <= x + j && x + j < src.cols){
                        w1 = src.at<uchar>(y + i, x + j);
                    }
                    if(0 <= y + i - 1 && y + i - 1 < src.rows && 0 <= x + j + 1  && x + j + 1 < src.cols){
                        w2 = src.at<uchar>(y + i - 1, x + j + 1);
                    }
                    results[5] += pow(w1 - w2, 2);
                }
            }
            min_result = min_result < results[5] ? min_result : results[5];
            //  1,  1 >> 右下
            results[6] = 0.0f;
            for(int i = -r; i <= r; i++){
                for(int j = -r; j <= r; j++){
                    float w1 = 0.0f, w2 = 0.0f;
                    if(0 <= y + i && y + i < src.rows && 0 <= x + j && x + j < src.cols){
                        w1 = src.at<uchar>(y + i, x + j);
                    }
                    if(0 <= y + i + 1 && y + i + 1 < src.rows && 0 <= x + j + 1  && x + j + 1 < src.cols){
                        w2 = src.at<uchar>(y + i + 1, x + j + 1);
                    }
                    results[6] += pow(w1 - w2, 2);
                }
            }
            min_result = min_result < results[6] ? min_result : results[6];
            // -1,  1 >> 左下
            results[7] = 0.0f;
            for(int i = -r; i <= r; i++){
                for(int j = -r; j <= r; j++){
                    float w1 = 0.0f, w2 = 0.0f;
                    if(0 <= y + i && y + i < src.rows && 0 <= x + j && x + j < src.cols){
                        w1 = src.at<uchar>(y + i, x + j);
                    }
                    if(0 <= y + i + 1 && y + i + 1 < src.rows && 0 <= x + j - 1  && x + j - 1 < src.cols){
                        w2 = src.at<uchar>(y + i + 1, x + j - 1);
                    }
                    results[7] += pow(w1 - w2, 2);
                }
            }
            min_result = min_result < results[7] ? min_result : results[7];
            min_result /= (255.0f * 9.0f);
            dst.at<uchar>(y, x) = min_result > threshold? 255 : 0;
            // std::cout << min_result << std::endl;
        }
    }
}
Moravec角点检测 检测不到的角点,估计因亮度有关

Harris角点的思路

  1. 在Moravec算法上,采用如下几个方法改进:

    1. 增加权重系数,增加中心点像素贡献;
      • 权重系数使用典型的高斯平滑系数。
    2. 把方向作为特征变量,解决Moravec算法的方向性与对边缘的敏感性。
      • 判定灰度在各个方向的变化情况。
    3. 引入自由参数k,控制噪音的敏感度。
  2. 改进后的Harris角点检测特点:

    1. 对于同一场景,即使视角发生变化,通常具备稳定性质的特征;
    2. 该点附近区域的像素点无论在梯度方向上还是其梯度幅值上有着较大变化;

Harris角点的数学模型

模型的数学表示

  1. 角点检测计算公式:某点(x_0,y_0)处角点检测计算值
    • R=det(M)-k(trace(M))^2
  1. R值的本质是特征值(特征值计算比较麻烦,所以使用矩阵的行列式与矩阵的迹):
    • 假设\lambda_1 , \lambda_2是二次型矩阵的特征值做,则: R=\lambda_1 \lambda_2 - k(\lambda_1 + \lambda_2)^2
      • det(M) = \lambda_1 \lambda_2
      • trace(M) = \lambda_1 + \lambda_2

模型推理

  1. 考虑使用滑动窗口的方向作为变量情况下灰度变化的直观表示:

    • E(u,v)=\sum\limits_{(x,y) \in W}w(x,y)[I(x+u,y+v)-I(x,y)]^2
    • 问题转换:
      1. 不再考虑灰度值的具体计算,而是考虑灰度的变化状态(方向)与变化的剧烈程度。
        • 灰度变化的程度(变化剧烈程度)实际上与角点的判定无关,只需要使用阈值二值化为变化与无变化即可。
      2. 可以使用具体的灰度值差来判定灰度的变化,但是灰度的变化不一定要通过具体的灰度值的差来判定。(差分的协方差:灰度变化的分散状态)
  2. 再看角点的判定(从灰度值的变化值的判定到灰度变化的方向状态变化)

    1. 边缘:像素灰度值沿一个方向变化。(Morvec算法因为方向性,45°边缘容易判定为角点)
    2. 平面:像素灰度值沿所有方向都没有变化。
    3. 角点/噪音点:像素灰度值在两个方向都变化(不是x与y,而是某个方向与垂直方向,这个方向u,v使用上式E(u,v)来判定)
      • 噪音点与角点的区别通过自由参数确定(后面详细说明这个自由参数的设置)
平面区域:任何方向都无灰度变化 任意方向灰度都有变化 某一个方向上存在灰度变化
  1. 使用差分替代像素差(还记得Taylor泰勒展式不?)

    • I(x+u,y+v)≈ I(x,y)+uI_x(x,y)+vI_y(x,y)
  2. 灰度变化的差分表示:

    • E(u,v) ≈ \sum \limits_{(x,y) \in W}w(x,y)[I(x,y)+uI_x+vI_y-I(x,y)]^2
  3. 差分灰度表示的化简:

    • E(u,v) ≈ \sum\limits_{(x,y) \in W}w(x,y)[u^2I_x^2+2uvI_xI_y+v^2I_y^2]
  4. 差分灰度的矩阵表示:

    • E(u,v) ≈ \sum\limits_{(x,y) \in W } w(x,y) \begin{bmatrix}u & v \end{bmatrix}\begin{bmatrix} I_x^2 & I_xIy \\ I_xI_y & I_y^2\end{bmatrix}\begin{bmatrix}u \\ v\end{bmatrix}

    • E(u,v) ≈ \begin{bmatrix}u & v\end{bmatrix}(\sum\limits_{(x,y)\in W}w(x,y)\begin{bmatrix} I_x^2 & I_xIy \\ I_xI_y & I_y^2\end{bmatrix})\begin{bmatrix}u \\ v\end{bmatrix}

  5. E(u,v)深度分析:

    1. E(u,v)的核心是关于差分I_x,I_y的正定二次型矩阵:

      • M=\sum\limits_{(x,y) \in W}w(x,y)\begin{bmatrix} I_x^2 & I_xIy \\ I_xI_y & I_y^2\end{bmatrix}=\begin{bmatrix} A & C \\ C & B \end{bmatrix}
    2. 再回忆下高中的解析几何:

      • E(u,v)就是(u,v)的椭圆(二次曲线)解析式。
  6. 从正定二次型可以获取的信息

    1. M正定性矩阵是2 \times 2
    2. M正定型有两个特征值\lambda_1, \lambda_2
    3. M正定型的两个特征值的平方根的倒数就是E(u,v)椭圆的长轴与短轴。
      • \frac{1}{\sqrt{\lambda_1}}, \frac{1}{\sqrt{\lambda_2}}= 长短轴
  1. 从正定二次型矩阵看像素差分(正定二次型是差分的正定二次型矩阵),按照PCA的思路,则\lambda_1, \lambda_2就是差分的分布(分散)状态。
    1. 细长椭圆 = 边缘 = \lambda_1 >> \lambda_2 或者 \lambda_1 << \lambda_2
    2. 接近圆点 = 角点 = \lambda_1 \approx \lambda_2
    3. 大圆 = 平面 = \lambda_1 \approx \lambda_2 \approx 0
角点判定的示意图
  1. 数学的魅力与特征值求解
    • 因为特征值求解运算量大,所以Harris想出另外一种表达式来度量特征值的判定:R = \lambda_1 \lambda_2 - k (\lambda_1 + \lambda_2) ^2
      1. 巧妙的解决了特征值求解问题,转换为求矩阵的行列式与迹。
        • numpy等常见的库中都提供线性代数的实现功能:行列与迹的计算。numpydet函数与numpy.trace函数
      2. 把特征值的比较转换为对R的大小判定(R非灰度值变化)。
      3. 通过自由参数,过滤噪音点(影响角点数目)
R的判定与K值的影响
  1. R表达式的深度解析
%matplotlib inline
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import axes3d
import numpy as np
import matplotlib.animation as ani

# 1. 创建图(绘制环境)
figure = plt.figure('3D图形', figsize=(8, 6))

# 2. 创建3D坐标系(直接创建,使用Figure中的函数创建:这里使用函数)
ax = figure.add_axes([0.1,0.1,0.8,0.8], projection='3d')

k = 0.5
# 绘制R函数的曲面
x, y=np.mgrid[0:5:200j,0:5:200j]
z = x * y - k * (x + y) ** 2
ax.plot_surface(x, y, z, label='3D曲面', cmap=plt.cm.get_cmap('cool') )

ax.grid(b=True)   # 网格线

plt.show()
R的3D可视化
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import axes3d
import numpy as np
import matplotlib.animation as ani
from IPython.display import display, Image,clear_output

figure = plt.figure('3D图形', figsize=(8, 6))
ax = figure.add_axes([0.1,0.1,0.8,0.8], projection='3d')

frames = 100    # 动画总的帧数

def ani_frame(frame):
    k =  frame / 100  # 0.01 - 1.0
    x, y=np.mgrid[0:5:200j,0:5:200j]
    z = x * y - k * (x + y) ** 2
    ax.clear()
    ax.set_xlim3d(0, 6)
    ax.set_ylim3d(0, 6)
    ax.set_zlim3d(-100, 25)

    ax.set_xbound(0,6)
    ax.set_ybound(0,6)
    ax.set_zbound(-100,25)
    surface = ax.plot_surface(x, y, z, label='3D曲面', cmap=plt.cm.get_cmap('cool'))
    txt = ax.text(5, 5, 20, F'k={k:4.2f}', color=(1, 0, 0, 1))
    return [surface, txt]

anim = ani.FuncAnimation(figure, ani_frame, frames=frames, interval=20,  blit=False, repeat=True)
# anim.save('sin.html', writer='html')
anim.save('R2.gif', writer='pillow')
ax.grid(b=True)   # 网格线
figure.clear()
clear_output()
<Figure size 576x432 with 0 Axes>
from IPython.display import display, Image
display(Image(filename='R2.gif'))
简书不支持gif文件,大家自己运行python代码看效果

角点的实现

实现步骤

  1. 计算梯度

    • 梯度的计算调用OpenCV的梯度计算方法。
      1. I_x=\dfrac{\partial I}{\partial x}
      2. I_y=\dfrac{\partial I}{\partial y}
  2. 计算二次型矩阵M的个元素

    • M ^ \prime =\begin{bmatrix} I_x^2 & I_xI_y \\ I_xI_y & I_y^2\end{bmatrix}
      1. I_x^2
      2. I_xI_y
      3. I_y^2
  3. 对上面三项进行高斯模糊运算

    • M=\sum\limits_{(x,y)\in W}w(x,y)\begin{bmatrix} I_x^2 & I_xI_y \\ I_xI_y & I_y^2\end{bmatrix}
      1. A = \sum\limits_{(x,y) \in W}I_x^2*w(x,y)
      2. B = \sum\limits_{(x,y) \in W}I_y^2*w(x,y)
      3. C = \sum\limits_{(x,y) \in W}I_xI_y*w(x,y)
  4. 构造正定二次型矩阵

    • M=\begin{bmatrix} A& C \\C & B\end{bmatrix}
  5. 计算正定型二次矩阵的行列式与迹

    1. det(M)
    2. trace(M)
  6. 计算Harris角点判定值R(Harris响应值)

    • R=det(M)-k(trace(M))^2
  7. 使用阈值过滤角点

    • R=\{R:det(M)-k(trace(M))^2 > t \}
    • 注意:
      • 建议阈值选择不要使用绝对值,而是使用所有阈值中最大值的相对值比较合理,比如阈值= 最大值 \times 0.01
  8. 可选的实现

    • 局部非最大值抑制: 在指定局部窗口类,如果R不是最大,则抑制为非角点。
    • 目的:稀疏下角点,角点堆积区,只需要代表角点即可。
    • 提示:也可以不抑制。

实现代码

#include <iostream>
#include <cmath>
#include <climits>

#include <opencv2/opencv.hpp>
/*
    实现Harris滑动窗口角点检测算法
*/
// 滑动窗口角点检测
void harris(cv::Mat &src, cv::Mat &dst, int block_size, int ksize, float k, float threshold);

int main(int argc, char **argv){
    cv::Mat img = cv::imread("corner.jpg");
    cv::Mat img_src;
    cv::cvtColor(img, img_src, cv::COLOR_BGR2GRAY);  // OpenCV读取图像的颜色是BGR,灰度是8位图像
    // OpenCV标准函数 ------------------------------------------------------------------------
    cv::Mat img_out, img_harris;
    cv::cornerHarris(img_src, img_out, 5, 11, 0.04);
    cv::imwrite("harris_cv.jpg", img_out);

    // 角点检测-------------------------------------------------------------------------------
    harris(img_src, img_harris, 5, 11, 0.04f, 45.0f);
    cv::imwrite("harris_mo.jpg", img_harris); 
    return 0;
}

void harris(cv::Mat &src, cv::Mat &dst, int block_size, int ksize, float k, float threshold){
    // Sobel算子的缩放大小
    float scale = ksize * ksize * 2.0f;               // 2 ^ (ksize -1)   
    scale *= block_size;             
    scale *= 255.0f;                  // CV_8U才处理,一般像素使用浮点数表示不使用(浮点数表示图像都是0-1.0之间)
    scale = 1.0 / scale; 

    // 拷贝数据到GPU
    cv::cuda::GpuMat  g_src(src);
    // cv::cuda::GpuMat  g_src;
    // g_temp.convertTo(g_src, CV_32FC1);
    // 创建运算需要的Sobel核(导数核)
    cv::Ptr<cv::cuda::Filter> sobel_x = cv::cuda::createSobelFilter(CV_8UC1, CV_32FC1, 1, 0, ksize, scale);
    cv::Ptr<cv::cuda::Filter> sobel_y = cv::cuda::createSobelFilter(CV_8UC1, CV_32FC1, 0, 1, ksize, scale);
    // 创建运算需要的高斯kernel
    cv::Ptr<cv::cuda::Filter> gauss = cv::cuda::createGaussianFilter(CV_32FC1, CV_32FC1, cv::Size(block_size, block_size), 2);
    // 1. 计算Sobel差分
    cv::cuda::GpuMat I_x, I_y;
    sobel_x->apply(g_src, I_x);
    sobel_y->apply(g_src, I_y);
    // 2. 计算差分的乘积(Hadamard积)
    cv::cuda::GpuMat I_xx, I_xy, I_yy;
    cv::cuda::multiply(I_x, I_x, I_xx);
    cv::cuda::multiply(I_x, I_y, I_xy);
    cv::cuda::multiply(I_y, I_y, I_yy);
    // 3. 对差分乘积做高斯加权(高斯模糊)
    cv::cuda::GpuMat g_A, g_B, g_C;
    gauss->apply(I_xx, g_A);
    gauss->apply(I_yy, g_B);
    gauss->apply(I_xy, g_C);
    // ----------------------- 下面回到CPU时代
    cv::Mat A, B, C;
    g_A.download(A);
    g_B.download(B);
    g_C.download(C);
    dst.create(src.rows, src.cols, CV_32FC1);

    for(int y = 0; y < dst.rows; y++){
        for(int x = 0; x < dst.cols; x++){
            // 4. 为每个像素构造M正定二次型矩阵
            cv::Mat M = (cv::Mat_<float>(2, 2) << A.at<float>(y, x), C.at<float>(y, x), C.at<float>(y, x), B.at<float>(y, x)); 
            // 5. 计算每个像素对应的正定二次型的行列式与迹
            float det = cv::determinant(M);
            float trace = cv::trace(M)[0]; 
            // 6. 计算每个像素对应的Harris响应值R
            float R = det - k * trace * trace;
            // 7. 使用阈值做角点过滤(可以增加局部非最大抑制)
            dst.at<float>(y, x) = R;
        }
    }
}

实现效果对比

- 前面的我们自己的代码。后面一个是OpenCV的cornerHarris函数的效果。
- 目测上面代码中的scale在OpenCV中有固定的计算公式,与block_size与ksize有关,上面我们代码没有找到这个计算公式,使用经验并从防止数据爆炸的角度自己定义了一个公式。
    double scale = (double)(1 << ((ksize > 0 ? ksize : 3) - 1)) * blockSize;
    if (ksize < 0)
        scale *= 2.0;
    if (depth == CV_8U)
        scale *= 255.0;
    scale = std::pow(scale, -4.0);

    double scale = (double)(1 << ((aperture_size > 0 ? aperture_size : 3) - 1)) * block_size;
    if (aperture_size < 0)
        scale *= 2.0;
    if (depth == CV_8U)
        scale *= 255.0;
    scale = 1.0 / scale;

opencv的实现效果与我们的手工实现效果的对比

关于K的取值

角点的性质与改进

  1. 根据上面算法一个明显的改进是图像旋转后,角点依然可以检测。

    • 因为检测的依据不是灰度变化的值,而是灰度的变化的方向的分散程度。
  2. 图像的亮度对检测没有影响

    • 因为检测的像素的灰度是否变化,而不是变化值的大小。
  3. 问题:

    • 图像的缩放会影响角点的检测,因为窗体不变的情况下,有的边缘可能被检测为角点
Harris算法不具备尺度不变性

附录

角点检测与参数影响的代码

  1. 程序文件结构

    • 主文件
      • main.cpp
    • 窗体界面文件
      • dlgharris.h
      • dlgharris.cpp
    • UI文件
      • harris.ui
    • 工程脚本文件
      • main.pro
  2. 工程脚本文件

# -------------编译时选项设置
QMAKE_CXXFLAGS += /source-charset:utf-8
QMAKE_CXXFLAGS += /execution-charset:utf-8

# -------------QT的配置
TEMPLATE        = app

CONFIG         += debug
CONFIG         += console
CONFIG         += thread
CONFIG         += qt

QT             += core
QT             += gui
QT             += widgets

# -------------OpenCV的配置
INCLUDEPATH    += "C:\opencv\install\include"

LIBS           += -L"C:\opencv\install\x64\vc16\lib" 
LIBS           += -lopencv_core420d 
LIBS           += -lopencv_imgcodecs420d 
LIBS           += -lopencv_highgui420d 
LIBS           += -lopencv_imgproc420d
LIBS           += -lopencv_cudafilters420d
LIBS           += -lopencv_cudaimgproc420d
LIBS           += -lopencv_cudafeatures2d420d
LIBS           += -lopencv_cudaobjdetect420d
LIBS           += -lopencv_videoio420d

# -------------源代码,头文件,ui文件
SOURCES        += main.cpp
SOURCES        += dlgharris.cpp

HEADERS        += dlgharris.h

FORMS          += harris.ui

# -------------编译的最终执行文件
TARGET          = main

  1. 主文件
#include <opencv2/opencv.hpp>
#include <QtWidgets/QApplication>
#include "dlgharris.h"

int main(int argc, char *argv[]){

    QApplication  app(argc, argv);
    DlgHarris dlg;
    dlg.show();
    return app.exec();
}


  1. 窗体界面文件
#ifndef DIALOG_HARRIS_H
#define DIALOG_HARRIS_H
#include <opencv2/opencv.hpp>
#include <QtWidgets/QDialog>
#include "ui_harris.h"

class DlgHarris : public QDialog{
    Q_OBJECT
private:
    Ui::Harris  *ui;
    cv::Mat img_src;
    cv::Mat img_harris;
    // Harris角点计算需要的三个参数
    int n_blicksize;
    int n_kernelsize;
    double k;

    void show_img(cv::Mat);

public:
    DlgHarris(QWidget *parent = nullptr, Qt::WindowFlags f = Qt::WindowFlags());
    virtual ~DlgHarris();

public slots:
    void block_size(int);
    void ksize(int);   
    void param_k(int); 
};
#endif


#include "dlgharris.h"
#include <iostream>

DlgHarris::DlgHarris(QWidget *parent, Qt::WindowFlags f):
    ui(new Ui::Harris()),
    QDialog(parent, f){
    ui->setupUi(this);
    // 加载图像
    img_src = cv::imread("corner2.jpg");
    
    if(img_src.channels() == 3){  // 三通道颜色就直接转换位灰度图像
        std::cout << "图像通道:" <<  img_src.channels() << std::endl;
        cv::cvtColor(img_src, img_src, cv::COLOR_BGR2GRAY);
    }
    // 初始化参数
    n_blicksize = ui->slider_blocksize->value();
    n_kernelsize = ui->slider_kernelsize->value() * 2 + 1;
    k = ui->slider_k->value() / 1000.0;
    // Harris角点计算
    cv::cornerHarris(img_src, img_harris, n_blicksize, n_kernelsize, k);
    // 显示图像
    show_img(img_harris);
}
DlgHarris::~DlgHarris(){
    delete ui;
}

void DlgHarris::show_img(cv::Mat img){
    img.convertTo(img, CV_8U);  // 转换格式
    normalize(img, img, 0, 256, cv::NORM_MINMAX);  // 归一化
    cv::imwrite("harris_out.jpg", img);
    QImage q_img(img.data, img.cols, img.rows, QImage::Format_Grayscale8);
    // 转换为组件能处理的像素格式
    QPixmap p_img = QPixmap::fromImage(q_img);
    // 显示图像
    ui->lbl_show->setPixmap(p_img);
    // 缩放图像适应组件大小
    ui->lbl_show->setScaledContents(true);    

}
void DlgHarris::block_size(int){
    n_blicksize = ui->slider_blocksize->value();
    std::cout << "窗体大小:" << n_blicksize << std::endl;
    cv::cornerHarris(img_src, img_harris, n_blicksize, n_kernelsize, k);
    show_img(img_harris);

}
void DlgHarris::ksize(int){
    n_kernelsize = ui->slider_kernelsize->value() * 2 + 1;
    std::cout << "核大小:" << n_kernelsize << std::endl;
    cv::cornerHarris(img_src, img_harris, n_blicksize, n_kernelsize, k);
    show_img(img_harris);
}
void DlgHarris::param_k(int){
    k = ui->slider_k->value() / 1000.0;
    std::cout << "自由因子:" << k << std::endl;
    cv::cornerHarris(img_src, img_harris, n_blicksize, n_kernelsize, k);
    show_img(img_harris);
}

  1. UI文件
    • 三个slots函数:
      • block_size(int)
      • ksize(int)
      • param_k(int)
    • 三个signals链接:
      • 对应三个滑动条的信号链接。
UI文件的设计界面
上一篇下一篇

猜你喜欢

热点阅读