现在,我们来讨论获得BMP图像之后对图像的处理,比如:平滑、锐化,这些处理可以从图中获得我们所需要的信息。
一、平滑
为什么我们需要平滑?我的理解是,图像可能因为sensor的原因,出现很多白噪声、白点,我们想要将这些白点给去除掉,即进行去噪处理,也就是平滑。
我们可以看到去除噪声之后的图像,视觉效果来看好了很多,但是也发现模糊了不少。因为是平滑,可以理解为让一个个高低不平的山峰变得相平一些,在一定程度上抹去他们的差异,视觉对比度减小了,看起来就变得更为模糊一些了。
那怎么实现呢?课程上我们是选择卷积。
计算机视觉CV 的卷积其实和卷积原本的意思(我也忘了什么意思)有所简化,卷积运算如下:
假设有一副图像的部分像素灰度值如下:
我们定义一个 3x3 的卷积核(类似于矩阵),卷积核的形状及其值如下:
首先我们将这个卷积核在上述像素灰度值中从左上角开始进行卷积计算,像这样
卷积计算,就是使用卷积核的权值(红色)和对应的像素值(黑色)进行相乘,并且求和,例子中的计算就是:
2 x 20 + 1 x 54 + 3 x 125 + 1 x 12 + 0 x 12 + 2 x 98 + 6 x 23 + 5 x 56 + 4 x 78
简单的矩阵点乘之后求和得到一个数,这个数存储到新的矩阵中。新的矩阵又是啥?就是一个存储卷积结果的矩阵,见上图,我们可以看到在红色框单步向右移动的过程中,会产生三个值,上图:
在完成一行的遍历后往下方向还有两行,一共三行,我们就可以知道结果矩阵就是一个 3 x 3 的矩阵,用来存储每一步卷积计算的结果。
遍历完之后,卷积运算就结束了。以此类推,对于整幅图像的卷积计算,其实就是一个窗口在图像上一行一行的划动求得一个新的矩阵的过程。
那么我们可以发现,在上面的例子中,如果我们将 3x3 卷积核的每个权值设置为九分之一,那不就是可以求得每个窗口的均值了吗。
思想很简单,接下来就是代码:
不对,似乎还有一个问题。
卷积核超出了图像边界怎么办?问得好!为了不增加工作量(偷懒),我们可以先计算卷积核单侧的长度,比如 3x3 的卷积核单侧的长度就是 1 ,5x5 的就是 2。卷积核长度一般为奇数哈,为了保证两边对称、能找出最中间的值。
所以在代码实现中呢,以单侧长度为1的3x3卷积核为例,卷积操作一般不会对图像最外的那一圈进行操作,防止卷积核突出图像边界。
在上代码之前,希望同学们弄清楚一点,卷积的代码会有意义上的两层循环,一层是遍历图像,另外一层是遍历卷积核。
好,废话不多说,上代码:
public System.Drawing.Bitmap Convolution_calculation(System.Drawing.Bitmap bitmap,
double[ , ] kernel = null)//卷积运算
{
/*Paras:
bitmap: 进行卷积运算的Bitmap
kernel: 卷积核数组
*/
System.Drawing.Bitmap new_bitmap = new System.Drawing.Bitmap(bitmap);
//将bitmap复制给将要作为结果输出的new_bitmap
int kernel_length; //意为卷积核单侧的长度
if (kernel.GetLength(0) != kernel.GetLength(1) || kernel == null)
{ //若卷积核长宽不相等或不存在卷积核则报错
MessageBox.Show("The convolution kernel is wrong!", "ERROR!");
return null;
}
if (kernel.GetLength(0) % 2 == 1) //奇数
{//卷积核为奇数则给卷积核单侧长度赋值
kernel_length = (kernel.GetLength(0) - 1) / 2;
}
else
{//若卷积核不为奇数则报错
MessageBox.Show("Kernel_size shoule be singular!", "ERROR!");
return null;
}
int[] val = new int[3];
//长度为3的数组,因为BMP图像有三个通道,所以我们需要三个数来存储点乘的值
for (int x = kernel_length; x + kernel_length < bitmap.Width;x++)
for (int y = kernel_length; y + kernel_length < bitmap.Height;y++)
{ //注意理解这里的xy循环起始值,就是为了防止卷积核超出图像外
val[0] = 0;//在每一次卷积核移动时,记得清空上一次卷积计算的结果
val[1] = 0;
val[2] = 0;
for (int j = -kernel_length; j <= kernel_length; j++)
{
for (int i = -kernel_length; i <= kernel_length; i++)
{ //在卷积核内的循环
val[0] += (int)((double)bitmap.GetPixel(x + i, y + j).R * kernel[j + kernel_length, i + kernel_length]);//R通道的点乘结果
val[1] += (int)((double)bitmap.GetPixel(x + i, y + j).G * kernel[j + kernel_length, i + kernel_length]);//G通道的点乘结果
val[2] += (int)((double)bitmap.GetPixel(x + i, y + j).B * kernel[j + kernel_length, i + kernel_length]);//B通道的点乘结果
}
}
this.clip_array(0,255,ref val);//这里的clip_array函数其实就是一个截断,遍历这个数组并保证像素值在0-255之间,数组的实现我放在下边。
new_bitmap.SetPixel(x, y, System.Drawing.Color.FromArgb(val[0], val[1], val[2]));//获得当前卷积结果后设置新的像素值
}
return new_bitmap; //返回卷积运算后的结果
}
其中,clip_array函数的实现如下:
private void clip_array(int min,int max,ref int[] array) //截断
{
for(int i = 0; i{
if (array[i] > max)
array[i] = max;
if (array[i] < min)
array[i] = min;
}
}
好!主要的函数介绍到这里就完毕了!
那要怎么样进行图像的平滑呢(手把手教)?有了函数,就不再多说了,再上代码
double[,] kernel = new double[3, 3] { { 1.0 / 9.0, 1.0 / 9.0, 1.0 / 9.0 },
{ 1.0 / 9.0, 1.0 / 9.0, 1.0 / 9.0 },
{ 1.0 / 9.0, 1.0 / 9.0, 1.0 / 9.0 } };//就像前面说的,权值都设置为九分之一
pictureBox1.Image = Convolution_calculation(My_bitmap, kernel)
//这样就成功显示了新的Bmp图
另外,图像Nodata的修复也可以使用卷积平滑哦~
二、锐化
锐化多用于边缘检测,我们直接上图给大家看看效果。
原始图是这样(来源:SWJTU 地院曹老师),我们使用Laplace算子后:
kernel = new double[3, 3] { { 0, 1.0, 0 },
{ 1.0, -4.0, 1.0 },
{ 0, 1.0, 0 } };
我们可以看到,一些边缘的地方被高亮显示了,其中还包括植物(因为树叶是绿色的,阴影是黑色的,也算一个边缘)。锐化就是用来如此提取边缘的,网上还有其他的算子,Prewitt、Sobel,大家都可以去试试。
好!本篇关于卷积的介绍就到这里结束(又要熄灯了)!