获取源工程可访问gitee可在此工程的基础上进行学习。
A部分:
(3)使用VC++设计程序:对一幅256级灰度图像,实现基于拉普拉斯算子的边缘检测功能。
(4)使用VC++设计程序:对一幅256级灰度图像,实现Canny边缘检测方法。
B部分:
(1)包括A部分全部要求。
(2)使用VC++设计程序:对一幅256级灰度图像,实现Otsu分割方法。
边缘检测是图像处理中一种常见的操作,其目的是识别图像中物体或场景中的边缘部分。图像中的边缘通常是灰度、颜色或强度发生显著变化的地方,边缘检测旨在捕捉这些变化,并用于图像分析、计算机视觉和图像识别等领域。
边缘检测的概念和目的包括:
检测图像中物体的轮廓: 边缘通常表示不同区域之间的边界,因此边缘检测有助于定位图像中物体的轮廓。这对于对象识别和目标跟踪等任务至关重要。
提取图像中的特征: 边缘是图像中的重要特征之一。通过检测边缘,可以提取出物体的形状和结构信息,有助于进行更高层次的图像分析。
减少图像数据量: 边缘检测可以将图像中的详细信息转化为简化的边缘表示,从而降低图像的数据量。这对于图像压缩和存储是有益的。
改善图像分割: 边缘检测有助于图像分割,即将图像划分为不同的区域或对象。通过找到区域之间的边缘,可以更好地区分不同的物体。
增强图像特征: 在某些情况下,边缘检测可用于增强图像的局部特征。通过突出物体的轮廓,有助于人眼更容易理解和识别图像内容。
常见的边缘检测方法包括Sobel算子、Prewitt算子、Canny边缘检测等。这些方法通常基于图像梯度、灰度变化或滤波等技术,以找到图像中灰度或颜色变化较大的地方。
拉普拉斯算子(Laplacian operator)是一种常用于边缘检测的图像处理算子,它通过计算图像中像素值的二阶导数来寻找图像中的边缘。
拉普拉斯算子的原理基于以下思想:
灰度变化的二阶导数: 边缘通常对应于图像中像素值变化较大的区域。拉普拉斯算子对灰度变化的二阶导数进行检测,因为边缘处的像素值变化会导致灰度的急剧变化。
图像中的边缘位置: 在图像中,边缘位置对应于灰度函数的极值点。通过计算灰度函数的二阶导数,我们可以找到灰度函数的极值点,这些极值点通常表示边缘的位置。
数学上,拉普拉斯算子通常使用以下的离散形式表示:
∇ 2 I ( x , y ) = I ( x − 1 , y ) + I ( x + 1 , y ) + I ( x , y − 1 ) + I ( x , y + 1 ) − 4 I ( x , y ) \nabla^2 I(x, y) = I(x-1, y) + I(x+1, y) + I(x, y-1) + I(x, y+1) - 4I(x, y) ∇2I(x,y)=I(x−1,y)+I(x+1,y)+I(x,y−1)+I(x,y+1)−4I(x,y)
其中, ∇ 2 I ( x , y ) \nabla^2 I(x, y) ∇2I(x,y) 表示图像 I I I 在位置 ( x , y ) (x, y) (x,y) 处的拉普拉斯值。
在实际应用中,为了增强边缘的检测效果,通常会在计算完拉普拉斯值后,应用阈值处理或者进一步的图像增强操作。
值得注意的是,由于噪声和图像中其他变化可能影响到灰度的二阶导数,因此在应用拉普拉斯算子时,可能需要进行平滑或者使用其他预处理技术,以提高边缘检测的准确性。
/*************************************************************************
*
* \函数名称:
* LaplacianOperator()
*
* \输入参数:
* CDib * pDib - 指向CDib类的指针,含有原始图象信息
* double * pdGrad - 指向梯度数据的指针,含有图像的梯度信息
*
* \返回值:
* 无
*
* \说明:
* LaplacianOperator算子,是二阶算子,不想Roberts算子那样需要两个模板计算
* 梯度,LaplacianOperator算子只要一个算子就可以计算梯度。但是因为利用了
* 二阶信息,对噪声比较敏感
*
*************************************************************************
*/
void LaplacianOperator(CDib * pDib, double * pdGrad)
{
// 遍历图象的纵坐标
int y;
// 遍历图象的横坐标
int x;
// 图象的长宽大小
CSize sizeImage = pDib->GetDimensions();
int nWidth = sizeImage.cx ;
int nHeight = sizeImage.cy ;
// 图像在计算机在存储中的实际大小
CSize sizeImageSave = pDib->GetDibSaveDim();
// 图像在内存中每一行象素占用的实际空间
int nSaveWidth = sizeImageSave.cx;
// 图像数据的指针
LPBYTE lpImage = pDib->m_lpImage;
// 初始化
for(y=0; y<nHeight ; y++ )
for(x=0 ; x<nWidth ; x++ )
{
*(pdGrad+y*nWidth+x)=0;
}
// 设置模板系数
static int nWeight[3][3] ;
nWeight[0][0] = -1 ;
nWeight[0][1] = -1 ;
nWeight[0][2] = -1 ;
nWeight[1][0] = -1 ;
nWeight[1][1] = 8 ;
nWeight[1][2] = -1 ;
nWeight[2][0] = -1 ;
nWeight[2][1] = -1 ;
nWeight[2][2] = -1 ;
//这个变量用来表示Laplacian算子象素值
int nTmp[3][3];
// 临时变量
double dGrad;
// 模板循环控制变量
int yy ;
int xx ;
// 下面开始利用Laplacian算子进行计算,为了保证计算所需要的
// 的数据位于图像数据的内部,下面的两重循环的条件是
// y
// 而不是x
for(y=1; y<nHeight-2 ; y++ )
for(x=1 ; x<nWidth-2 ; x++ )
{
dGrad = 0 ;
// Laplacian算子需要的各点象素值
// 模板第一行
nTmp[0][0] = lpImage[(y-1)*nSaveWidth + x - 1 ] ;
nTmp[0][1] = lpImage[(y-1)*nSaveWidth + x ] ;
nTmp[0][2] = lpImage[(y-1)*nSaveWidth + x + 1 ] ;
// 模板第二行
nTmp[1][0] = lpImage[y*nSaveWidth + x - 1 ] ;
nTmp[1][1] = lpImage[y*nSaveWidth + x ] ;
nTmp[1][2] = lpImage[y*nSaveWidth + x + 1 ] ;
// 模板第三行
nTmp[2][0] = lpImage[(y+1)*nSaveWidth + x - 1 ] ;
nTmp[2][1] = lpImage[(y+1)*nSaveWidth + x ] ;
nTmp[2][2] = lpImage[(y+1)*nSaveWidth + x + 1 ] ;
// 计算梯度
for(yy=0; yy<3; yy++)
for(xx=0; xx<3; xx++)
{
dGrad += nTmp[yy][xx] * nWeight[yy][xx] ;
}
// 梯度值写入内存
*(pdGrad+y*nWidth+x)=dGrad;
}
}
Canny 边缘检测是一种经典的边缘检测算法,由John F. Canny 在 1986 年提出。它的设计旨在满足三个主要要求:
Canny 边缘检测的步骤包括:
噪声抑制: 使用高斯滤波器对图像进行平滑,以减少噪声对边缘检测的影响。
计算梯度: 使用 Sobel 等算子计算图像的梯度,得到每个像素点的梯度强度和方向。
非极大值抑制: 对梯度图进行非极大值抑制,以保留梯度方向上的局部极大值。
双阈值检测: 设置两个阈值,高阈值 T high T_{\text{high}} Thigh 和低阈值 T low T_{\text{low}} Tlow。将梯度图分为强边缘、弱边缘和非边缘三类。强边缘是梯度值大于 T high T_{\text{high}} Thigh 的点,非边缘是梯度值小于 T low T_{\text{low}} Tlow 的点,弱边缘是梯度值介于两者之间的点。
边缘跟踪: 通过连接强边缘像素,形成完整的边缘。通常使用连接弱边缘像素的方式,如果弱边缘像素与某个强边缘像素相邻,则被认为属于同一边缘。
这样,Canny 边缘检测能够在图像中找到细且明显的边缘,同时能够抑制噪声。
/*************************************************************************
*
* \函数名称:
* Canny()
*
* \输入参数:
* unsigned char *pUnchImage- 图象数据
* int nWidth - 图象数据宽度
* int nHeight - 图象数据高度
* double sigma - 高斯滤波的标准方差
* double dRatioLow - 低阈值和高阈值之间的比例
* double dRatioHigh - 高阈值占图象象素总数的比例
* unsigned char *pUnchEdge - canny算子计算后的分割图
*
* \返回值:
* 无
*
* \说明:
* canny分割算子,计算的结果保存在pUnchEdge中,逻辑1(255)表示该点为
* 边界点,逻辑0(0)表示该点为非边界点。该函数的参数sigma,dRatioLow
* dRatioHigh,是需要指定的。这些参数会影响分割后边界点数目的多少
*************************************************************************
*/
void Canny(unsigned char *pUnchImage, int nWidth, int nHeight, double sigma,
double dRatioLow, double dRatioHigh, unsigned char *pUnchEdge)
{
// 经过高斯滤波后的图象数据
unsigned char * pUnchSmooth ;
// 指向x方向导数的指针
int * pnGradX ;
// 指向y方向导数的指针
int * pnGradY ;
// 梯度的幅度
int * pnGradMag ;
pUnchSmooth = new unsigned char[nWidth*nHeight] ;
pnGradX = new int [nWidth*nHeight] ;
pnGradY = new int [nWidth*nHeight] ;
pnGradMag = new int [nWidth*nHeight] ;
// 对原图象进行滤波
GaussianSmooth(pUnchImage, nWidth, nHeight, sigma, pUnchSmooth) ;
// 计算方向导数
DirGrad(pUnchSmooth, nWidth, nHeight, pnGradX, pnGradY) ;
// 计算梯度的幅度
GradMagnitude(pnGradX, pnGradY, nWidth, nHeight, pnGradMag) ;
// 应用non-maximum 抑制
NonmaxSuppress(pnGradMag, pnGradX, pnGradY, nWidth, nHeight, pUnchEdge) ;
// 应用Hysteresis,找到所有的边界
Hysteresis(pnGradMag, nWidth, nHeight, dRatioLow, dRatioHigh, pUnchEdge);
// 释放内存
delete pnGradX ;
pnGradX = NULL ;
delete pnGradY ;
pnGradY = NULL ;
delete pnGradMag ;
pnGradMag = NULL ;
delete pUnchSmooth ;
pUnchSmooth = NULL ;
}
Otsu 分割方法是一种自适应阈值分割的算法,它由日本学者大津秀一在1979年提出,主要用于图像二值化。Otsu 方法的目标是找到一个全局阈值,将图像分为两个类别,使得两个类别之间的类内方差最小,即最大化两个类别之间的类间方差。
算法步骤如下:
直方图计算: 对图像进行灰度直方图统计,得到每个灰度级别的像素数目。
归一化直方图: 将直方图归一化,得到每个灰度级别的概率。
计算类间方差: 对于每个可能的阈值 t t t,将直方图分为两个部分:背景(小于等于阈值)和前景(大于阈值)。计算两个类别的类间方差:
σ 2 ( t ) = w 1 ( t ) ⋅ w 2 ( t ) ⋅ [ μ 1 ( t ) − μ 2 ( t ) ] 2 \sigma^2(t) = w_1(t) \cdot w_2(t) \cdot [ \mu_1(t) - \mu_2(t) ]^2 σ2(t)=w1(t)⋅w2(t)⋅[μ1(t)−μ2(t)]2
其中:
找到最大类间方差: 找到最大的类间方差对应的阈值 $$,即 t Otsu t_{\text{Otsu}} tOtsu。
图像二值化: 使用找到的阈值 t Otsu t_{\text{Otsu}} tOtsu 对图像进行二值化。
通过这种方法,Otsu 分割能够自适应地找到一个能够将图像背景和前景分离的阈值,适用于各种图像类型。
//获取图高
int height = pDoc->m_pDibInit->GetHeight();
//获取图宽
int width = pDoc->m_pDibInit->GetWidth();
int m,i;
//大津阈值分割
float avl = 0; //灰度平均值
int graysum = 0; //灰度值之和
int num[256] = { 0 }; //每个灰度级的频数
double p[256] = { 0 }; //~频率
for( i=0;i<height;i++)
for (int j = 0; j < width; j++)
{
int gray = pDoc->m_pDibInit->GetPixelGray(i, j);
num[gray]++;
graysum += gray;
}
avl = graysum / (width * height);
for ( i = 0; i < 256; i++)
p[i] = (num[i]+0.0) / (width * height);
double max = 0; //最大类间方差
int max_k=0; //记录令取得最大类间方差的K
for (int k = 0; k < 256; k++) //分别算以k分割的均值和类间方差
{
double p_sum1 = 0, p_sum2 = 0; //sum1为小于等于k的灰度值概率和
double avl1 = 0, avl2 = 0; //均值
double sqare = 0; //类间方差
for ( m = 0; m <= k; m++)
p_sum1 += p[m];
for ( m = k; m < 256; m++)
p_sum2 += p[m];
for ( m = 0; m < 256; m++)
{
if (m <= k)
avl1 += (m * p[m] + 0.0) / p_sum1;
else
avl2 += (m * p[m] + 0.0) / p_sum2;
}
sqare = p_sum1 * p_sum2 * (avl1 - avl2) * (avl1 - avl2);
if (sqare >= max)
{
max = sqare;
max_k = k;
}
}
CString str1;
str1.Format("阈值:%d", max_k);
MessageBox(str1);
//二值化
for( i=0;i<width;i++)
for (int j = 0; j < height; j++)
{
if (pDoc->m_pDibInit->GetPixelGray(i, j) <= max_k)
pDoc->m_pDibInit->SetPixelGray(i, j, 0);
else
pDoc->m_pDibInit->SetPixelGray(i, j, 255);
}