新学期开始啦,新的学期举起我新的的flag,开开心心新学期,认认真真搞学习。
什么?这次程序设计作业要做图像的边缘检测?图片边缘是什么?要怎么来找?
再认真读一遍题目:“按照 11.4 最后一个Bitmap的案例(更新中),构建按一个 Bitmap 结构体,利用图片像素和周围像素数值上的差异,描绘出图片的边缘,并自行拓展更多的功能”。
边缘检测的最最基础的方式要利用图片像素和周围像素数值上的差异,最简单的方法就是相邻像素值直接相减。等等,好像忘了什么,一个图片是一个二维的矩阵,如果只考虑横向的相邻像素值之差,那么如果边缘是横线,那么不就检测不出来了吗?那么就把纵向的像素值之差也考虑进来。
利用左右像素值的差与上下像素值的差的平方和开根号,和阈值进行比较,大于阈值就判定为边缘,灰度值设置为255,小于阈值就判定不是边缘,灰度值设为0。
源码01:
//用左右像素点灰度值之差与上下两像素点灰度值之差的平方和再开根号,与阈值比较的方法进行边缘检测
void edgedetection(Bitmap* pBmp, unsigned char threshold)
{
for (int y = 0; y height - 1; ++y)
{
unsigned char* lineBegin = pBmp->data + y * pBmp->lineSize;
unsigned char* nlineBegin = pBmp->data + (y + 1) * pBmp->lineSize;//下一行开头
for (int x = 0; x width * 3 - 3; x += 3)
{
BGR* p = (BGR*)(lineBegin + x);
BGR* xnp = (BGR*)(lineBegin + x + 3);//右侧相邻像素点
BGR* ynp = (BGR*)(nlineBegin + x);//左侧相邻像素点
double gray = p->Red * 0.3 + p->Green * 0.6 + p->Blue * 0.1;//当前像素点的灰度值
double xngray = xnp->Red * 0.3 + xnp->Green * 0.6 + xnp->Blue * 0.1;//左侧像素的灰度值
double yngray = ynp->Red * 0.3 + ynp->Green * 0.6 + ynp->Blue * 0.1;//下侧像素的灰度值
double t = (gray - xngray) * (gray - xngray) + (gray - yngray) * (gray - yngray);
t = sqrt(t);
if (t > threshold)
p->Red = p->Green = p->Blue = 255;
else
p->Red = p->Green = p->Blue = 0;
}
}
}
一通操作之后,得到在不同的阈值下的图片边缘检测的结果:
原图:
阈值为30时的处理结果:
阈值为50时的处理结果:
阈值为70时的处理结果:
对于老师给的示例图片,这样来做边缘检测好像效果也不错。那么对于图片处理领域最经典的图片lena呢?采用同样的方法进行边缘检测:
原图:
阈值为30时的处理结果:
阈值为50时的处理结果:
阈值为70时的处理结果:
对于这张经典图片,我们这个方法就显得不太灵了,可以看出阈值为30的时候其他地方的边缘检测还算可以,但是帽子区域以及后面的装饰物区域的边缘化明显效果不太好。
既然土办法不太行,那么就只能求助CSDN了。
微分的概念
让我们来看看CSDN上的大佬们是怎么来定义图片边缘的:图像的边缘是图像的基本特征,边缘点是灰度阶跃变化的像素点,即灰度值的导数较大或极大的地方。
看到这里就会有小伙伴产生疑惑了,灰度图片不是一个二维矩阵吗,怎么又要求导了?别慌,让我们细细品。图像可以看成二元函数f(x,y),(x,y)是像素的位置,f(x,y)是该处的灰度值,这样一张图片就变成了一个离散的二元函数(如下图)。但我们在学高数的时候只学过连续函数求导,我们学的高数里面不连续的函数是不存在导数的,这里的导数又是怎么一回事呢?
在离散的数据中,我们用逼近微分,即差分来表示数据的导数。
1.一阶差分:
2.二阶差分:
这么一看,一阶微分(简单来说)不就是我们之前求的相邻两个像素点灰度值之差嘛!
为了能够更好的解决图像求微分的问题,我们首先要引入微分算子的概念。算子(英文:Operator)是从函数到函数的映射。例如微分算子(或者叫求导算子)作用在函数n上,就得到其导数
首先介绍用一阶微分算子来检测图像边缘。对于二维的图像,用二元的梯度算子来求得图像的梯度场:
梯度的模值:
简单来看:求图像在x方向的偏导,即为左右像素点灰度值之差,求图像y方向上的偏导,即为上下两像素点灰度值之差,梯度的模值即为两个差的平方和再开平方,和我们刚开始用的方法不谋而合。
等等,我们CSDN的目的不是为了改进这个边缘检测的方法吗?怎么绕了一大圈又回来了?
这个方法肯定是哪里出了问题, 排除纯理论的知识,可能造成边缘检测效果不同的重要原因就是在逼近微分即差分的过程中,差分的方式不同。我们这种差分的方式只考虑了左右两个像素点之间以及上下两个点之间的关系,这样做肯定是不够全面的,我们需要对这种差分方式进行改进。
一阶微分算子模板简介
在用一阶微分算子来进行边缘检测的时候,有两个关键点:
图像是离散的,微分算子也要离散化,我们用差分的方式去逼近微分,并将微分算子简化为模板;
到底要不要把一个点看作边缘点,我们是通过设定一个阈值来判断的,微分后大于这个阈值,就判作边缘。
在实际应用过程中,常常将微分算子简化为一个n*n的矩阵,如:
Roberts算子:
Sobel算子:
Prewitt算子:
卷积
为了实现边缘检测,我们首先需要用微分算子模板对图像进行卷积的操作。卷积是一种新的数学运算,我们在这里不做过多的解释(好奇的同学可以看https://www.matongxue.com/madocs/32.html),我们只来看图像是如何卷积的。
(用于卷积的微分算子模板叫做卷积核。)
第一步卷积核位于图片的左上角,将卷积核覆盖点的像素值与对应的位置卷积核的值相乘,然后将乘积相加。将结果放置在卷积核中心对应的位置,构成新的图像。
卷积过程中的第一步(图源https://mlnotebook.github.io/post/CNN1/)
然后卷积核每次移动一个像素重复此过程知道遍历图像中所有像素点为止。
卷积核在图像上移动,执行卷积(图源https://mlnotebook.github.io/post/CNN1/)
不知道你看到这里是否发现一个问题,这样经过卷积之后图像会比原来小上一圈!!!要如何解决这个问题呢?常用的方法是在卷积之前对原图像进行
“padding ”,即提前补上一圈。这一圈可以是一圈0,也可以根据需要补上其他值。
使用零填充以使生成的图像不会缩小(图源https://mlnotebook.github.io/post/CNN1/)
Sobel算子示例
下面我们以Sobel算子为例大致介绍一下微分算子模板该怎么来用。
首先用Sx和Sy分别对图像进行卷积,得到两个卷积后的图像,此时每个像素点上的数据就类似于我们之前求得的左右两个像素点灰度值之差和上下两个像素点灰度值之差。将卷积后的图像对应的像素点的值的绝对值相加,跟阈值比较,大于阈值表示此处变化较大,可视为边缘,小于阈值则视为不是边缘点。
取不同的阈值可以得到如下图所示结果,可以看出,细节处的边缘检测明显得到了改善。
源码02
//将BMP图像转化为灰度矩阵,便于后续的数据处理
void BGRtoGray(Bitmap* pBmp, double* gray)
{
for (int y = 0; y height; ++y)
{
unsigned char* lineBegin = pBmp->data + y * pBmp->lineSize;
for (int x = 0; x width * 3; x += 3)
{
BGR* p = (BGR*)(lineBegin + x);
*(gray+y* pBmp->width+x/3) = (float)(p->Red * 0.3 + p->Green * 0.6 + p->Blue * 0.1);
}
}
}
//将灰度矩阵补上一圈零
void padding(double* gray, double* afterpadding,int width,int height)
{
int i = 0, j = 0;
//补第一行和最后一行
for (i = 0; i < width + 2; i++)
{
*(afterpadding + i) = 0;
*(afterpadding + (width + 2) * (height + 1) + i) = 0;
}
//补第一列和最后一列
for (j = 1; j < height + 1; j++)
{
*(afterpadding + j*(width+2)) = 0;
*(afterpadding + j*(width + 2) + width+1) = 0;
}
//将原数据直接复制到中间的区域
for (i = 0; i < height; ++i)
{
for (j = 0; j < width; ++j)
{
*(afterpadding + (i + 1) * (width + 2) + j + 1) = *(gray + i * width + j);
}
}
}
//用sobel算子进行卷积
void sobelconvolution(double* gray, double* afterpadding, int width, int height)
{
double ul, uc, ur, dl, dc, dr;
double lu, lc, ld, ru, rc, rd;
double hir, vec;
for (int i = 0; i < height; i++)
{
for (int j = 0; j < width; j++)
{
// 垂直梯度算子:检测水平边缘
vec = 0;
ul = *(afterpadding + (i)* (width+2) + j) * (-1);
uc = *(afterpadding + (i)* (width+2) + j + 1) * (-2);
ur = *(afterpadding + (i)* (width+2) + j + 2) * (-1);
dl = *(afterpadding + (i + 2) * (width+2) + j) * 1;
dc = *(afterpadding + (i + 2) * (width+2) + j + 1) * 2;
dr = *(afterpadding + (i + 2) * (width+2) + j + 2) * 1;
vec = ul + uc + ur + dl + dc + dr;
// 水平梯度算子:检测垂直边缘
hir = 0;
lu = *(afterpadding + (i)* (width+2) + j) * (-1);
lc = *(afterpadding + (i + 1)* (width+2) + j) * (-2);
ld = *(afterpadding + (i + 2)* (width+2) + j) * (-1);
ru = *(afterpadding + (i) * (width+2) + j) * 1;
rc = *(afterpadding + (i) * (width+2) + j) * 2;
rd = *(afterpadding + (i) * (width+2) + j) * 1;
hir = lu + lc + ld + ru + rc + rd;
*(gray+i*width+j) = abs(vec)+abs(hir);
}
}
}
//将卷积后的矩阵与阈值对比,判断边缘位置
void threshold_judgment(Bitmap* pBmp, double* gray, unsigned char threshold)
{
for (int y = 0; y height; ++y)
{
unsigned char* lineBegin = pBmp->data + y * pBmp->lineSize;
for (int x = 0; x width * 3; x += 3)
{
BGR* p = (BGR*)(lineBegin + x);
if (*(gray + y * pBmp->width + x/3) > threshold)
{
p->Red = p->Green = p->Blue = 255;
}
else
{
p->Red = p->Green = p->Blue = 0;
}
}
}
}
//采用sobel算子卷积的方法进行边缘查找的总函数
void sobel(Bitmap* pBmp, unsigned char threshold)
{
int width = pBmp->width;
int height = pBmp->height;
//cout << width << '\r' << height << endl;
//cout <data) << endl;
double* gray = new double[width * height];
BGRtoGray(pBmp, gray);
double* afterpadding = new double[(width+2) * (height+2)];
padding(gray, afterpadding, width, height);
sobelconvolution(gray, afterpadding, width, height);
threshold_judgment(pBmp, gray, threshold);
}
延伸
上面介绍的Sobel算子以及提到的Roberts算子和Prewitt算子都是一阶微分算子,感兴趣的同学也可以自己取探索用二阶微分算子进行边缘检测(传送