在图像中,点的位置很多时候是很有用的。我们经常会在图像处理的过程中提取我们的兴趣点,而角点是很好的兴趣点。在解决许多实际问题时,使用角点特征的效果优于线(或者边缘)。这是因为观察一条移动直线时,只有其垂直于线段的运动分量能被观测到,而语线段共线的分量则不可见。角点则不同,他们提供唯一匹配。
Harris角点检测器是一种比较常用的检测器,它对二维平移和旋转、少量光照变化和视角变化具有鲁棒性,并且计算量很小。另外,该算法通过构造矩阵,利用矩阵的特征值来描述图像局部信息的思想是值得学习的。
Harris角点检测考虑了灰度值平方差的和的差分。从一幅二维图像f中,选取一个图像块, 平且平移。则图像块W内,灰度值与其平移后的图像之差的平方和S为: (1)
将平移后图像灰度值用一阶泰勒展开近似,有: (2),
此时,(1)式有解析解,将(2)式代入(1)式并化简,有以下公式:
(3)
式中AW矩阵表示图像区域W在点(x,y)处的二阶导数的一半。记A为两倍AW,称为Harris矩阵代表了图像的局部结构。
(4)
通常会使用一个各向同性的窗用A并用,常用高斯窗。由于高斯算子是线性算子,故可先对图像进行高斯平滑后,在计算A。A是一个半正定对称矩阵,其主要变换模式对应正交方向的偏微分,并由其两个特征值反映。有以下三种不同的情形:1. 两个特征值都很小,意味着水平与竖直方向的变换都很小,中心像素处于平坦区域,没有角点;2. 一个特征值很大,另一个很小,意味着图像位于边缘,垂直于边缘的运动方向,f的变化很大,而沿着边缘的方向变化很小。也认为不是角点;3.两个特征值都很大,意味着水平竖直方向的变化率都很快,任何方向稍有运动图像都会发生显著的变化,此时认为是角点。
Harris提出了一种不用计算矩阵的特征值而可以得到局部特征信息的方法。通过构造一个响应函数: (5)
式中,kappa是可调参数,建议值在0.04~0.15。
/////////////////////////////////
//输入图像数组 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角点。