【图像预处理】 Harris角点检测器原理及C++实现

在图像中,点的位置很多时候是很有用的。我们经常会在图像处理的过程中提取我们的兴趣点,而角点是很好的兴趣点。在解决许多实际问题时,使用角点特征的效果优于线(或者边缘)。这是因为观察一条移动直线时,只有其垂直于线段的运动分量能被观测到,而语线段共线的分量则不可见。角点则不同,他们提供唯一匹配。
Harris角点检测器是一种比较常用的检测器,它对二维平移和旋转、少量光照变化和视角变化具有鲁棒性,并且计算量很小。另外,该算法通过构造矩阵,利用矩阵的特征值来描述图像局部信息的思想是值得学习的。

原理

Harris角点检测考虑了灰度值平方差的和的差分。从一幅二维图像f中,选取一个图像块这里写图片描述, 平且平移这里写图片描述。则图像块W内,灰度值与其平移后的图像之差的平方和S为: 这里写图片描述 (1)
将平移后图像灰度值用一阶泰勒展开近似,有:这里写图片描述 (2),
此时,(1)式有解析解,将(2)式代入(1)式并化简,有以下公式:
【图像预处理】 Harris角点检测器原理及C++实现_第1张图片 (3)
式中AW矩阵表示图像区域W在点(x,y)处的二阶导数的一半。记A为两倍AW,称为Harris矩阵代表了图像的局部结构。
这里写图片描述 (4)

通常会使用一个各向同性的窗用A并用,常用高斯窗。由于高斯算子是线性算子,故可先对图像进行高斯平滑后,在计算A。A是一个半正定对称矩阵,其主要变换模式对应正交方向的偏微分,并由其两个特征值反映。有以下三种不同的情形:1. 两个特征值都很小,意味着水平与竖直方向的变换都很小,中心像素处于平坦区域,没有角点;2. 一个特征值很大,另一个很小,意味着图像位于边缘,垂直于边缘的运动方向,f的变化很大,而沿着边缘的方向变化很小。也认为不是角点;3.两个特征值都很大,意味着水平竖直方向的变化率都很快,任何方向稍有运动图像都会发生显著的变化,此时认为是角点。


算法实现过程

Harris提出了一种不用计算矩阵的特征值而可以得到局部特征信息的方法。通过构造一个响应函数:这里写图片描述 (5)
式中,kappa是可调参数,建议值在0.04~0.15。


  1. 对输入图像进行高斯滤波
  2. 遍历图像,计算每个像素的水平,竖直方向梯度的大小。使用近似于导数的差分核做两次卷积即可。
  3. 对每一像素设置一定半径的邻域,计算局部特征矩阵A(式4)以及响应函数(式5)。
  4. 为响应函数选取一个阈值,为了选择最佳的候选角点,使用非极大抑制方法。

代码C++

/////////////////////////////////
//输入图像数组 BYTE* m_pBitmap;
//宽×高:m_dwHeight * m_dwWidth;
//位深:8位灰度图像
////////////////////////////////

实现过程中用到的函数:
高斯滤波

void ImgGaussSmooth(BYTE* pBitmap, int dwHeight, int dwWidth, float seta)
{
    double pi = 3.1415926;
    int i, j, m, n;
    int nHWndSize = (int)(3.0 * sqrt(2.0) * seta + 0.5) - 1;        // 模板半宽 
    int nWndSize  = (nHWndSize << 1) + 1;                       // 模板宽

    if( nHWndSize < 1 ) return;


    double * pFilter = new double [nWndSize];
    double dSum = 0;

    // set filter 
    for (i = 0; i < nHWndSize + 1; ++i)
    {
        pFilter[nHWndSize + i] = pFilter[nHWndSize - i] = exp(- i * i / (2.0 * seta * seta)) / (sqrt(2 * pi) * seta);
    }
    for (i = 0; i < nWndSize; ++i)
    {
        dSum += pFilter[i];
    }
    for (i = 0; i < nWndSize; ++i)
    {
        pFilter[i] /= dSum;
    }


    double* pResult = new double[dwHeight*dwWidth];

    /* 两次卷积 */
    for (i = 0; i < dwHeight; ++i)
    for (j = 0; j < dwWidth;  ++j)
    {
        dSum = 0;
        for( m = -nHWndSize; m <= nHWndSize; m++ )
        {
            n = m + j;
            if( n < 0         ) n = -n; 
            if( n > dwWidth-1 ) n = (dwWidth-1)*2-n;
            dSum += pBitmap[i*dwWidth+ n] * pFilter[m+nHWndSize];
        }
        pResult[i*dwWidth+j] = dSum;
    }
    for (i = 0; i < dwHeight; ++i)
    for (j = 0; j < dwWidth;  ++j)
    {
        dSum = 0;
        for( m = -nHWndSize; m <= nHWndSize; m++ )
        {
            n = m + i;
            if( n < 0         ) n = -n; 
            if( n > dwHeight-1 ) n = (dwHeight-1)*2 - n;
            dSum += pResult[n*dwWidth+ j] * pFilter[m+nHWndSize];
        }
        m = dSum+0.5;

        if( m > 255 ) 
            pBitmap[i*dwWidth+j] = 255;
        else if( m< 0 ) 
            pBitmap[i*dwWidth+j] = 0;
        else 
            pBitmap[i*dwWidth+j] = m;
    }

    if( pResult ) delete pResult;
    if( pFilter ) delete pFilter;
    return;
}

Sobel求方向导数
使用的Sobel算子计算一阶方向导数,这样做不一定效果就好,可以使用更加简单的差分算子

void GetSobelDerivative(BYTE* pImgSrc, DWORD dwHeight, DWORD dwWidth, double*& pGradX, double*& pGradY)
{
    if (pGradX) delete[] pGradX;
    if (pGradY) delete[] pGradY;
    pGradX = new double[dwHeight * dwWidth];
    pGradY = new double[dwHeight * dwWidth];

    //mask
    int nMaskX[9] = { -1, 0, 1, -2, 0, 2, -1, 0, 1 };
    int nMaskY[9] = { -1, -2, -1, 0, 0, 0, 1, 2, 1 };

    int i, j, m, n;
    double dTmpX, dTmpY;
    int nCol, nRow;
    for (i = 0; i < dwHeight; i++)
    for (j = 0; j < dwWidth; j++)
    {
        dTmpX = dTmpY = 0;
        for (m = -1; m <= 1; m++)
        for (n = -1; n <= 1; n++)
        {
            //判断越界
            nCol = j + n < dwWidth ? j + n : dwWidth - 1;
            nRow = i + m < dwHeight ? i + m : dwHeight - 1;
            nCol = nCol > 0 ? nCol : 0;
            nRow = nRow > 0 ? nRow : 0;

            dTmpX += nMaskX[(m + 1) * 3 + n + 1] * pImgSrc[nRow * dwWidth + nCol];
            dTmpY += nMaskY[(m + 1) * 3 + n + 1] * pImgSrc[nRow * dwWidth + nCol];
        }

        pGradX[i*dwWidth + j] = dTmpX;
        pGradY[i*dwWidth + j] = dTmpY;
    }
}

主干代码如下,没有封装成函数。小生使用的MFC的框架,读者可使用opencv进行图像读取和显示。

    //高斯滤波
    dib.ImgGaussSmooth(m_pBitmap, m_dwHeight, m_dwWidth, 0.7);

    //计算梯度
    int i, j, m, n;
    double* pdGradX = NULL;
    double* pdGradY = NULL;

    //求方向导数,使用Sobel算子
    GetSobelDerivative(m_pBitmap, m_dwHeight, m_dwWidth, pdGradX, pdGradY);

    const int& nR = 1;                                              //给定邻域窗口半径
    const double& dKappa = 0.04;                                    //建议值0.04~0.15

    double* ppdMatrix[4] = { NULL };                                //Harris矩阵
    for (i = 0; i < 4; i++)
    {
        ppdMatrix[i] = new double[m_dwWidth * m_dwHeight];
        memset(ppdMatrix[i], 0, sizeof(double) * m_dwWidth * m_dwHeight);
    }
    double* pdRespond = new double[m_dwHeight * m_dwWidth];         //响应函数
    memset(pdRespond, 0, sizeof(double) * m_dwWidth * m_dwHeight);

    for (i = nR; i < m_dwHeight - nR; i++)
    for (j = nR; j < m_dwWidth - nR; j++)
    {   
        for (m = -nR; m <= nR; m++)
        for (n = -nR; n <= nR; n++)
        {
            ppdMatrix[0][i * m_dwWidth + j] += powf(pdGradX[(i + m) *      m_dwWidth + j + n], 2);
            ppdMatrix[1][i * m_dwWidth + j] += pdGradX[(i + m) * m_dwWidth + j + n] * pdGradY[(i + m) * m_dwWidth + j + n];
            ppdMatrix[2][i * m_dwWidth + j] = ppdMatrix[1][i * m_dwWidth + j];
            ppdMatrix[3][i * m_dwWidth + j] += powf(pdGradY[(i + m) * m_dwWidth + j + n], 2);
        }

        for (n = 0; n < 4; n++)
            ppdMatrix[n][i * m_dwWidth + j] *= 2;
    }

    //响应函数
    double dDet;
    BYTE* pShow = NULL;
    for (i = 0; i < m_dwHeight * m_dwWidth; i++)
    {
        dDet = ppdMatrix[0][i]* ppdMatrix[3][i] - ppdMatrix[1][i] * ppdMatrix[2][i];
        pdRespond[i] = dDet - dKappa * powf(ppdMatrix[0][i] + ppdMatrix[3][i], 2);
    }


    //非极大值抑制
    double dMax, dThreshold;
    int nSearchR = 2;
    int nCol, nRow;
    dThreshold = 0;
    for (i = 0; i < m_dwHeight * m_dwWidth; i++)
    {
        if (pdRespond[i] > dThreshold)
            dThreshold = pdRespond[i];
    }
    dThreshold *= 0.01; //阈值

    for (i = 0; i < m_dwHeight; i++)
    for (j = 0; j < m_dwWidth; j++)
    {
        dMax = 0;
        for (m = -nSearchR; m <= nSearchR; m++)
        for (n = -nSearchR; n <= nSearchR; n++)
        {
            //越界判断
            nRow = i + m < m_dwHeight ?  i + m : m_dwHeight - 1;
            nRow = nRow > 0 ? nRow : 0;
            nCol = j + n < m_dwWidth ? j + n : m_dwWidth - 1;
            nCol = nCol > 0 ? nCol: 0;

            if (dMax < pdRespond[nRow * m_dwWidth + nCol])
                dMax = pdRespond[nRow * m_dwWidth + nCol];
        }

        if (dMax > dThreshold && pdRespond[i * m_dwWidth + j] == dMax)
            pdRespond[i * m_dwWidth + j] = 1;
        else
            pdRespond[i * m_dwWidth + j] = 0;
    }

    //绘制
    dib.Translate8To24Color(m_pBitmap, m_dwHeight, m_dwWidth, pShow);   
    for (i = 3; i < m_dwHeight - 3; i++)
    for (j = 3; j < m_dwWidth - 3; j++)
    {
        if (pdRespond[i * m_dwWidth + j] == 1)
        {
            for (m = -3; m <= 3; m++)
            {
                pShow[((i + m) * m_dwWidth + j) * 3 + 2] = 255;
                pShow[((i + m) * m_dwWidth + j) * 3 + 1] = 0;
                pShow[((i + m) * m_dwWidth + j) * 3 + 0] = 0;
            }
            for (n = -3; n <= 3; n++)
            {
                pShow[(i * m_dwWidth + j + n) * 3 + 2] = 255;
                pShow[(i * m_dwWidth + j + n) * 3 + 1] = 0;
                pShow[(i * m_dwWidth + j + n) * 3 + 0] = 0;
            }
        }
    }
    AddInterResultImage(pShow, m_dwHeight, m_dwWidth, 24, "");   //显示图像

    Invalidate(FALSE);
    UpdateWindow();

    //释放
    delete[] pdGradX;
    delete[] pdGradY;
    delete[] pdRespond;
    delete[] pShow;
    pdGradX = NULL;
    pdGradY = NULL;
    pShow = NULL;
    pdRespond = NULL;

    for (i = 0; i < 4; i++)
    {
        delete[] ppdMatrix[i];
        ppdMatrix[i] = NULL;
    }

实现结果

参数如以上代码:计算梯度使用的是Sobel算子;kappa选取的是0.04;邻域半径为1的8邻域;响应函数结果的阈值选取的是图像所以像素点响应函数最大值的0.01倍;非极大值抑制选取的领域为半径为2的8邻域。
以下图像的顺序分别是原图,归一化到显示范围的各个像素响应函数值的图像,以及原图中标记出找到的Harris角点。
【图像预处理】 Harris角点检测器原理及C++实现_第2张图片
【图像预处理】 Harris角点检测器原理及C++实现_第3张图片
【图像预处理】 Harris角点检测器原理及C++实现_第4张图片


【图像预处理】 Harris角点检测器原理及C++实现_第5张图片
【图像预处理】 Harris角点检测器原理及C++实现_第6张图片
【图像预处理】 Harris角点检测器原理及C++实现_第7张图片


互相学习,欢迎指正。

你可能感兴趣的:(图像处理与机器视觉)