现代图像处理算法教程-全-
现代图像处理算法教程(全)
一、简介
这本书包含图像处理算法的描述,如降噪,包括减少脉冲噪声,对比度增强,阴影校正,边缘检测,等等。该书的配套网站上包含了用 C#编程语言实现算法的项目源代码。源代码是 Windows 窗体项目,而不是 Microsoft 基础类库(MFC)项目。这些项目中的控件和图形是通过简单易懂的方法实现的。我选择了这种实现控件和图形服务的方式,而不是基于 MFC 的方式,因为使用 MFC 的集成开发环境(IDE)非常昂贵。此外,使用 MFC 的软件相当复杂。它在一个项目中包括许多不同的文件,用户很大程度上无法理解这些文件的意义和用途。相反,Windows 窗体及其实用工具是免费的,更容易理解。它们提供了类似于 MFC 的控件和图形。
为了提供快速的图像处理,我们将 Windows 窗体中的标准类Bitmap
的对象转换为我们的类CImage
的对象,它们的方法很快,因为它们直接访问像素集,而使用Bitmap
的标准方式包括实现相对较慢的方法GetPixel
和SetPixel
或使用LockBits
的方法,它们很快,但不能用于索引图像。
类CImage
相当简单:它包含图像的属性width
、height
和nBits
以及实际项目中使用的方法。我的方法在专门讨论项目的章节中有描述。下面是我们这个阶层CImage
的定义。
class CImage
{ public Byte[] Grid;
public int width, height, nBits;
public CImage() { } // default constructor
public CImage(int nx, int ny, int nbits) // constructor
{
width = nx;
height = ny;
nBits = nbits;
Grid = new byte[width * height * (nBits / 8)];
}
public CImage(int nx, int ny, int nbits, byte[] img) // constructor
{
width = nx;
height = ny;
nBits = nbits;
Grid = new byte[width * height * (nBits / 8)];
for (int i = 0; i < width * height * nBits / 8; i++) Grid[i] = img[i];
}
} //*********************** end of class CImage *****************
项目描述中描述了类CImage
的方法。
二、降噪
数字图像经常因随机误差而失真,随机误差通常被称为噪声。主要有两种噪声:高斯噪声和脉冲噪声(见图 2-1 )。高斯噪声是具有类似于高斯分布的概率分布的统计噪声。它主要出现在采集期间(例如,在传感器中)。这可能是由于照明不足或传感器温度过高造成的。它来自许多自然来源,如导体中原子的热振动,称为热噪声。它影响图像的所有像素。
脉冲噪声也称为椒盐噪声,表现为稀疏出现的亮像素和暗像素。它来自于脉冲失真,比如那些来自于电子设备附近的电焊所产生的失真,或者是由于旧照片的不正确存储所导致的失真。它影响像素组的一小部分。
图 2-1
噪声的例子:(a)高斯噪声;脉冲噪声
最简单的过滤器
我们首先考虑降低高斯噪声强度的方法。本章稍后将讨论降低脉冲噪声强度的算法。
降低高斯噪声强度的最有效的方法是用在 P 附近的一小组像素的亮度的平均值来代替像素 P 的亮度。这种方法基于随机值理论的事实:N 个均匀分布的随机值的平均值的标准偏差比单个值的标准偏差小√ N 倍。这种方法使用遮罩对图像进行二维卷积,遮罩是排列在一个 W × W 像素的正方形中的权重数组,实际像素 P 位于正方形的中间。本章稍后将介绍该过滤器的源代码。这种方法有两个缺点:它相当慢,因为它对图像的每个像素使用 W × W 加法,并且它使图像模糊。它将近似同质区域边界处的精细边缘转换为斜坡,其中亮度在宽度等于 W 像素的条纹中线性变化。快速均值滤波器克服了第一个缺点。然而,第二个缺点非常重要,它妨碍了均值滤波器用于消除噪声。然而,平均滤波器对于改善具有阴影的图像(即,表示非均匀照明物体的图像)是重要的,我们将在后面看到。我建议另一个过滤器的目的是消除噪音。
首先,让我们描述一下最简单的均值滤波器。我给出了这个简单方法的源代码,读者可以在自己的程序中使用它。在这段代码以及许多其他代码示例中,我们使用了某些类,这些类将在下一节中定义。
最简单的均值滤波器
非加权平均滤波器计算一个滑动方形窗口中的平均灰度值,该窗口为 W × W 像素,其中 W 是方形滑动窗口的宽度和高度。窗口大小 W 越大,对高斯噪声的抑制越强:滤波器以因子 W 降低噪声。为了对称,数值 W 通常为奇数 W = 2 × h + 1。窗口内像素的坐标( x + xx,y + yy )以当前中心像素( x,y )为中心对称变化,间隔:h≤xx≤+h和h≤YY≤+h**h= 1,2,3
在图像边界附近,窗口部分位于图像之外。在这种情况下,计算失去了其自然的对称性,因为只有图像内部的像素可以被平均。解决边界问题的合理方法是控制坐标( x + xx,y + yy ),如果它们指向图像之外。如果这些坐标在图像之外,灰度值的求和必须暂停,并且除数 nS 不应增加。
这里给出了一个颜色平均算法的例子。在我们的代码中,我们经常使用由某些符号行表示的注释:标有=
的行标记循环的开始和结束,标有减号的行标记if
指令,等等。这使得代码的结构更加清晰可见。
该算法最简单的慢速版本有四个嵌套的for
循环。
public void CImage::Averaging(CImage input, int HalfWidth)
{ int nS, sum;
for (int y=0; y<height; y++) //================================
{ for (int x=0; x<width; x++) //==============================
{ nS=sum=0;
for (int yy=-HalfWidth; yy<=HalfWidth; yy++) //=======
{ if (y+yy>=0 && y+yy<input.height )
for (int xx=-HalfWidth; xx<=HalfWidth; xx++) //===
{ if (x+xx>=0 && x+xx<input.width)
{ sum+=input.Grid[x+xx+width*(y+yy)];
nS++;
}
} //====== end for (xx... ===================
} //======== end for (yy... =======================
Grid[x+width*y]=(sum+nS/2)/nS; //+nS/2 is for rounding
} //============ end for (x... ========================
} //============== end for (y... ========================
} //****************** end Averaging ****************************
这是源代码:读者可以复制它并放入它的 C#源代码中,它就可以工作了。
参数HalfWidth
是滑动窗口宽度的一半。窗口的宽度和高度都等于2*HalfWidth+1
。前面代码中的变量x
和y
是Grid,
中像素的索引,xx
和yy
是滑动平均窗口内像素的索引。
最内层for
循环中sum
的计算需要 W × W 加法和 W × W 对输入图像的每个像素进行图像访问,相当耗时。
让我们再一次注意到,虽然平均滤波器在降低高斯噪声的强度方面非常有效,但是它们强烈地模糊了图像。因此,它们不应用于降噪。为此,我建议使用本章稍后描述的 sigma 滤波器。平均用于阴影校正(参见第四章)。因此,它将主要用于相当大的滑动窗口,大到图像宽度的一半。那么最简单的平均程序变得非常耗时,以致于实际上不可能使用它。例如,在 1000 × 1000 像素的灰度图像和 400 × 400 像素的滑动窗口的情况下,这对于阴影校正是典型的,函数Averaging
在标准 PC 上的运行时间可能需要大约 20 分钟。
快速平均滤波器
最简单的平均滤波器对图像的每个像素进行加法运算和除法运算。例如,在滑动窗口为 5 × 5 = 25 个像素的情况下,它对每个像素进行 5 5 + 1 = 26 次操作。有些应用(例如阴影校正)使用更大的滑动窗口;比如 400 × 400 的一个= 16 万像素。如果应用使用最简单的均值滤波器和如此大的滑动窗口,它可以运行几分钟。
可以使用以下基本思想来加速平均滤波器:可以首先计算 1 × W 像素的小一维窗口中的灰度值之和。我们称这些总和的数组为列。在以下对基本思想的描述中,我们使用笛卡尔坐标系,图像的列的索引是横坐标 x ,行的索引是纵坐标 y 。我们认为纵坐标轴是向下的,从上到下,这在图像处理中是常见的,并且不同于数学中纵坐标轴的方向。
快速均值滤波器解决的问题与最简单的滤波器相同,但速度要快得多。快速滤镜以与最简单滤镜相同的方式模糊图像。因此,它的主要应用是阴影校正,而不是噪声抑制。
在使用刚才提到的基本思想时,可以将每个像素的运算次数从 W × W 减少到≈4
:快速滤波器计算并保存每列 W 像素的灰度值之和,每列的中间像素位于图像的实际行中(图 2-2 )。然后,滤波器直接计算其中心像素位于一行开始处的窗口上的总和;也就是说,将各列中保存的总数相加。然后,窗口沿该行移动一个像素,筛选器通过将窗口右边框的列总和的值相加,并减去左边框的列总和的值,来计算下一个位置的总和。需要检查要增加或减少的列是否在图像内部。如果不是,则必须跳过相应的加法或减法。
图 2-2
快速平均滤波器的功能说明
由于对列和的计算应用了类似的过程,每个像素的平均加法或减法次数减少到≈2 + 2 = 4
。窗口内的总和必须仅针对每行开头的像素直接计算(即,通过添加HalfWidth
+ 1 列总和)。必须仅对图像第一行的像素直接计算列的总和。
当前进到图像的下一行时,过滤器通过增加每列下端以下的灰度值并减去每列上端的灰度值来更新列的值(图 2-2 )。在这种情况下,还需要检查要加或减的灰度值是否在图像中。一旦计算出窗口中灰度值的总和,该滤波器就将该总和除以(舍入)窗口与图像相交处的像素数,并将结果保存在输出图像的相应像素中。
这是为过滤灰度图像而设计的快速平均滤波器的最简单版本的源代码。
public int FastAverageM(CImage Inp, int hWind, Form1 fm1)
// Filters the grayscale image "Inp" and returns the result in 'Grid' of the
// calling image.
{
width = Inp.width ; height = Inp.height; // elements of class CImage
Grid = new byte[width * height];
int[] SumColmn; int[] nPixColmn;
SumColmn = new int[width];
nPixColmn = new int[width];
for (int i = 0; i < width ; i++) SumColmn[i] = nPixColmn[i] = 0;
int nPixWind = 0, SumWind = 0;
for (int y = 0; y < height + hWind; y++) //=============================
{
int yout = y - hWind, ysub = y - 2 * hWind - 1;
SumWind = 0; nPixWind = 0;
int y1 = 1 + (height + hWind) / 100;
for (int x = 0; x < width + hWind; x++) //===========================
{
int xout = x - hWind, xsub = x - 2 * hWind - 1; // 1\. and 2\. addition
if (y < height && x < width )
{
SumColmn[x] += Inp.Grid[x + width * y];
nPixColmn[x]++; // 3\. and 4\. addition
}
if (ysub >= 0 && x < width )
{
SumColmn[x] -= Inp.Grid[x + width * ysub];
nPixColmn[x]--;
}
if (yout >= 0 && x < width )
{
SumWind += SumColmn[x];
nPixWind += nPixColmn[x];
}
if (yout >= 0 && xsub >= 0)
{
SumWind -= SumColmn[xsub];
nPixWind -= nPixColmn[xsub];
}
if (xout >= 0 && yout >= 0)
Grid[xout + width * yout] = (byte)((SumWind + nPixWind / 2) / nPixWind);
} //===================== end for (int x = 0; =====================
} //====================== end for (int y = 0; ======================
return 1;
} //************************* end FastAverageM ***********************
接下来我将展示为彩色和灰度图像设计的快速平均滤波器的通用源代码。它使用变量int nbyte
,对于彩色图像设置为 3,对于灰度图像设置为 1。我们为(2*hWind + 1) 2 像素的滑动窗口中的颜色强度之和定义了一个三元素的数组SumWind[3]
,用于红色、绿色和蓝色强度之和。在灰度图像的情况下,只有元素SumWind[0]
被使用。我们使用下面描述的变量。
具有坐标(c+nbyte*x, nbyte*y)
的位置是颜色通道的位置,红色、绿色或蓝色通道之一,其强度被添加到数组SumColmn
的相应元素。位置(c+nbyte*x, nbyte*ysub)
是一个颜色通道的位置,其强度将从SumColmn
中减去。变量c+nbyte*x
是短列的横坐标,其内容将被添加到SumWind[c]
。变量c+nbyte*xsub
是短列的横坐标,其内容将从SumWind[c]
中减去。
public int FastAverageUni(CImage Inp, int hWind, Form1 fm1)
// Filters the color or grayscale image "Inp" and returns the result in
// 'Grid' of calling image.
{ int c = 0, nByte = 0;
if (Inp.N_Bits == 8) nByte = 1;
else nByte = 3;
width = Inp.width ; height = Inp.height; // elements of the class "Cimage"
Grid = new byte[nByte * width * height];
int[] nPixColmn;
nPixColmn = new int[width];
for (int i = 0; i < width ; i++) nPixColmn[i] = 0;
int[,] SumColmn;
SumColmn = new int[width, 3];
int nPixWind = 0, xout = 0, xsub = 0;
int[] SumWind = new int[3];
for (int y = 0; y < height + hWind; y++) //=============================
{ int yout = y - hWind, ysub = y - 2 * hWind - 1;
nPixWind = 0;
for (c = 0; c < nByte; c++) SumWind[c] = 0;
int y1 = 1 + (height + hWind) / 100;
for (int x = 0; x < width + hWind; x++) //============================
{ xout = x - hWind;
xsub = x - 2 * hWind - 1; // 1\. and 2\. addition
if (y < height && x < width) // 3\. and 4\. addition
{ for (c=0; c< nByte; c++)
SumColmn[x, c] += Inp.Grid[c + nByte*(x + width*y)];
nPixColmn[x]++;
}
if (ysub >= 0 && x < width )
{ for (c=0; c<nByte; c++)
SumColmn[x, c] -=Inp.Grid[c+nByte*(x+ width*ysub)];
nPixColmn[x]--;
}
if (yout >= 0 && x < width )
{ for (c = 0; c < nByte; c++) SumWind[c] += SumColmn[x, c];
nPixWind += nPixColmn[x];
}
if (yout >= 0 && xsub >= 0)
{ for (c = 0; c < nByte; c++) SumWind[c] -= SumColmn[xsub, c];
nPixWind -= nPixColmn[xsub];
}
if (xout >= 0 && yout >= 0)
for (c = 0; c < nByte; c++)
Grid[c+nByte*(xout+width*yout)]=(byte)( SumWind[c] / nPixWind);
} //============= end for (int x = 0; =============================
} //============== end for (int y = 0; ==============================
return 1;
} //***************** end FastAverageUni ********************************
该源代码可以在相应的 Windows 窗体项目中使用。它不是最快的版本;通过去除内部循环中的乘法运算,速度可以提高 50%。一些乘法可以在开始循环之前执行;其他一些可以通过添加来代替。更快的版本可以包含以下九个循环,而不是在FastAverageM
或FastAverageUni
中具有索引 y 和 x 的两个循环:
for (int yOut=0; yOut<=hWind; yOut++)
{ for(int xOut=0; xOut<=hWind; xOut++){...} //Loop 1
for(xOut=hWind+1; xOut<=width-hWind-1; xOut++) {...} //Loop 2
for(xOut=width-hWind; xOut<width; xOut++) {...} //Loop 3
}
for(yOut=hWind+1; yOut<=height-hWind-1; yOut++)
{ for(xOut=0; xOut<=hWind; xOut++) {...} //Loop 4
for(xOut=hWind+1; xOut<=width-hWind-1; xOut++) {...} //Loop 5
for(xOut=width-hWind; xOut<width; xOut++) {...} //Loop 6
}
for(yOut=height-hWind; yOut<height; yOut++)
{ for(xOut=0; xOut<=hWind; xOut++) {...} /Loop 7
for(xOut=hWind+1; xOut<=width-hWind-1; xOut++) {...} //Loop 8
for(xOut=width-hWind; xOut<width; xOut++) {...} //Loop 9
}
九个循环中的每一个循环都处理图像的一部分(见图 2-3 ),该部分要么是hWind
+ 1 像素宽,要么是hWind
+ 1 像素高。
图 2-3
图像的九个部分对应于九个循环
仅当满足条件hWind
≤ min(宽度,高度)/2 - 1 时,才能使用此版本的快速平均滤波器。在这个版本的例程中,变量为xOut
的内部循环不包含乘法运算和if
指令。该例程比之前描述的FastAverageM
快 60%。然而,它要长得多,调试起来也困难得多。速度的提高并不重要:这段代码用 0.7 秒处理一个 2448 × 3264 像素的大彩色图像,滑动窗口为 1200 × 1200 像素,而FastAverageM
需要 1.16 秒。这些计算时间几乎与滑动窗口的大小无关:对于 5 × 5 像素的滑动窗口,它们相应地为 0.68 和 1.12 秒。
快速高斯滤波器
平均滤波器产生平滑的图像,其中可以看到原始图像中不存在的一些矩形形状。这些形状的出现是因为平均滤镜将每个亮像素转换为滑动窗口大小的均匀亮矩形。一旦一个像素具有明显不同于相邻像素值的亮度,矩形就变得可见。这是不必要的失真。当使用高斯滤波器时,可以避免这种情况,该高斯滤波器将要相加的灰度值乘以根据二维高斯定律随着距窗口中心的距离而衰减的值。此外,高斯滤波器能更好地抑制噪声。重量示例如图 2-4 所示。
图 2-4
经典高斯滤波器滑动窗口中的权重示例
这些值称为过滤器的权重。对应于二维高斯定律的权重是小于 1 的浮点数:
w(、和=(2π】和
**它们可以预先计算出来,保存在一个二维数组中,数组的大小与滑动窗口的大小相对应(图 2-4 )。然后将滤波图像的灰度值或颜色通道乘以权重,并计算乘积的和。这个过程需要对要滤波的灰度图像的每个像素进行W2浮点乘法和 W 2 加法,其中 W 是滑动窗口的宽度。在彩色图像的情况下,这个数字是 3W2。
使用统计学知识,即许多等效概率分布的卷积趋向于高斯分布,有可能获得大致相同的结果。这一过程的收敛如此之快,以至于仅计算三个矩形分布的卷积就足以获得良好的近似。矩形分布在区间内密度恒定,在区间外密度为零。如果一个图像用一个滤波器处理三次,其结果相当于用三个矩形权重的卷积进行加权滤波。因此,为了近似执行图像的高斯滤波,用快速平均滤波器对图像滤波三次就足够了。该过程要求独立于窗口大小的像素的每个颜色通道进行 4 × 3 = 12 次整数加法。我们已经计算出等效高斯分布的标准与平均滤波器的滑动窗口的半宽度成比例。在表 2-1 hWind
中,是平均窗口的半宽度,Sigma
是随机变量的标准,其分布对应于通过快速滤波器的三重滤波计算的权重。
表 2-1。随着 hWind 的增加,适马/hWind 的关系趋于 1
|hWind
|
one
|
Two
|
three
|
four
|
five
|
| --- | --- | --- | --- | --- | --- |
| Sigma
| One point four one four | Two point four five six | Three point four five nine | Four point four five six | Five point four nine |
| Sigma/hWind
| One point four one four | One point two one eight | One point one five three | One point one one four | One point zero nine eight |
可以看到,当窗口宽度增加时,关系Sigma/hWind
趋于 1。
图 2-5 显示了近似高斯滤波器的权重与真实高斯权重的差异。
图 2-5
近似高斯滤波器和真实高斯滤波器的权重
中值滤波器
平均和高斯滤波器可最有效地抑制高斯噪声。具有宽度为 W = 2 h + 1 像素的滑动窗口的平均滤波器将同质区域的陡峭边缘转换为宽度为 W 的斜坡。在高斯滤波器的情况下,斜坡更陡。但是,这两种滤镜都会模糊图像,因此不应用于噪声抑制。
大多数关于图像处理的教科书都推荐使用中值滤波器来抑制噪声。中值滤波器对滑动窗口中的颜色强度进行排序,并用位于排序序列中间的强度替换滑动窗口中间的强度。中值滤波器也可用于抑制脉冲噪声或椒盐噪声。
然而,几乎没有哪本教科书能让读者注意到中值滤波器的重大缺陷。首先,它严重扭曲了形象。具有(2 * h + 1) 2 像素的滑动窗口的中值滤波器删除宽度小于 h 像素的每个条纹。它还删除了矩形每个角上大约 2 个 h 像素的三角形部分。更重要的是,如果条纹之间的空间宽度也等于 h ,它会反转包含宽度为 h 的一些平行条纹的图像的一部分(比较图 2-6 和图 2-7 )。如果读者注意到 median 根据多数做出决策,这就很容易理解了:如果滑动窗口中的大多数像素都是暗的,那么中心像素就会变暗。
图 2-7
中值为 5 × 5 像素的滤波后的相同图像
图 2-6
原始图像和 5 × 5 像素的滑动窗口
也不推荐使用中值来抑制脉冲噪声,因为这将删除与噪声无关的细线形状的对象。我在后面的章节中建议了一个有效的方法。
适马滤波器:最有效的一种
西格玛滤波器以与平均滤波器相同的方式减少噪声:通过平均许多灰度值或颜色。西格玛滤波器的思想是仅对滑动窗口中与中心像素的强度相差不超过固定参数容差的那些强度(即颜色通道的灰度值或强度)进行平均。根据这一思想,西格玛滤波器减少了高斯噪声,并保留了图像中的边缘不模糊。
西格玛过滤器是由 John-Sen Lee (1983)提出的。然而,直到最近,它仍然鲜为人知:据我所知,没有任何图像处理教科书提到过它。在一篇专业论文中只提到过一次 Chochia (1984)。
托马西和曼杜奇(1998)提出了一种类似于西格玛滤波器的滤波器,他们称之为双边滤波器 。他们建议为被平均的颜色分配两种权重:域权重随着平均像素与滑动窗口的中心像素的距离增加而变小,范围权重随着被平均的像素和中心像素的颜色强度之间的差异增加而变小。两个权重都可以定义为高斯分布的密度。该滤波器工作良好:它减少了高斯噪声,并保留了边缘的清晰度。然而,它本质上比 sigma 滤波器慢;例如,处理 2500 × 3500 像素的彩色图像,双边滤波器需要 30 秒,而最简单的 sigma 滤波器只需要 7 秒。因此,双边滤波器大约比 sigma 滤波器慢四倍。双边过滤器的作者在参考文献中没有提到 sigma 过滤器。
为了解释为什么 sigma 滤波器工作得如此好,它可以被视为众所周知的期望最大化(EM)算法的第一次迭代。将滑动窗口中的颜色细分成两个子集,接近中心像素颜色的子集和远离中心像素颜色的子集,可以被认为是期望步骤,而接近颜色的平均是最大化步骤。众所周知,EM 算法收敛相当快。因此,单次第一次迭代带来的结果接近滑动窗口中像素子集的平均值,该像素子集的颜色很可能接近中间像素的颜色。
当比较 sigma 滤波器和双边滤波器时,可以注意到 sigma 滤波器使用类似于双边滤波器的算法,其中高斯分布被可以更快计算的更简单的矩形分布代替。
让我们首先展示使用 sigma 过滤器过滤灰度图像的伪代码。我们用Input
表示输入图像,用Output
表示输出图像。让一个坐标为(X, Y)
的像素滑过输入图像。设M
为(X, Y)
处的灰度值。对每个位置(X, Y)
做:
sum=0; number=0; M=Input(X,Y);
Let another pixel (x, y) run through the gliding window with the center of window at (X, Y).
for each pixel Input(x,y) do:
if (abs(Input(x,y) - M) < tolerance)
{ sum+=Input(x,y);
number++;
}
Output(X,Y)=Round(sum / number);
现在让我们展示这个简单解决方案的源代码。我们用加法代替了经常重复的乘法,使这个方法更快(13%)。
public int SigmaSimpleColor (CImage Inp, int hWind, int Toleranz)
// The sigma filter for 3 byte color images.
{
int[] gvMin = new int[3], gvMax = new int[3], nPixel = new int[3];
int [] Sum = new int[3];
int c, hWind3 = hWind * 3, NX3 = width * 3, x3, xyOut;
N_Bits = Inp.N_Bits; // "N_Bits is an element of the class CImage
for (int y = xyOut = 0; y < height; y++) // ==============================
{
int y1, yStart = Math.Max(y - hWind, 0) * NX3,
yEnd = Math.Min(y + hWind, height - 1) * NX3;
for (int x = x3 = 0; x < width; x++, x3+=3, xyOut+=3) //=================
{
int x1, xStart = Math.Max(x3 - hWind3, 0),
xEnd = Math.Min(x3 + hWind3, NX3 - 3);
for (c=0; c<3; c++)
{
Sum[c] = 0;
nPixel[c] = 0;
gvMin[c] = Math.Max(0, Inp.Grid[c + xyOut] - Toleranz);
gvMax[c] = Math.Min(255, Inp.Grid[c + xyOut]+Toleranz);
}
for (y1 = yStart; y1 <= yEnd; y1 += NX3)
for (x1 = xStart; x1 <= xEnd; x1 += 3)
for (c = 0; c < 3; c++)
{
if (Inp.Grid[c+x1+y1]>=gvMin[c] && Inp.Grid[c+x1+y1]<=gvMax[c])
{ Sum[c]+=Inp.Grid[c+x1+y1];
nPixel[c]++;
}
}
for (c = 0; c < 3; c++) //======================================
{ if (nPixel[c] > 0) Grid[c+xyOut] = (byte)((Sum[c] + nPixel[c]/2)/nPixel[c]);
else Grid[c+xyOut]=0;
} //================= end for (c... ===========================
} //================== end for (int x... ==========================
} //=================== end for (int y... ===========================
return 1;
} //********************** end SigmaSimpleColor *************************
这种解决方案工作得很好,但是如果滑动窗口的大小大于 5 × 5 像素,则它相当慢:对于灰度图像,它需要大约每像素 OPP = 4 **W2 操作,或者对于彩色图像,它需要 9 **W2 操作。在大多数实际情况下,使用窗口大小为 3 × 3 或 5 × 5 像素的 sigma 滤波器就足够了。因此SigmaSimpleColor
几乎可以在任何地方使用。
稍后我们将看到更快版本的 sigma 滤波器。不幸的是,在这种情况下不可能应用快速平均滤波器中使用的方法,因为该过程是非线性的。由于使用了局部直方图,该过程可以变得更快。直方图是一个数组,其中每个元素包含窗口中相应灰度值或颜色强度的出现次数。西格玛滤波器通过更新过程计算窗口每个位置的直方图:窗口右边界的垂直列中的灰度值或颜色强度用于增加直方图的相应值,而左边界的值用于减少它们。
设 OPP 是每个像素的运算次数。23 W 是实现直方图所需的运算次数,23(2tol+1)是计算直方图的 3(2tol+1)个值和相应的像素数所需的运算次数。这样总的 OPP = 23* W +23(2* tol +1)。
这是西格玛过滤器的源代码,带有彩色图像的局部直方图。
public int SigmaColor(CImage Inp, int hWind, int Toleranz, Form1 fm1)
// The sigma filter for color images with 3 bytes per pixel.
{
int gv, y1, yEnd, yStart;
int[] gvMin = new int[3], gvMax = new int[3], nPixel = new int[3];
int Sum = new int[3];
int[,] hist = new int[256, 3];
int c;
for (y1 = 0; y1 < 256; y1++) for (c = 0; c < 3; c++) hist[y1, c] = 0;
N_Bits = Inp.N_Bits;
fm1.progressBar1.Value = 0;
int yy = 5 + nLoop * height / denomProg;
for (int y = 0; y < height; y++) //=====================================
{
if ((y % yy) == 0) fm1.progressBar1.PerformStep();
yStart = Math.Max(y - hWind, 0);
yEnd = Math.Min(y + hWind, height - 1);
for (int x = 0; x < width; x++) //====================================
{
for (c = 0; c < 3; c++) //========================================
{
if (x == 0) //-------------------------------------------
{
for (gv = 0; gv < 256; gv++) hist[gv,c] = 0;
for (y1 = yStart; y1 <= yEnd; y1++)
for (int xx = 0; xx <= hWind; xx++)
hist[Inp.Grid[c + 3 * (xx + y1 * width)], c]++;
}
else
{
int x1 = x + hWind, x2 = x - hWind - 1;
if (x1 < width - 1)
for (y1 = yStart; y1 <= yEnd; y1++)
hist[Inp.Grid[c + 3 * (x1 + y1 * width)], c]++;
if (x2 >= 0)
for (y1 = yStart; y1 <= yEnd; y1++)
{
hist[Inp.Grid[c + 3 * (x2 + y1 * width)], c]--;
if (hist[Inp.Grid[c + 3 * (x2 + y1 * width)], c] < 0) return -1;
}
} //---------------- end if (x==0) ------------------------
Sum[c] = 0; nPixel[c] = 0;
gvMin[c] = Math.Max(0, Inp.Grid[c + 3 * x + 3 * width * y] - Toleranz);
gvMax[c] = Math.Min(255, Inp.Grid[c + 3 * x + 3 * width * y] + Toleranz);
for (gv = gvMin[c]; gv <= gvMax[c]; gv++)
{
Sum[c] += gv * hist[gv, c]; nPixel[c] += hist[gv, c];
}
if (nPixel[c] > 0)
Grid[c + 3 * x + 3 * width * y] = (byte)((Sum[c] + nPixel[c] / 2) / nPixel[c]);
else Grid[c + 3 * x + 3 * width * y] = Inp.Grid[c + 3 * x + 3 * width * y];
} //================ end for (c... ============================
} //================= end for (int x... ===========================
} //================== end for (int y... ============================
return 1;
} //********************** end SigmaColor ******************************
如果滑动窗口的宽度小于 7(hWind
小于 3),方法SigmaSimpleColor
比SigmaColor
快。宽度值越大SigmaColor
越快。SigmaColor
的工作时间随滑动窗口的宽度变化相当缓慢。参见表 2-2 。
表 2-2
1200 × 1600 像素彩色图像的工作时间(秒)
| |hWind
|
| --- | --- |
| 方法 | one | Two | three | six |
| SigmaSimpleColor
| Zero point seven one | One point six eight | Three point one one | Ten point two six |
| SigmaColor
| Two point four three | Two point five five | Two point six three | Three point zero two |
本书中描述的几乎所有项目都使用了方法SigmaSimpleColor
和灰度图像的类似方法。
脉冲噪声的抑制
正如已经指出的,脉冲噪声影响单个的、偶尔选择的像素或相邻像素的小组,而不是图像的所有像素。后者是高斯噪声的特征。我们区分暗脉冲噪声和亮脉冲噪声。暗噪声包含亮度低于其邻域亮度的像素或像素组,而在亮噪声的情况下,像素的亮度高于其邻域的亮度。
抑制灰度或彩色图像中的脉冲噪声的问题相当复杂:在暗噪声的情况下,需要自动检测满足以下条件的像素的所有子集 S :
-
子集 S 的所有像素必须具有低于或等于阈值 T 的亮度。
-
子集 S 被连接。
-
像素数(即 S 的面积)低于预定值 M 。
-
不属于 S 但是与 S 的像素相邻的所有像素必须具有高于 T 的亮度。
-
对于不同的子集 S ,阈值 T 可以不同。
这个问题很难,因为阈值 T 是未知的。理论上,在一个接一个地测试 T 的所有可能值的同时解决问题是可能的。然而,这样的算法会非常慢。
米(meter 的缩写))I. Schlesinger (1997 年)提出了以下快速解决方案的想法:
我们提出了这样一个过程,即在二进制图像中,利用后续阈值处理的“噪声去除”给出了与利用后续常用噪声去除的阈值处理相同的结果。这种等价适用于每个阈值的值。
项目
PulseNoise
的算法由四个步骤组成:(1)按照像素亮度的递增顺序对像素进行排序,(2)去除白噪声,(3)按照像素亮度的递减顺序对像素进行排序,以及(4)去除黑噪声。
使用长度等于不同亮度数量的附加阵列,在像素的双重扫描期间完成像素的排序。
在有序像素的单倍扫描期间进行白噪声去除。设 t i 为亮度为 v 的像素(t i )。像素的处理包括检查是否存在包含该像素的白噪声区域 G。最初该区域只包含 t i 。然后该区域增长,使得如果(1)t’是区域 G 的邻居,则某个像素 t’被包括在其中;以及(2)v(t ')≥v(tI)。区域增长,直到满足以下条件之一:
1。所获得的区域 G 的尺寸超过了预定尺寸。在这种情况下,区域 G 不被认为是噪声,并且下一个像素 t (i+1) 被处理。
2。没有像素可以包含在 G 中。在这种情况下,G 是一个噪声,其所有像素的亮度等于相邻像素的最大亮度。
去除黑噪声类似于去除白噪声。"
下面是我们在 Windows 窗体项目PulseNoiseWF
中实现该算法的详细描述和源代码。我们做了一些无关紧要的改变:我们使用亮度直方图histo[256]
并定义了一个二维数组Index[256][*]
,而不是根据亮度对像素进行排序,这太慢了。该阵列包含每个亮度值light
的不同数量的像素索引(x + Width*y)
的Index[light][histo[light]]
。值(x + Width*y)
是坐标为(x,y)的像素的索引,通过该索引可以在图像中找到该像素。因此,例如,值Index[light][10]
是亮度等于light
的所有像素的集合中的第十个像素的索引。当从小亮度开始读取数组Index
时,我们获得以亮度增加的顺序排序的像素的索引,但是当以相反的方向读取时,我们获得以亮度减少的顺序排序的像素的索引。
该项目包含一个文件CImage.cs
,该文件定义了包含用于预处理图像的方法的类CImage
。这些方法处理CImage
类的对象,这比在 Windows 窗体中处理位图更容易也更快。
该项目还包含文件CPnoise.cs
,其类CPnoise
包含实现脉冲噪声抑制的方法。还有一个包含类Queue
的小文件Queue.cs
,它定义了实现众所周知的结构Queue
的简单方法。队列是先进先出(FIFO)的数据结构。在 FIFO 数据结构中,添加到队列中的第一个元素将是第一个被移除的元素。这相当于这样的要求:一旦添加了新元素,在删除新元素之前,必须删除在它之前添加的所有元素。我在下面描述了这个项目的方法。
我们一起考虑灰度和彩色图像的情况,因为这两种情况之间的唯一区别是,在彩色图像的情况下,像素的三个通道(红色、绿色和蓝色)的共同亮度被认为与灰度图像的灰度值相同。我们使用第三章中描述的方法MaxC(byte R, byte G, byte B)
来计算彩色像素的亮度。在这个项目中,我们只在给找到的集合 S 的像素分配颜色的指令中直接使用颜色信息。
项目PulseNoiseWF
显示如图 2-8 所示的表格。当用户单击“打开图像”时,“打开文件对话 1”对话框打开,用户可以选择图像。图像打开,并根据打开图像的像素格式,通过方法BitmapToGrid
或BitmapToGridOld
转换成对象CImage.Orig
。BitmapToGrid
使用BitmapData
类的方法LockBits
,该方法允许直接评估位图的像素;这比使用GetPixel
的标准评估要快得多。这里显示的是BitmapToGrid
的代码。
图 2-8
项目的形式PulseNoiseWF
private void BitmapToGrid(Bitmap bmp, byte[] Grid)
{
Rectangle rect = new Rectangle(0, 0, bmp.Width, bmp.Height);
BitmapData bmpData = bmp.LockBits(rect, ImageLockMode.ReadWrite,
bmp.PixelFormat);
int nbyte;
switch (bmp.PixelFormat)
{
case PixelFormat.Format24bppRgb: nbyte = 3; break;
case PixelFormat.Format8bppIndexed: nbyte = 1; break;
default: MessageBox.Show("Not suitable pixel format=" + bmp.PixelFormat);
return;
}
IntPtr ptr = bmpData.Scan0;
int length = Math.Abs(bmpData.Stride) * bmp.Height;
byte[] rgbValues = new byte[length];
System.Runtime.InteropServices.Marshal.Copy(ptr, rgbValues, 0, length);
progressBar1.Visible = true;
for (int y = 0; y < bmp.Height; y++)
{
int y1 = 1 + bmp.Height / 100;
if (y % y1 == 1) progressBar1.PerformStep();
for (int x = 0; x < bmp.Width; x++)
{
if (nbyte == 1) // nbyte is global according to the PixelFormat of "bmp"
{
Color color = bmp.Palette.Entries[rgbValues[x+Math.Abs(bmpData.Stride)*y]];
Grid[3 * (x + bmp.Width * y) + 0] = color.B;
Grid[3 * (x + bmp.Width * y) + 1] = color.G;
Grid[3 * (x + bmp.Width * y) + 2] = color.R;
}
else
for (int c = 0; c < nbyte; c++)
Grid[c + nbyte * (x + bmp.Width * y)] =
rgbValues[c + nbyte * x + Math.Abs(bmpData.Stride) * y];
}
}
bmp.UnlockBits(bmpData);
progressBar1.Visible = false;
} //*********************** end BitmapToGrid ********************
方法BitmapToGridOld
使用GetPixel
。在 8 位图像的情况下,必须使用颜色表Palette
来定义每个像素的颜色。这在GetPixel
中比在Palette.Entries
中进行得更快,因为在BitmapToGrid
中就是这种情况。因此,我们在 8 位原始图像的情况下使用BitmapToGridOld
。
表格Form1
包含以下定义:
private Bitmap origBmp;
private Bitmap Result; // result of processing
CImage Orig; // copy of original image
CImage Work; // work image
public Point[] v = new Point[20];
// "v" are corners of excluded rectangles
int Number, // number of defined elements "v"
maxNumber = 8;
bool Drawn = false;
图像显示在表单左侧的pictureBox1
中。用户现在可以改变要消除的暗斑和亮斑的最大尺寸(像素数)的值。这可以用带有标签Delete dark
和Delete light
的工具numericUpDown
来完成。
当用户点击脉冲噪声时,会启动相应的程序残端。这是树桩的代码。
private void button3_Click(object sender, EventArgs e) // Impulse noise
{
Work.Copy(Orig); // "Work" is the work image defined outside
int nbyte = 3;
Drawn = true;
Work.DeleteBit0(nbyte);
int maxLight, minLight;
int[] histo = new int[256];
for (int i = 0; i < 256; i++) histo[i] = 0;
int light, index;
int y1 = Work.height / 100;
for (int y = 0; y < Work.height; y++) //==========================
{
if (y % y1 == y1 - 1) progressBar1.PerformStep();
for (int x = 0; x < Work.width; x++) //=======================
{
index = x + y * Work.width; // Index of the pixel (x, y)
light = MaxC(Work.Grid[3 * index+2] & 254,
Work.Grid[3 * index + 1] & 254,
Work.Grid[3 * index + 0] & 254);
if (light < 0) light = 0;
if (light > 255) light = 255;
histo[light]++;
} //=============== end for (int x=1; .. ===================
} //================ end for (int y=1; .. =====================
progressBar1.Visible = false;
for (maxLight = 255; maxLight > 0; maxLight--) if (histo[maxLight] != 0) break;
for (minLight = 0; minLight < 256; minLight++) if (histo[minLight] != 0) break;
CPnoise PN = new CPnoise(histo, 1000, 4000);
PN.Sort(Work, histo, Number, pictureBox1.Width, pictureBox1.Height, this);
progressBar1.Visible = false;
int maxSizeD = 0;
if (textBox1.Text != "") maxSizeD = int.Parse(textBox1.Text);
int maxSizeL = 0;
if (textBox2.Text != "") maxSizeL = int.Parse(textBox2.Text);
PN.DarkNoise(ref Work, minLight, maxLight, maxSizeD, this);
progressBar1.Visible = false;
Work.DeleteBit0(nbyte);
PN.LightNoise(ref Work, minLight, maxLight, maxSizeL, this);
progressBar1.Visible = false;
Result = new Bitmap(origBmp.Width, origBmp.Height,
PixelFormat.Format24bppRgb);
progressBar1.Visible = true;
int i1 = 1 + nbyte * origBmp.Width * origBmp.Height / 100;
for (int i = 0; i < nbyte * origBmp.Width * origBmp.Height; i++)
{
if (i % i1 == 1) progressBar1.PerformStep();
if (Work.Grid[i] == 252 || Work.Grid[i] == 254) Work.Grid[i] = 255;
}
progressBar1.Visible = false;
GridToBitmap(Result, Work.Grid);
pictureBox2.Image = Result;
Graphics g = pictureBox1.CreateGraphics();
Pen myPen = new Pen(Color.Blue);
// Drawing the excluded rectangles
for (int n = 0; n < Number; n += 2)
{
g.DrawLine(myPen, v[n + 1].X, v[n + 0].Y, v[n + 1].X, v[n + 1].Y);
g.DrawLine(myPen, v[n + 0].X, v[n + 0].Y, v[n + 1].X, v[n + 0].Y);
g.DrawLine(myPen, v[n + 0].X, v[n + 0].Y, v[n + 0].X, v[n + 1].Y);
g.DrawLine(myPen, v[n + 0].X, v[n + 1].Y, v[n + 1].X, v[n + 1].Y);
}
progressBar1.Visible = false;
} //**************************** end Impulse noise ***************
stump 调用方法DeleteBit0
,删除图像中所有像素的第 0 位,使得标记已经使用的像素成为可能。然后计算图像亮度的直方图,定义最小亮度MinLight
和最大亮度MaxLight
。
然后调用根据亮度对像素进行分类的方法Sort
。没有必要通过已知的分类算法对像素进行分类,因为这将花费太多时间。相反,我们的方法Sort
获得图像亮度的直方图histo[256]
,并填充索引的二维数组Index[256][ ]
。Index[light]
的论点light
是像素的明度。子数组Index[light][ ]
是一个索引数组,其中nPixel[light]
是具有亮度light
的像素的数量。值nPixel[light]
等于histo[light]
。子阵列Index[light][ ]
的每个元素是一个坐标为(x, y)
的像素的索引,该像素的亮度等于light
。这里x
是一列的编号,y
是一行的编号,width
是图像中的列数。方法Sort
保存第i
个像素的索引ind=x+ width*y
,其亮度light
和坐标(x, y)
在Index[light][i]
中。因此,图像的所有像素按照亮度的递增顺序进行排序。如果需要降序,可以反方向读取数组Index
。下面是定义类CPnoise
及其方法的源代码。我们在类CPnoise
的定义之外描述了这些方法并展示了它们的代码,尽管是根据 C#的规定。他们应该在里面。这种方法更方便,因为我们可以在方法代码附近找到方法的描述。
class CPnoise
{ unsafe
public int[][] Index; // saving indexes of all pixels ordered by lightness
int[] Comp; // contains indexes of pixels of the connected subset
int[] nPixel; // number of pixels with certain lightness
int MaxSize;// admissible size of the connected subset
Queue Q1;
unsafe
public CPnoise(int[] Histo, int Qlength, int Size) // Constructor
{
this.maxSize = Size;
this.Q1 = new Queue(Qlength);//necessary to find connected subsets
this.Comp = new int[MaxSize];
this.nPixel=new int[256];//256 is the number of lightness values
for (int light = 0; light < 256; light++) nPixel[light] = 0;
Index = new int[256][];
for (int light = 0; light < 256; light++)
Index[light] = new int[Histo[light] + 1];
} //************************ end of Constructor ******************
正如我们在前面的代码button3_Click
中看到的,方法Sort
在直方图计算之后被调用。它将图像所有像素的索引插入到索引的二维数组Index[256][ ]
中。应该消除脉冲噪声的图像有时包含不是噪声并且不应该被消除的小斑点。例如,图像中人的眼睛有时是小而暗的斑点,其大小类似于暗噪声的斑点的大小。为了防止它们被消除,我们开发了一个简单的程序:用户应该用鼠标在输入图像中画出一些矩形,包括不应该被消除的位置。方法Sort
获得这些矩形的参数v[ ]
,并跳过将这些矩形内的像素插入到数组Index[256][ ]
中。为此,采用简单的方法getCond
,该方法使用Form1
中定义的Points
的数组v[ ]
。这里没有给出代码。下面是方法Sort
的源代码。
public int Sort(CImage Image, int[] histo, int Number, int picBox1Width,
int picBox1Height, Form1 fm1)
{
int light, i;
double ScaleX = (double)picBox1Width / (double)Image.width;
double ScaleY = (double)picBox1Height / (double)Image.height;
double Scale; // Scale of the presentation of the image in "pictureBox1"
if (ScaleX < ScaleY) Scale = ScaleX;
else Scale = ScaleY;
bool COLOR;
if (Image.nBits == 24) COLOR = true;
else COLOR = false;
double marginX = (double)(picBox1Width - Scale *Image.width)*0.5; // left of image
double marginY=(double)(picBox1Height - Scale*Image.height)*0.5; // above image
bool Condition = false; // Skipping pixel (x, y) if it lies in a global rectangles "fm1.v"
fm1.progressBar1.Value = 0;
fm1.progressBar1.Step = 1;
fm1.progressBar1.Visible = true;
fm1.progressBar1.Maximum = 100;
for (light = 0; light < 256; light++) nPixel[light] = 0;
for (light = 0; light < 256; light++)
for (int light1 = 0; light1 < histo[light]; light1++) Index[light][light1] = 0;
int y1 = Image.height / 100;
for (int y = 1; y < Image.height; y++) //=================================
{
if (y % y1 == y1 - 1) fm1.progressBar1.PerformStep();
for (int x = 1; x < Image.width; x++) //================================
{
Condition = false;
for (int k = 0; k < Number; k += 2)
Condition = Condition || getCond(k, x, y, marginX, marginY, Scale, fm1);
if (Condition) continue;
i = x + y * Image.width; // Index of the pixel (x, y)
if (COLOR)
light = MaxC(Image.Grid[3 * i+2] & 254, Image.Grid[3 * i + 1] & 254,
Image.Grid[3 * i + 0] & 254);
else light = Image.Grid[i] & 252;
if (light < 0) light = 0;
if (light > 255) light = 255;
Index[light][nPixel[light]] = i; // record of index "i" of a pixel with lightness "light"
if (nPixel[light] < histo[light]) nPixel[light]++;
} //============== end for (int x=1; .. =============================
} //=============== end for (int y=1; .. ==============================
fm1.progressBar1.Visible = false;
return 1;
} //***************** end Sort *****************************************
方法Sort
读取图像的所有像素,并用像素的索引填充二维数组Index[256][ ]
。这个数组由方法DarkNoise
和LightNoise
使用。
我们首先考虑暗噪声的情况。方法DarkNoise
包含一个带有变量light
的for
循环,它从最大亮度减 2 开始扫描light
的值,直到最小亮度minLight
。这个循环包含另一个带有变量i
的循环,它从 0 到nPixel[light]
扫描二维数组Index[light][i]
的第二个索引的值。它从数组Index[light][i]
中读取像素的索引,并测试具有该索引的像素是否具有等于light
的亮度以及其标签是否等于零。如果两个条件都满足,那么对索引为Index[light][i]
的像素调用方法BreadthFirst_D
。该方法构建亮度小于或等于light
的像素的连通子集,包含索引为Index[light][i]
的起始像素。
下面是DarkNoise
的源代码:
public int DarkNoise(ref CImage Image, int minLight, int maxLight, int maxSize, Form1 fm1)
{
bool COLOR = (Image.nBits == 24);
int ind3 = 0, // index multiplied with 3
Label2, Lum, rv = 0;
if (maxSize == 0) return 0;
fm1.progressBar1.Maximum = 100;
fm1.progressBar1.Step = 1;
fm1.progressBar1.Value = 0;
fm1.progressBar1.Visible = false;
int bri1 = 2;
fm1.progressBar1.Visible = true;
for (int light = maxLight - 2; light >= minLight; light--) //==============
{
if ((light % bri1) == 1) fm1.progressBar1.PerformStep();
for (int i = 0; i < nPixel[light]; i++) //=============================
{
ind3 = 3 * Index[light][i];
if (COLOR)
{
Label2 = Image.Grid[2 + ind3] & 1;
Lum = MaxC(Image.Grid[2 + ind3] & 254, Image.Grid[1 + ind3] & 254,
Image.Grid[0 + ind3] & 254);
}
else
{
Label2 = Image.Grid[Index[light][i]] & 2;
Lum = Image.Grid[Index[light][i]] & 252;
}
if (Lum == light && Label2 == 0)
{
rv = BreadthFirst_D(ref Image, i, light, maxSize);
}
} //================= end for (int i.. ============================
} //================== end for (int light.. ==========================
fm1.progressBar1.Visible = false;
return rv;
} //*********************** end DarkNoise ******************************
方法DarkNoise
调用方法BreadthFirst_D( )
,将值Image
、i
、光和maxSize
作为其参数。maxSize
的值是所寻找的亮度小于或等于light
的连通像素组中的最大允许像素数。该方法实现了已知的宽度优先搜索算法,该算法被设计来标记树结构的所有顶点。亮度小于或等于light
的所有像素的连接集合是一棵树。下面是BreadthFirst_D( )
的代码:
private int BreadthFirst_D(ref CImage Image, int i, int light, int maxSize)
{
int lightNeib, lightness of the neighbor
index, Label1, Label2,
maxNeib, // maxNeib is the maximum number of neighbors of a pixel
Neib, // the index of a neighbor
nextIndex, // index of the next pixel in the queue
numbPix; // number of pixel indexes in "Comp"
bool small; // equals "true" if the subset is less than "maxSize"
bool COLOR = (Image.nBits == 24);
index = Index[light][i];
int[] MinBound = new int[3]; // color of pixel with min. lightness near the subset
for (int c = 0; c < 3; c++) MinBound[c] = 300; // a value out of [0, 255]
for (int p = 0; p < MaxSize; p++) Comp[p] = -1; // MaxSize is element of the class
numbPix = 0;
maxNeib = 8; // maximum number of neighbors
small = true;
Comp[numbPix] = index;
numbPix++;
if (COLOR)
Image.Grid[1 + 3 * index] |= 1; // Labeling as in Comp (Label1)
else
Image.Grid[index] |= 1; // Labeling as in Comp
Q1.input = Q1.output = 0;
Q1.Put(index); // putting index into the queue
while (Q1.Empty() == 0) //= loop running while queue not empty ============
{
nextIndex = Q1.Get();
for (int n = 0; n <= maxNeib; n++) // == all neighbors of "nextIndex"
{
Neib = Neighb(Image, nextIndex, n); // the index of the nth neighbor of nextIndex
if (Neib < 0) continue; // Neib < 0 means outside the image
if (COLOR)
{
Label1 = Image.Grid[1 + 3 * Neib] & 1;
Label2 = Image.Grid[2 + 3 * Neib] & 1;
lightNeib = MaxC(Image.Grid[2 + 3 * Comp[m]],
Image.Grid[1 + 3 * Comp[m]], Image.Grid[0 + 3 * Comp[m]]) & 254;
}
else
{
Label1 = Image.Grid[Neib] & 1;
Label2 = Image.Grid[Neib] & 2;
lightNeib = Image.Grid[Neib] & 252; // MaskGV;
}
if (lightNeib == light && Label2 > 0) small = false;
if (lightNeib <= light) //-------------------------------------------
{
if (Label1 > 0) continue;
Comp[numbPix] = Neib; // putting the element with index Neib into Comp
numbPix++;
if (COLOR)
Image.Grid[1 + 3 * Neib] |= 1; // Labeling with "1" as in Comp
else
Image.Grid[Neib] |= 1; // Labeling with "1" as in Comp
if (numbPix > maxSize) // Very important condition
{
small = false;
break;
}
Q1.Put(Neib);
}
else // lightNeib<light
{
if (Neib != index) //----------------------------------------------
{
if (COLOR)
{
if (lightNeib < (MinBound[0] + MinBound[1] + MinBound[2]) / 3)
for (int c = 0; c < 3; c++) MinBound[c] = Image.Grid[c + 3 * Neib];
}
else
if (lightNeib < MinBound[0]) MinBound[0] = lightNeib;
} //---------------- end if (Neib!=index) -------------------------
} //---------------- end if (lightNeib<=light) and else ------------
} // ===================== end for (n=0; .. ========================
if (small == false) break;
} // ===================== end while =============================
int lightComp; // lightness of a pixel whose index is contained in "Comp"
for (int m = 0; m < numbPix; m++) //==================================
{
if (small != false && MinBound[0] < 300) //"300" means MinBound not calculated ---
{
if (COLOR)
for (int c = 0; c < 3; c++) Image.Grid[c + 3 * Comp[m]] = (byte)MinBound[c];
else
Image.Grid[Comp[m]] = (byte)MinBound[0];
}
else
{
if (COLOR)
lightComp = MaxC(Image.Grid[3 * Comp[m]],
Image.Grid[1 + 3 * Comp[m]], Image.Grid[2 + 3 * Comp[m]]) & 254;
else
lightComp = Image.Grid[Comp[m]] & 252; // MaskGV;
if (lightComp == light) //-------------------------------------------
{
if (COLOR) Image.Grid[2 + 3 * Comp[m]] |= 1;
else Image.Grid[Comp[m]] |= 2;
}
else // lightComp!=light
{
if (COLOR)
{
Image.Grid[1 + 3 * Comp[m]] &= (byte)254; // deleting label 1
Image.Grid[2 + 3 * Comp[m]] &= (byte)254; // deleting label 2
}
else
Image.Grid[Comp[m]] &= 252; // deleting the labels
} //-------------- end if (lightComp == light) and else--------------
} //-----------------end if (small != false) and else -----------------
} //==================== end for (int m=0 .. ========================
return numbPix;
} //*********************** end BreadthFirst_D ***************************
方法BreadthFirst_D
获得阵列Index[light][i]
中像素 P 的亮度light
和数量i
作为参数。该方法的任务是找到亮度小于或等于light
并包含像素 p 的像素的连通集合 S ,如果该集合包含小于或等于参数maxSize
的像素数量numbPix
,则 S 的像素获得较亮的颜色,并作为暗像素变得不可见。否则,S 的所有像素获得一个标签,该标签指示这些像素具有亮度light
并且属于多于maxSize
个像素的连通子集。
该方法从Index[light][i]
中取出index
,将其放入队列Q
中,并且只要Q
不为空,就开始一个while
循环运行。在循环中,从队列Q
中取出值nextIndex
,并且测试其索引为Neib
的八个邻居中的每一个的亮度是否小于或等于light
。除此之外Neib
的标签也测试过。
我们使用Image.Grid
中像素的最低有效位来标记像素。在灰度图像的情况下,我们使用两个最低有效位:位 0 是LabelQ1
,位 1 是LabelBig2
。在彩色图像的情况下,我们使用绿色通道的一位作为LabelQ1
,红色通道的一位作为LabelBig2
。
如果索引已经放入队列,则设置LabelQ1
。如果像素属于大于maxSize
的像素的连通大集合 S 并且像素的亮度等于light
,则LabelBig2
被设置。
如果索引为Neib
的像素的LabelQ1
没有设置,并且其亮度小于或等于light
,那么索引Neib
被保存在包含所有亮度小于或等于light
的像素的索引的数组Comp
中。然后索引Neib
被放入队列,像素获得标签LabelQ1
。然而,如果Neib
的亮度大于light
,该亮度将用于计算颜色MinBound
,该颜色用于填充要变亮的一小组 S 的所有像素。
这两个标签对于使方法BreadthFirst_D
更快都很重要:一个设置了LabelQ1
的像素不能被再次输入到队列中,一个设置了LabelBig2
的像素表示包含该像素的集合 S 不小。方法DarkNoise
不为标有LabelBig2
的像素调用方法BrightFirst_D
。
在BrightFirst_D
的while
循环中,从队列中取出索引nextIndex
作为它的下一个元素。nextIndex
的所有八个邻居的索引Neib
通过方法Neighb(Image, nextIndex, n)
( n
从 0 到 7)依次传递。如果邻居Neib
位于图像之外(nextIndex
位于图像的边界),则方法Neighb
返回负值,并且Neib
将被忽略。否则Neib
的亮度lightNeb
取自Image
。如果lightNeb
大于light
,则Neib
不属于组成比light
更暗或与light
同样暗的像素的连通集合(分量)的像素集合 S 。值lightNeb
然后用于计算颜色MinBound
,该颜色是与 S 相邻的像素中亮度最小的颜色。
如果lightNeb
等于light
并且像素Neib
设置了LabelBig2
,则逻辑变量small
设置为false
。否则,如果lightNeb
大于等于light
,有两种情况:在Neib
设置了LabelQ1
的情况下,忽略像素Neib
;否则,将已经包含在搜索集合 S 中的像素数量numbPix
与maxSize
进行比较。如果大于maxSize
,那么变量small
被设置为false
并且while
循环被中断。然而,如果numbPix
小于或等于maxSize
,那么Neib
被包含到集合 S ( Neib
被保存在数组Comp
中),像素Neib
得到LabelQ1
集合,Neib
被放入队列。
在while
循环结束后,正在检查small
的值。如果为真,那么索引保存在数组Comp
中的 S 的所有像素得到颜色MinBound
。否则亮度等于light
的Comp
的像素得到标签LabelBig2
。 S 的所有其他像素没有标签。该方法返回值numbPix
。
方法BreadthFirst_L
与BreadthFirst_D
类似。区别主要在于互换了一些>和<操作符(而不是指令if (numbPix > maxSise) {...}
)。方法BreadthFirst_L
被方法LightNoise
调用,类似于DarkNoise
。方法LightNoise
从最低亮度开始读取数组Index
并调用方法BreadthFirst_L
。
所有方法都适用于灰度和彩色图像。这是由于采用了方法MaxC(Red, Green, Blue)
(参见第三章中的解释),该方法根据以下简单公式计算颜色为(Red, Green, Blue)
的像素的亮度:
明度= max(0.713* R,G ,0.527* B )。
读者可以在图 2-9 中看到一个将该项目应用于德国著名女画家 Louise Seidler 的一幅绘画作品的老照片的例子。
图 2-9
将项目PulseNoiseWF
应用于 Louise Seidler 的一幅旧照片的例子(左)。小于 380 像素的暗点被移除(右图)。
如第四章和第五章所述,当我们使用阴影校正后的脉冲噪声抑制时,代表图纸旧照片的灰度图像的结果要好得多。图 2-9 中的图像也将在应用阴影校正方法后看起来更好。
图 2-10 是另一个有轻噪点的图像的例子。这是路易丝·塞德勒的油画《耶稣和孩子们》的照片片段。
图 2-10
将项目PulseNoiseWF
应用于路易丝·塞德勒(左)的一张照片片段的示例。小于 30 像素的光斑被去除(右图)。**
三、对比度增强
我们将数字图像的对比度定义如下:
CIP=(李 max - 李min)/域
其中李 max 为最大值李 min 为图像中的最小明度域为明度的域或差值的最大可能值李max-李 min 。
刚刚定义的对比度度量显然取决于单个像素的亮度。为了获得更鲁棒的测量,有必要知道亮度值的频率:出现在少量像素中的值并不重要。频率包含在图像的直方图中。可以找到这样的亮度值MinInp
和MaxInp
,亮度低于MinInp
和亮度高于MaxInp
的像素数量小于预定值,我们称之为丢弃区域(因为像素数量是面积的度量)。则鲁棒对比度度量为
cr=(MaxInp–MinInp)/Domain。
自动线性对比度增强
大多数对比度增强程序,如 IrfanView,通过中间部分导数大于 1 的函数,使用图像中一组亮度值到另一组亮度值的映射(见图 3-1 )。
图 3-1
在其他程序中用于增加对比度的函数的图形示例
这里提出了另一种基于研究图像直方图的方法。我们算法的思想在于将图像中最低和最高亮度之间的间隔映射到整个间隔上;比如到[0, 255]
上。重要的是要注意,具有极低或极高值的少量像素可以从本质上影响映射的结果。
因此,有必要从映射中排除具有极值的像素的预定部分(例如,1%)。为此,需要找到值MinInp
和MaxInp
,使得亮度小于MinInp
和亮度大于MaxInp
的像素数量小于图像中像素总数的 1%。
为了增加图像的对比度,我们必须增加最大和最小亮度之间的差异。这可以通过减少图像中亮度的小值并增加较大的值来实现。为了获得 1 的最大可能对比度,有必要以这样的方式变换亮度值,即用 0 代替MinInp
和用 255 代替MaxInp
,并且中间值单调变化;即,它们的顺序保持不变。
可以通过自动程序增加对比度。用户必须仅以图像大小的百分比来定义丢弃区域的值。建议使用查找表(LUT)来加快输出亮度的计算。LUT 是一个数组,包含尽可能多的输入值;例如 256 或从 0 到 255。LUT 的每个元素都包含预先计算的输出值。因此,只计算 256 次输出值就足够了,而不是宽度×高度的倍数,尽管宽度×高度的数值可能高达数千甚至数百万。从数组中获取一个值比计算这个值花费的时间要少得多。
也容易降低对比度。当计算 LUT 时,设置以下值就足够了:MinInp=0
、MaxInp=255
、MinOut>0
和MaxOut<255
。大幅降低对比度,然后自动提高,这是一个有趣的实验!
增加对比度的最简单方法(见图 3-2 )是通过以下分段线性变换:
图 3-2
最简单的分段线性对比度增强
if (Linp <= MinInp) Lout = 0;
else if (Linp >= MaxInp) Lout = 255;
else Lout = 255*(Linp - MinInp)/(MaxInp – MinInp);
这种非常简单的方法产生了非常好的结果,但对于包含大面积黑暗或大面积明亮区域的图像来说却不是这样。在这种情况下,当采用直方图均衡化方法时,有可能获得更好的结果,这将在下一节中解释。
直方图均衡
一些图像包含较大的低对比度暗区(参见图 3-3a )。先前描述的最简单的分段线性方法均匀地拉伸亮度值,而对于这样的图像,暗区域中的低值必须比其他区域中的值更强烈地分开。这种变换不能用最简单的分段线性方法来完成。
图 3-3
对比度变化的例子:(a)带直方图的原图;(b)直方图对比度降低;(c)线性对比度增强;直方图均衡化
增加这种图像的对比度的一个众所周知的想法是直方图均衡化 : 有必要以这样一种方式转换亮度值,使得所有值获得几乎相同的频率。为了实现这一点,具有相对较高频率的灰度值 inp 1 的像素(图 3-4a )必须被许多具有较低频率的不同灰度值的像素替换。这些灰度值 out 1 、 out 2 等等必须组成一个相邻值的序列(图 3-4b )。没有已知的方法来决定这些像素中的哪些必须获得某个值。幸运的是,这不是必须的:以平均频率保持不变的方式,用具有更大差值的其他值替换原始灰度值就足够了(图 3-4c )。这可以通过直方图均衡化来实现。
图 3-4
变换直方图以使频率或平均频率恒定:(a)具有大频率的直方图部分;(b)具有高频率的灰度值被许多具有较低频率的值代替;(c)具有使平均频率恒定的较大差值的值
为了实现直方图均衡,有必要计算累积直方图。这是一个有 256 个元素的整数数组。对应于亮度值 L 的元素包含亮度值小于或等于 L 的像素数量。为了计算 L 的累积直方图的值,将小于或等于 L 的所有值的常用直方图的值相加就足够了:
直方图均衡化的 LUT 可以通过累积直方图来计算。大多数关于图像处理的教科书建议对 LUT 的元素使用以下公式:
eq[l= 255cum[l]/max cum;(3.1)
其中 MaxCum 是累积直方图的最大值,显然等于图像的尺寸宽度*高度。
然而,当使用该公式时,如果原始图像包含大量灰度值等于 gMin,的像素,这是直方图大于 0 的最小灰度值,则会出现困难。在这种情况下,通过根据等式 3.1 计算的 LUT 变换的图像可能非常差,因为许多像素具有与比 0 大得多的和 [ gMin ]成比例的大灰度值。这种转换的一个例子如图 3-5b 所示。
图 3-5
直方图均衡的例子:(a)原始图像;(b)根据等式 3.1 用 LUT 变换;(c)根据等式 3.2 用 LUT 转换
为了解决这个问题,设置 LUT 等式的第 L 个元素的值与差值Cum[L–MinHist成比例就足够了,而不是与 Cum [ L 成比例,其中MinHist=Cum[gMin]=LUT 的元素必须根据以下公式计算:
如果 L 小于或等于 gMin ,则等式[L]= 0;其他
eq[l]= 255(cum**l-minorist)/max cum
其中 MaxCum= width*height 为 Cum [ L 的最大值。
图 [3-5c 显示了根据等式 3.2 计算的 LUT 的变换示例。
直方图均衡化的结果并不总是令人满意的。因此,合理的做法是使用分段线性方法与均衡方法的组合,称为混合方法(见图 3-6 ):
作战[【l】]=【w】**【lut】[]+(3.3)
其中 LUT L 为分段线性 LUT,权重 W 1 和 W 2 可以选择为和等于 1 的非负刹车。图 [3-6c 显示了使用 CombiLUT 与 W 1 = 0.2 和 W 2 = 0.8 的结果。
图 3-6
组合对比度增强:(a)原始图像;分段线性的;(c)直方图均衡;混合方法(20%)
测量彩色图像的亮度
为了定义彩色图像的对比度,需要计算每个像素的亮度或明度。在文献中,彩色像素的亮度有许多不同的定义。维基百科在论文“HSL 和 HSV”中提出了“明度”的不同定义;例如:
I = (R+G+B)/3 或
V = max(R,G,B)或
L = (max(R,G,B)+min(R,G,B))/2 或
Y = 0.30R + 0.59G + 0.11B
其中 R 、 G、和 B 分别是红色、绿色和蓝色通道的强度。作者开发了一个项目WFluminance
来实验测试不同的亮度定义。
该项目在表单的左侧显示了八个不同颜色的矩形(图 3-7 ,并提供了通过相应的工具numericUpDown
改变每个矩形的亮度而不改变其色调的可能性。每个彩色矩形的亮度已经改变,直到所有矩形看起来都具有相同的视觉上可接受的亮度,等于左边灰色矩形的亮度。众所周知,灰色具有彼此相等的所有三个颜色通道的强度:R = G = B。因此,可以假设灰色的亮度等于该恒定强度。
图 3-7
视觉亮度恒定的矩形
该项目根据不同的定义计算每个矩形的亮度值。结果如表 3-1 所示。从表 3-1 中可以看出,对于图 3-7 的所有八个矩形,MC 的值几乎是恒定的,看起来它们具有恒定的亮度。如图 3-7 右侧所示,其他颜色的 MC 值也测试成功,代表视觉亮度相等的矩形对。
表 3-1
根据不同定义的亮度值
|矩形
|
颜色
|
稀有
|
G
|
B
|
M
|
L
|
Y
|
主持人
|
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
| one | 蓝色 | Zero | Zero | Two hundred and forty-two | Two hundred and forty-two | One hundred and twenty-one | Seventeen | One hundred and twenty-seven |
| Two | 品红 | One hundred and sixty | Zero | One hundred and sixty | One hundred and sixty | Eighty | Forty-five | One hundred and fourteen |
| three | 红色 | One hundred and seventy-four | Zero | Zero | One hundred and seventy-four | Eighty-seven | Thirty-six | One hundred and twenty-four |
| four | 黄色 | One hundred and twenty-two | One hundred and twenty-two | Zero | One hundred and twenty-two | Sixty-one | One hundred and thirteen | One hundred and twenty-two |
| five | 格林(姓氏);绿色的 | Zero | One hundred and twenty-four | Zero | One hundred and twenty-four | Sixty-two | Eighty-eight | One hundred and twenty-four |
| six | 蓝绿色 | Zero | One hundred and twenty-two | One hundred and twenty-two | One hundred and twenty-two | Sixty-one | Ninety-six | One hundred and twenty-two |
| seven | 蓝色 | Zero | Zero | Two hundred and forty-two | Two hundred and forty-two | One hundred and twenty-one | Seventeen | One hundred and twenty-seven |
| eight | 灰色的 | One hundred and twenty-one | One hundred and twenty-one | One hundred and twenty-one | One hundred and twenty-one | One hundred and twenty-one | One hundred and twenty-one | One hundred and twenty-one |
注:M = max(R,G,B);L = (M + min((R,G,B));y = 0.2126 * R+0.7152 * G+0.0722 * R;MC = max(0.713R,G,0.527B)。
右半部分颜色矩形左侧的文本显示矩形中颜色的 MC 值。项目的用户可以通过改变相应numericUpDown
工具的值来改变相邻灰色矩形的灰度值,直到灰色矩形的视觉亮度等于彩色矩形的视觉亮度。如果numericUpDown
的值近似等于 MC 值,则这证明 MC 方法给出了该颜色亮度的良好估计。如您所见,所有矩形的值大致相等。
有一个问题是彩色像素亮度的精确定义是否重要。此定义用于将彩色图像转换为灰度图像以及阴影校正。我们已经尝试使用不同的方法来计算彩色像素的亮度,同时将不同的彩色图像转换成灰度图像。在大多数情况下,结果并不取决于方法的选择。然而,在图 3-8 所示的例子中,当使用方法 Y(表 3-1 )时,彩色图像的相当大一部分消失了。
我们还测试了阴影校正的结果如何依赖于亮度测量方法的选择。与将彩色图像转换成灰度图像的情况类似,阴影校正的结果大多不取决于该选择。但是,有些图像的阴影校正强烈依赖于该选择。例如,参见图 3-9 中的图像。
图 3-9
阴影校正强烈依赖于亮度测量方法的图像示例;方法 Y 和方法 I
图 3-8
方法 Y 下零件消失的图像示例
考虑到刚才描述的结果,我们决定在任何地方都使用 MC 方法(作为方法MaxC
)。
彩色图像的对比度
为了执行彩色图像的对比度增强,有必要计算亮度的直方图,然后像在灰度图像的情况下那样处理它。用于亮度变换的 LUT 的计算可以以与之前相同的方式进行,或者利用具有丢弃区域的分段线性规则,或者利用直方图均衡化。然而,为了改变颜色通道的强度,应该使用新的过程:需要为每个像素计算转换后的亮度与原始亮度的关系,然后将三个强度 R 、 G、和 B 中的每一个乘以该关系。例如,像素 P 的变换红色通道为:
rt=【ro】**【lit/lio】
其中 RT 和 LiT 是变换后的红色强度和亮度,而 RO 和 LiO 是像素 P 的红色强度和亮度的原始值。以这种方式,所有三种强度可以成比例地改变,并且色调保持不变,由此对比度增加。
手动控制对比度增强
一些照片具有相当不均匀的亮度,这是由被摄物体的不均匀照明引起的。照片的这一缺点可以通过直方图均衡化方法得到很大程度的纠正,直方图均衡化方法通常是一种很好的对比度增强方法。但是,有时需要在查看结果的同时更正 LUT 的内容。
一些建议的图像处理程序(例如,Photoshop)包含允许将 LUT 的图形制作成贝塞尔曲线的工具,贝塞尔曲线是具有连续导数的平滑曲线。然而,这是一个坏主意,因为由 LUT 实现的函数一定不是平滑的。它必须是连续的,更重要的是,单调递增的。贝塞尔曲线并不总是满足最后一个条件。如果 LUT 的函数不是单调递增的,那么得到的图像几乎不会失真。
将 LUT 函数定义为如图 3-10 所示的分段线性单调递增的直线要简单得多。
图 3-10
LUT 的分段线性函数
断点 V1 和 V2 可以通过鼠标点击来定义。转换后的图像必须在断点定义后立即显示。
我们开发了 Windows 窗体项目WFpiecewiseLinear
来实现这个过程。运行项目的形式如图 3-11 所示。
图 3-11
运行项目的形式示例WFpiecewiseLinear
该表单包含两个按钮。当用户单击打开图像时,他或她可以选择一个图像。图像将显示在左边的图片框中。同时,图像的副本出现在右边的图片框中。图像的直方图出现在表单的左下角,蓝色线条显示 LUT 的函数图。用户现在可以点击线附近的两个点。线以这样一种方式改变,即它的断点位于被单击的点上。LUT 相应变化,用户可以观察到右边图片框中图像的选择。一旦用户对图像的外观感到满意,他或她就可以单击保存结果。然后,用户可以选择名称并保存结果图像。保存图像的类型可以是Bmp
或Jpg
。
如前一部分所解释的,为了计算颜色通道的新强度,有必要通过 LUT 计算像素的新亮度,然后将每个旧的颜色强度乘以新亮度,并除以旧亮度。必须对图像的每个像素执行该过程。由于乘法和除法,这花费了太多的时间,并且变换后的图像出现延迟。
为了使变换图像的计算更快,我们开发了一种使用bigLUT
的方法。这是一个颜色通道的新强度的 2 16 = 65,536 个值的 LUT。作为该表的一个参数,我们使用一个整数,其较低值字节是旧的颜色强度,第二个字节是亮度的旧值(不使用字节 3 和 4 ),取自先前准备的原始图像的灰度值版本。新色彩强度的 65,536 个值中的每一个都等于旧色彩强度与 LUT 值的乘积除以旧亮度。这些值在定义bigLUT
时只计算一次。在项目运行期间,只需为每个像素计算自变量。新的强度仅仅是从bigLUT
中读出的。下面是计算bigLUT
的方法代码:
int[] bigLUT = new int[65536];
private void makeBigLUT(int[] LUT)
{ for (int colOld = 0; colOld < 256; colOld++) // old color intensity
{ for (int lightOld = 1; lightOld < 256; lightOld++) //old lightness
{ int colNew = colOld * LUT[lightOld] / lightOld;
int arg = (lightOld << 8) | colOld;
bigLUT[arg] = colNew;
}
}
}
接下来是项目最重要部分的源代码。当单击“打开图像”按钮时,第一部分开始。这个部分打开图像,生成位图origBmp
,定义类CImage
的三个对象,并用origBmp
的内容填充对象origIm
。软件WindowsForms
建议通过GetPixels
的方法访问位图的像素,这个方法比较慢。我们开发了一个更快的方法BitmapToGrid
,它使用了类Bitmap
的方法LockBits
。然而,BitmapToGrid
不应该用于每像素 8 位的索引位图,因为这些位图包含一个调色板,像素值总是由调色板值定义。因此,对于 8 位索引位图,方法BitmapToGrid
变得太慢。对于这样的位图,我们使用带有GetPixel
的标准方法。
此外,我们项目的第一部分使用我们的方法MaxC
计算彩色像素的亮度,用方法ColorToGrayMC
将原始图像origIm
转换成灰度图像origGrayIm
。它还计算origGrayIm
的直方图,绘制pictureBox3
中的直方图,计算包含分段线性曲线(没有对比度增强)的值的标准 LUT,并绘制代表标准 LUT 的函数的线。它还将图像contrastIm
定义为结果图像,通过类似于前面提到的方法BitmapToGrid
的快速方法GridToBitmap
在位图ContrastBmp
中制作其副本,并在pictureBox2
中显示ContrastBmp
。
private void button1_Click(object sender, EventArgs e) // Open image
{
OpenFileDialog openFileDialog1 = new OpenFileDialog();
if (openFileDialog1.ShowDialog() == DialogResult.OK)
{
try
{
origBmp = new Bitmap(openFileDialog1.FileName);
pictureBox1.Image = origBmp;
}
catch (Exception ex)
{
MessageBox.Show("Error: Could not read file from disk. Original error: " +
ex.Message);
}
}
width = origBmp.Width;
height = origBmp.Height;
origIm = new CImage(width, height, 24);
origGrayIm = new CImage(width, height, 8); // grayscale version of origIm
contrastIm = new CImage(width, height, 24);
if (origBmp.PixelFormat == PixelFormat.Format8bppIndexed)
{
progressBar1.Visible = true;
Color color;
int y2 = height / 100 + 1, nbyte = 3;
for (int y = 0; y < height; y++) //=====================
{
if (y % y2 == 1) progressBar1.PerformStep();
for (int x = 0; x < width; x++) //=================
{
int i = x + width * y;
color = origBmp.GetPixel(i % width, i / width);
for (int c = 0; c < nbyte; c++)
{
if (c == 0) origIm.Grid[nbyte * i] = color.B;
if (c == 1) origIm.Grid[nbyte * i + 1] = color.G;
if (c == 2) origIm.Grid[nbyte * i + 2] = color.R;
}
} //========= end for ( int x... =================
} //========== end for ( int y... ==================
progressBar1.Visible = false;
}
else BitmapToGrid(origBmp, origIm.Grid);
// Calculating the histogram:
origGrayIm.ColorToGrayMC(origIm, this);
for (int gv = 0; gv < 256; gv++) histo[gv] = 0;
for (int i = 0; i < width * height; i++) histo[origGrayIm.Grid[i]]++;
MaxHist = 0;
for (int gv = 0; gv < 256; gv++) if (histo[gv] > MaxHist)
MaxHist = histo[gv];
MinGV = 255;
MaxGV = 0;
for (MinGV = 0; MinGV < 256; MinGV++)
if (histo[MinGV] > 0) break;
for (MaxGV = 255; MaxGV >= 0; MaxGV--)
if (histo[MaxGV] > 0) break;
// Drawing the histogram:
Graphics g = pictureBox3.CreateGraphics();
SolidBrush myBrush = new SolidBrush(Color.LightGray);
Rectangle rect = new Rectangle(0, 0, 256, 256);
g.FillRectangle(myBrush, rect);
Pen myPen = new Pen(Color.Red);
Pen greenPen = new Pen(Color.Green);
for (int gv = 0; gv < 256; gv++)
{
int hh = histo[gv]*255/MaxHist;
if (histo[gv] > 0 && hh < 1) hh = 1;
g.DrawLine(myPen, gv, 255, gv, 255 - hh);
if (gv == MinGV || gv == MaxGV)
g.DrawLine(greenPen, gv, 255, gv, 255 - hh);
}
// Calculating the standard LUT:
int[] LUT = new int[256];
int X = (MinGV + MaxGV) / 2;
int Y = 128;
for (int gv = 0; gv < 256; gv++)
{
if (gv <= MinGV) LUT[gv] = 0;
if (gv > MinGV && gv <= X) LUT[gv] = (gv - MinGV) * Y / (X - MinGV);
if (gv > X && gv <= MaxGV) LUT[gv] = Y + (gv - X) * (255 - Y) / (MaxGV - X);
if (gv >= MaxGV) LUT[gv] = 255;
}
// Drawing the standard curve:
int yy = 255;
Pen myPen1 = new Pen(Color.Blue);
g.DrawLine(myPen1, 0, yy, MinGV, yy);
g.DrawLine(myPen1, MinGV, yy, X, yy - Y);
g.DrawLine(myPen1, X, yy - Y, MaxGV, 0);
g.DrawLine(myPen1, MaxGV, 0, yy, 0);
// nbyte = 3; origIm and contrastIm are both 24-bit images
for (int i = 0; i < nbyte * width * height; i++) contrastIm.Grid[i] = (byte)LUT[(int)origIm.Grid[i]];
ContrastBmp = new Bitmap(width, height, PixelFormat.Format24bppRgb);
progressBar1.Visible = true;
GridToBitmap(ContrastBmp, contrastIm.Grid);
pictureBox2.Image = ContrastBmp;
progressBar1.Visible = false;
} //***************** end Open image ***********************************
在第二章中描述了将位图内容快速复制到CImage
类图像的方法BitmapToGrid
。方法GridToBitmap
类似,所以我不在这里介绍。
项目中用于控制LUT
形状的部分允许用户在包含直方图图像的pictureBox3
中点击两次,从而定义 LUT 分段线性曲线的尼克斯点。该部分使用了Form1
中定义的整数变量cntClick
、X1
、Y1
、X2
和Y2
。变量cntClick
对点击进行计数,但是它的值3
立即被1
替换,因此它只能取值1
和2
。
当cntClick
等于1
时,则第一个拐点V1
的坐标X1
和Y1
(图 3-11 )由点击点定义。从直方图的起点到计算点(X1, Y1
)的分段线性曲线的第一个分支。
当cntClick
等于2
时,则定义第二个尼克斯点V2
的坐标X2
和Y2
(图 3-11 )。用户应设置该点,使X2
大于X1
且Y2
大于Y1
。如果碰巧X2
小于X1
,那么X2
被设置为X1 + 1
;类似地Y2
。因此分段线性曲线总是保持单调。计算从点(X1, Y1
)到点(X2, Y2
)的分段线性曲线的第二个分支和从点(X2, Y2
)到直方图终点的第三个分支。然后如前所述计算 LUT bigLUT
,并在pictureBox2
中计算和显示对比图像。
下面是计算 LUT 并同时显示结果图像的部分代码。
private void pictureBox3_MouseDown(object sender, MouseEventArgs e)
// making new LUT and the resulting image
{
int nbyte = 3;
Graphics g = g2Bmp; //pictureBox3.CreateGraphics();
SolidBrush myBrush = new SolidBrush(Color.LightGray);
Rectangle rect = new Rectangle(0, 0, 256, 256);
cntClick++;
if (cntClick == 3) cntClick = 1;
int oldX = -1, oldY = -1;
if (cntClick == 1)
{
X1 = e.X;
if (X1 < MinGV) X1 = MinGV;
if (X1 > MaxGV) X1 = MaxGV;
Y1 = 255 - e.Y; // (X, Y) is the clicked point in the graph of the LUT
if (X1 != oldX || Y1 != oldY) //---------------------------------------
{
// Calculating the LUT for X1 and Y1:
for (int gv = 0; gv <= X1; gv++)
{
if (gv <= MinGV) LUT[gv] = 0;
if (gv > MinGV && gv <= X1) LUT[gv] = (gv - MinGV) * Y1 / (X1 - MinGV);
if (LUT[gv] > 255) LUT[gv] = 255;
}
}
oldX = X1;
oldY = Y1;
}
if (cntClick == 2)
{
X2 = e.X;
if (X2 < MinGV) X2 = MinGV;
if (X2 > MaxGV) X2 = MaxGV;
if (X2 < X1) X2 = X1 + 1;
Y2 = 255 - e.Y; // (X2, Y2) is the second clicked point in the graph of the LUT
if (Y2 < Y1) Y2 = Y1 + 1;
if (X2 != oldX || Y2 != oldY) //---------------------------------------
{
// Calculating the LUT for X2 and Y2:
for (int gv = X1 + 1; gv < 256; gv++)
{
//if (gv <= MinGV) LUT[gv] = 0;
if (gv > X1 && gv <= X2) LUT[gv] = Y1 + (gv - X1) * (Y2 - Y1) / (X2 - X1);
if (gv > X2 && gv <= MaxGV)
LUT[gv] = Y2 + (gv - X2) * (255 - Y2) / (MaxGV - X2);
if (LUT[gv] > 255) LUT[gv] = 255;
if (gv >= MaxGV) LUT[gv] = 255;
}
}
oldX = X2;
oldY = Y2;
}
Pen myPen1 = new Pen(Color.Blue);
makeBigLUT(LUT);
// Drawing the curve for LUT:
g.FillRectangle(myBrush, rect);
Pen myPen = new Pen(Color.Red);
g.DrawLine(myPen, MinGV, 255, MinGV, 250);
for (int gv = 0; gv < 256; gv++)
{
int hh = histo[gv] * 255 / MaxHist;
if (histo[gv] > 0 && hh < 1) hh = 1;
g.DrawLine(myPen, gv, 255, gv, 255 - hh);
}
int yy = 255;
g.DrawLine(myPen1, 0, yy-LUT[0], MinGV, yy-LUT[0]);
g.DrawLine(myPen1, MinGV, yy - LUT[0], X1, yy - LUT[X1]);
g.DrawLine(myPen1, X1, yy - LUT[X1], X2, yy - LUT[X2]);
g.DrawLine(myPen1, X2, yy - LUT[X2], MaxGV, 0);
g.DrawLine(myPen1, MaxGV, 0, 255, 0);
// Calculating 'contrastIm':
int[] GV = new int[3];
int arg, colOld, colNew;
for (int i = 0; i < nbyte * width * height; i++) contrastIm.Grid[i] = 0;
progressBar1.Visible = true;
int y3 = 1 + nbyte * width * height / 100;
for (int y = 0, yn=0; y < width * height; y += width, yn+=nbyte * width) //======
{
if (y % y3 == 1) progressBar1.PerformStep();
for (int x = 0, xn=0; x < width; x++, xn+=nbyte)
{
int lum = origGrayIm.Grid[x + y];
for (int c = 0; c < nbyte; c++)
{
colOld = origIm.Grid[c + xn + yn]; // xn + yn = nbyte*(x + width * y);
arg=(lum << 8) | colOld;
colNew = bigLUT[arg];
if (colNew > 255) colNew = 255;
contrastIm.Grid[c + xn + yn] = (byte)colNew;
}
}
} //================ end for (int y = 0 ... ======================
progressBar1.Visible = false;
// Calculating "ContrastBmp":
GridToBitmap(ContrastBmp, contrastIm.Grid);
pictureBox2.Image = ContrastBmp;
pictureBox3.Image = BmpPictBox3;
progressBar1.Visible = false;
} //****************** end pictureBox3_MouseDown ***********************
还有一小部分代码用于保存结果图像。它使用对话框SaveFileDialog
。如果用户想要将结果图像保存到输入原始图像的文件中,一些附加操作是必要的。输入文件的名称OpenImageFile
必须确定并保存在程序部分Open image
中。此外,必须定义一个名为tmpFileName
的临时文件。生成的图像保存在tmpFileName
文件中。那么必须调用下面的方法:
File.Replace(tmpFileName, OpenImageFile,
OpenImageFile.Insert(OpenImageFile.IndexOf("."), "BuP"));
该方法将tmpFileName
的内容复制到输入文件OpenImageFile
,删除tmpFileName
,并将输入文件的旧内容保存到新文件中,新文件的名称是通过在.
字符前的OpenImageFile
中插入字符串BuP
产生的。以下是项目这一部分的代码。
private void button2_Click(object sender, EventArgs e) // Save result
{
SaveFileDialog dialog = new SaveFileDialog();
dialog.Filter =
"Image Files(*.BMP;*.JPG;*.GIF)|*.BMP;*.JPG;*.GIF|All files (*.*)|*.*";
if (dialog.ShowDialog() == DialogResult.OK)
{
string tmpFileName;
if (dialog.FileName == OpenImageFile)
{
tmpFileName = OpenImageFile.Insert(OpenImageFile.IndexOf("."), "$$$");
if (dialog.FileName.Contains("jpg"))
ContrastBmp.Save(tmpFileName, ImageFormat.Jpeg); // saving tmpFile
else ContrastBmp.Save(tmpFileName, ImageFormat.Bmp);
origBmp.Dispose();
File.Replace(tmpFileName, OpenImageFile,
OpenImageFile.Insert(OpenImageFile.IndexOf("."), "BuP"));
origBmp = new Bitmap(OpenImageFile);
pictureBox1.Image = origBmp;
}
else
{
if (dialog.FileName.Contains("jpg"))
ContrastBmp.Save(dialog.FileName, ImageFormat.Jpeg);
else ContrastBmp.Save(dialog.FileName, ImageFormat.Bmp);
}
MessageBox.Show("The result image saved under " + dialog.FileName);
}
} //****************** end Save result ******************************
图 3-12 显示了使用所述项目获得的对比度增强的示例。该项目可用于增强彩色和灰度图像的对比度。
图 3-12
项目WFpiecewiseLinear
的对比度增强示例
项目WFpiecewiseLinear
可以在本书的源代码中找到。
四、阈值阴影校正
有些照片的光线不均匀,因为拍摄的对象没有得到均匀的照明。这种照片的暗区中的细微细节很难辨认。我们建议一种方法来提高这种图像的质量。
一种用于校正由安装在其他星球上的空间探测器上的自动设备拍摄的数字照片的已知方法使用从原始图像的像素亮度中减去局部平均亮度:
GVnew [ x,y ] = GVold [ x,y–表示 [ x,y ] + Const
其中 GVold x,y 是坐标为( x,y )的像素的亮度, Mean [ x,y 是像素( x,y )的邻域中的平均亮度, Const 是不依赖于坐标的亮度值, GVnew 是像素的亮度
这种方法给出的结果大多是充分的。但是,我建议一个稍微不同的方法。考虑从具有反射率 R ( x,y )的表面反射的光的亮度 L ( x,y ),该表面由产生照明 I (x,y)的光源照明:
L ( x,y ) = R ( x,y);I(x,y))*
其中φ是落光方向和表面法线之间的角度。如果我们有表面照度的估计值 E ( x,y ,并且我们对知道表面的反射率 R ( x,y )感兴趣,并且我们假设 cosφ = 1,那么我们可以通过将观察到的亮度 L ( x, y )除以 E ( x,y ),而不是从 L ( x,y )减去 E ( x,y )。
照度 I ( x,y )主要是随着坐标 x 和 y 的变化而缓慢改变其值的函数,而表面的反射率 R ( x,y )快速变化。因此,可以通过计算每个点( x,y )附近的 L ( x,y )的平均值来获得照度的估计值。在数码摄影的情况下,数码照片像素的亮度与 L ( x,y )成正比,我们感兴趣的是函数 R ( x,y )。所以要得到一个与 R ( x,y )成正比的函数,就要计算出 E ( x,y )作为照片亮度的局部均值,再用 L ( x,y )除以 E ( x,y )。从 L ( x,y )减去 E ( x,y )也可以得到 R ( x,y )的估计值。如果所有的值 L ( x,y ), R ( x,y ), I ( x,y ),以及 E ( x,y )都与相应物理值的对数成正比,这是正确的。但是,使用通常的值 L ( x,y )、 R ( x,y )、 I ( x,y )和 E ( x,y )有时会产生良好的结果。因此,我们在项目中使用减法和除法。用户可以比较结果并选择最好的一个。
我在这里提出的项目WFshadingBin
执行阴影校正(见我们在互联网上的软件)和阴影校正图像的阈值。
给定图像的局部平均值可通过第 [2 章中描述的方法Averaging
计算。然而,现代数码照片很大。大多数照片包含超过 1000 个⋅1000 = 106像素。用于计算典型图像阴影校正平均值的窗口大小必须至少为 100 ⋅ 100 = 10,000 像素。对于这样的大小,Averaging
方法会太慢。最好使用提供快速平均的方法,如第二章所述的FastAverageM
。平均窗口的宽度由带有标签Window
的工具numericUpDown1
指定,作为图像宽度的一部分。这对于通常不知道已处理图像大小的用户来说更方便。
另一方面,对于绘图照片的阴影校正,我们需要一个局部平均窗口,其宽度略大于绘图中线条的宽度。后者的宽度通常在 4 到 9 个像素之间。这可能小于图像宽度的 1 %,图像宽度可能高达 2,000 像素。因此,我们决定将平均窗口的宽度指定为图像宽度的千分之一。
即使对于彩色图像,我们只需要亮度的局部平均值,而不需要颜色通道的平均值。因此,我们必须将彩色图像转换成灰度图像,其像素中包含三色亮度( R,G,B )。方法ColorToGrayMC
在为彩色图像的每个像素计算值的同时执行这种转换
亮度 =最大值(R0.713,G,B0.527)
用我们的方法MaxC
计算(见第三章)。然后可以使用方法FastAverageM
来计算灰度图像的局部平均值。
我接下来描述这个项目WFshadingBin
。本项目的形式如图 4-1 所示。
图 4-1
阴影校正后项目WFshadingBin
的形式
它包含五个图片框、四个按钮和两个类型为numericUpDown
的工具。第一个按钮“打开图像”启动项目的第一部分,与项目的第一部分WFpiecewiseLinear
类似,不同之处在于这里定义了七个图像— OrigIm
、SigmaIm
、SubIm
、DivIm
、GrayIm
、MeanIm
和BinIm
。图像SigmaIm
包含通过西格玛滤波器对原始图像进行滤波的结果。图像用于通过减法和除法以及图像SubIm
和DivIm
的阈值处理来执行阴影校正。阈值将在下一节中描述。
阴影校正按钮启动项目的相应部分。它包含这里介绍的方法CorrectShading
。
public void CorrectShading()
{
int c, i, x, y;
bool ROWSECTION = false; // used to show the values of images in one row.
int[] color = {0, 0, 0};
int[] color1 = {0, 0, 0};
int Lightness=(int)numericUpDown2.Value;
int hWind = (int)(numericUpDown1.Value * width / 2000);
MeanIm.FastAverageM(GrayIm, hWind, this); // uses numericUpDown1
progressBar1.Visible = true;
progressBar1.Value = 0;
int[] histoSub = new int[256];
int[] histoDiv = new int[256];
for (i = 0; i < 256; i++) histoSub[i] = histoDiv[i] = 0;
byte lum = 0;
byte lum1 = 0;
int jump = height / 17; // width and height are properties of Form1
for (y = 0; y < height; y++) //=======================================
{
if (y % jump == jump - 1) progressBar1.PerformStep();
for (x = 0; x < width; x++)
{ // nbyteIm is member of 'Form1'
for (c = 0; c < nbyteIm; c++) //===================================
{
color[c] = Round(SigmaIm.Grid[c + nbyteIm * (x + width * y)] * Lightness / (double)MeanIm.Grid[x + width * y]); // Division
if (color[c] < 0) color[c] = 0;
if (color[c] > 255) color[c] = 255;
DivIm.Grid[c + nbyteIm * (x + width * y)] = (byte)color[c];
color1[c] = SigmaIm.Grid[c + nbyteIm * (x + width * y)] + Lightness - MeanIm.Grid[x + width * y]; // Subtraction
if (color1[c] < 0) color1[c] = 0;
if (color1[c] > 255) color1[c] = 255;
SubIm.Grid[c + nbyteIm * (x + width * y)] = (byte)color1[c];
} //=============== end for (c... ===============================
if (nbyteIm == 1)
{
lum = (byte)color[0];
lum1 = (byte)color1[0];
}
else
{
lum = SigmaIm.MaxC((byte)color[2], (byte)color[1], (byte)color[0]);
lum1 = SigmaIm.MaxC((byte)color1[2], (byte)color1[1], (byte)color1[0]);
}
histoDiv[lum]++;
histoSub[lum1]++;
}
} //============== end for (y... ===================================
// Calculating MinLight and MaxLight for 'Div':
int MaxLightDiv, MaxLightSub, MinLightDiv, MinLightSub, Sum = 0;
for (MinLightDiv = 0; MinLightDiv < 256; MinLightDiv++)
{
Sum += histoDiv[MinLightDiv];
if (Sum > width * height / 100) break;
}
Sum = 0;
for (MaxLightDiv = 255; MaxLightDiv >= 0; MaxLightDiv--)
{
Sum += histoDiv[MaxLightDiv];
if (Sum > width * height / 100) break;
}
// Calculating MinLight and MaxLight for 'Sub':
Sum = 0;
for (MinLightSub = 0; MinLightSub < 256; MinLightSub++)
{
Sum += histoSub[MinLightSub];
if (Sum > width * height / 100) break;
}
Sum = 0;
for (MaxLightSub = 255; MaxLightSub >= 0; MaxLightSub--)
{
Sum += histoSub[MaxLightSub];
if (Sum > width * height / 100) break;
}
// Calculating MinLight and MaxLight for 'Sub':
Sum = 0;
for (MinLightSub = 0; MinLightSub < 256; MinLightSub++)
{
Sum += histoSub[MinLightSub];
if (Sum > width * height / 100) break;
}
Sum = 0;
for (MaxLightSub = 255; MaxLightSub >= 0; MaxLightSub--)
{
Sum += histoSub[MaxLightSub];
if (Sum > width * height / 100) break;
}
// Calculating LUT for 'Div':
byte[] LUT = new byte[256];
for (i = 0; i < 256; i++)
if (i <= MinLightDiv) LUT[i] = 0;
else
if (i > MinLightDiv && i <= MaxLightDiv)
LUT[i] = (byte)(255 * (i - MinLightDiv) / (MaxLightDiv - MinLightDiv));
else LUT[i] = 255;
// Calculating LUTsub for 'Sub':
byte[] LUTsub = new byte[256];
for (i = 0; i < 256; i++)
if (i <= MinLightSub) LUTsub[i] = 0;
else
if (i > MinLightSub && i <= MaxLightSub)
LUTsub[i] = (byte)(255 * (i - MinLightSub) / (MaxLightSub - MinLightSub));
else LUTsub[i] = 255;
// Calculating contrasted "Div" and "Sub":
for (i = 0; i < 256; i++) histoDiv[i] = histoSub[i] = 0;
jump = width * height / 17;
for (i = 0; i < width * height; i++) //==================================
{
if (i % jump == jump - 1) progressBar1.PerformStep();
for (c = 0; c < nbyteIm; c++)
{
DivIm.Grid[c + nbyteIm * i] = LUT[DivIm.Grid[c + nbyteIm * i]];
SubIm.Grid[c + nbyteIm * i] = LUTsub[SubIm.Grid[c + nbyteIm * i]];
}
if (nbyteIm == 1)
{
lum = DivIm.Grid[0 + nbyteIm * i];
lum1 = SubIm.Grid[0 + nbyteIm * i];
}
else
{
lum = SigmaIm.MaxC(DivIm.Grid[2 + nbyteIm * i], DivIm.Grid[1 + nbyteIm * i],
DivIm.Grid[0 + nbyteIm * i]);
lum1 = SigmaIm.MaxC(SubIm.Grid[2 + nbyteIm * i], SubIm.Grid[1 + nbyteIm * i],
SubIm.Grid[0 + nbyteIm * i]);
}
histoDiv[lum]++;
histoSub[lum1]++;
} //=============== end for (i = 0; ... ==============================
// Displaying the histograms and the row sections:
Graphics g1 = pictureBox4.CreateGraphics();
Graphics g = pictureBox5.CreateGraphics();
Graphics g0 = pictureBox1.CreateGraphics();
int MaxHisto1 = 0, SecondMax1 = 0;
int MaxHisto = 0, SecondMax = 0;
for (i = 0; i < 256; i++)
{
if (histoSub[i] > MaxHisto1) MaxHisto1 = histoSub[i];
if (histoDiv[i] > MaxHisto) MaxHisto = histoDiv[i];
}
for (i = 0; i < 256; i++) if (histoSub[i] != MaxHisto1 && histoSub[i] > SecondMax1) SecondMax1 = histoSub[i];
MaxHisto1 = SecondMax1 * 4 / 3;
for (i = 0; i < 256; i++) if (histoDiv[i] != MaxHisto && histoDiv[i] > SecondMax) SecondMax = histoDiv[i];
MaxHisto = SecondMax * 4 / 3;
Pen redPen = new Pen(Color.Red), yellowPen = new Pen(Color.Yellow),
bluePen = new Pen(Color.Blue), greenPen = new Pen(Color.Green);
SolidBrush whiteBrush = new SolidBrush(Color.White);
Rectangle Rect1 = new Rectangle(0, 0, 256, 200);
g1.FillRectangle(whiteBrush, Rect1);
Rectangle Rect = new Rectangle(0, 0, 256, 200);
g.FillRectangle(whiteBrush, Rect);
for (i = 0; i < 256; i++)
{
g1.DrawLine(redPen, i, pictureBox4.Height - histoSub[i] * 200 / MaxHisto1, i, pictureBox4.Height);
g.DrawLine(redPen, i, pictureBox5.Height - histoDiv[i] * 200 / MaxHisto, i, pictureBox5.Height);
}
for (i = 0; i < 256; i += 50)
{
g1.DrawLine(greenPen, i, pictureBox4.Height - 200, i, pictureBox4.Height);
g.DrawLine(greenPen, i, pictureBox5.Height - 200, i, pictureBox5.Height);
}
if (ROWSECTION)
{
y = height * 10 / 19;
g0.DrawLine(redPen, marginX, marginY + (int)(y * Scale1),
marginX + (int)(width * Scale1), marginY + (int)(y * Scale1));
int xold = marginX, xs = 0;
int yd = 0, ydOld = 256 - DivIm.Grid[0 + width * y];
int ys = 0, ysOld = 256 - SubIm.Grid[0 + width * y];
int yg = 0, ygOld = 256 - GrayIm.Grid[0 + width * y];
int ym = 0, ymOld = 256 - MeanIm.Grid[0 + width * y];
for (x = 1; x < width; x++)
{
xs = marginX + (int)(x * Scale1);
yd = 256 - DivIm.Grid[x + width * y];
ys = 256 - SubIm.Grid[x + width * y];
yg = 256 - GrayIm.Grid[x + width * y];
ym = 256 - MeanIm.Grid[x + width * y];
g0.DrawLine(redPen, xold, ymOld, xs, ym);
g0.DrawLine(yellowPen, xold, ydOld, xs, yd);
//g0.DrawLine(bluePen, xold, ysOld, xs, ys);
xold = xs;
ydOld = yd;
ysOld = ys;
ygOld = yg;
ymOld = ym;
}
g0.DrawLine(bluePen, marginX, 256 - Threshold,
marginX + (int)(width * Scale1, 256 - Threshold);
}
} //************** end CorrectShading ***********************************
方法FastAverageM
从标签为Window in per mille to Width
的工具numericUpDown1
中获取参数hWind
。该方法计算灰度图像的局部平均值GrayIm
。请注意,为了在图片照片的情况下获得良好的效果,有时需要选择相当小的Window
值。利用稍后描述的项目WFshadBinImpulse
可以获得更好的结果。
在具有变量y
和x
的循环中计算阴影校正图像SubIm
和DivIm
的内容。
正如你所看到的,方法CorrectShading
以两种方式计算带有校正阴影的图像。第一种方法包括将原始图像OrigIm
的值除以保存在图像MeanIm
中的局部平均亮度,并乘以通过工具numericUpDown2
手动指定的值Lightness
。第二种方法包括从原始图像OrigIm
中减去MeanIm
中保存的局部平均亮度,并加上Lightness
。使用这两种方法很重要,因为实验表明除法的结果并不总是比减法好。方法CorrectShading
以刚刚描述的两种方式计算两幅图像DivIm
和SubIm
。两个结果都显示给用户,用户可以决定应该保存两个结果中的哪一个。
校正图像的质量取决于两个参数:Window
和Lightness
。可以通过带有相应标签的工具来更改这些参数,并立即看到结果。参数Lightness
指定图像的平均亮度。参数Window
定义了计算原始图像平均亮度的滑动窗口的大小。必须选择这个参数,使得它近似等于由于不均匀亮度而出现的原始图像中的斑点的尺寸。例如,前面图 4-1 所示图像的最佳窗口值约为 200 像素。该值应以图像宽度的千分之一为单位进行设置。在这种情况下,从宽度= 400 开始,200 个像素构成千分之 500。用户必须定义Window=500
。阴影校正后的表单视图如图 4-1 所示。
当用户对阴影校正的结果满意时,他或她可以保存图像SubIm
或图像DivIm
,或者对这些图像进行阈值处理。接下来描述阈值处理的过程。
对图像进行阈值处理
为灰度图像选择最佳阈值的常用方法包括使用对应于直方图最小值的灰度值作为阈值。理由如下:具有两个区域的图像的直方图看起来像两列,每个区域具有恒定的灰度级。如果存在噪声,直方图看起来像两座小山,中间有一个山谷(图 4-2 )。
图 4-2
包含两个区域的图像直方图示例
最佳阈值对应于直方图的局部最小值,但不总是对应于全局最小值。因此,有必要将搜索区域限制为最小值,以保证图像的至少某个预定部分将是黑色或白色。例如,如果您希望图像的至少 5%为黑色,您必须找到这样一个灰度值 minGV ,使得直方图区域的 5%位于 minGV 的左侧。只需考虑 minGV 右侧的最小值。类似地,选择 maxGV 可以保证至少该区域的期望部分是白色的。
有时直方图有不止一个局部最小值,最深的一个并不总是对应于你想要的结果。在这种情况下,对于对应于所有局部最小值的阈值,显示小的二进制图像并选择最佳图像是合适的。
另一种可能性是产生多级图像,每一级对应于两个阈值之间的空间。还必须提供对应于小于第一阈值且大于最后阈值的灰度值的等级。这意味着等于 0 的亮度和等于 255 的亮度也应该被认为是阈值。产生多级图像意味着图像被量化:亮度在阈值TI和TI+1之间的所有像素获得值(TI+TI+1)/2。为了使对局部最小值的搜索更加确定,有必要平滑直方图,因为由于噪声,直方图可能具有许多非常小的局部最小值。图 4-3 显示了一个具有四个等级的图像示例,以及对应于直方图局部最小值的三个不同阈值的阈值处理结果。
图 4-3
在三个候选阈值中进行选择
人们提出了许多选择最佳阈值的不同方法,但我建议另一种方法:我们的项目提出了手动改变阈值并立即看到结果的可能性。因此,用户可以选择看起来最佳的结果。
一些要进行阈值处理的图像具有强烈的阴影,这意味着局部平均亮度从图像的一部分到另一部分逐渐变化。如果较暗区域的亮侧比较亮区域的暗侧亮,则不存在分隔这两个区域的恒定阈值。在这种情况下,阴影校正非常有用。阴影校正的过程已经描述过了。正如你已经知道的,有两种阴影校正的方法:可以使用减法或除以局部平均值。用户可以在用减法或除法对阴影校正的结果进行阈值处理之间进行选择。我们的项目WFshadingBin
在阴影校正后执行阈值处理。它显示了阴影校正图像的直方图。用户可以通过点击直方图(例如,在局部最小值中)来选择阈值,并立即看到结果。所选阈值在直方图中显示为一条蓝色垂直线。图 4-4 再次显示了项目WFshadingBin
的形式。
图 4-4
项目的形式WFshadingBin
阈值图像有时在均匀的白色区域中包含暗点。如果发生这种情况,必须增加Window
参数。
该项目可用于校正光线不足的图片。在这种情况下,平均窗口的最佳尺寸可以非常小,例如 6 或 9 个像素。这不到宽度的 1 %,宽度通常大于 1,000 像素。因此,我们决定在这个项目中定义平均窗口的大小,单位是千分之一,而不是图像宽度的百分比。
使用WFshadingBin
项目进行阈值处理的结果相当令人满意。图 4-5a 显示了 Wikipedia.en 上的文章“Otsu 的方法”中提供的图像的结果,该文章描述了日本科学家 Nobuyuki Otsu 开发的一种相当复杂的方法,用于选择最佳阈值。图 4-5b 显示了通过大津法获得的结果。
图 4-5
维基百科图像的阈值化采用(a) WFshadingBin,
和(b) Otsu 的方法
如你所见,结果是相似的。然而,我们的方法具有这样的优点,即用户可以容易且快速地改变阈值并立即看到结果图像。
我们项目中使用的方法CorrectShading
在前面的章节中已经描述过了。这里我们展示了源代码的一部分,它画出了显示阈值的蓝线并执行阈值处理。
private void pictureBox5_MouseClick(object sender, MouseEventArgs e)
// Thresholding DivIm
{
Threshold = e.X;
Graphics g = pictureBox5.CreateGraphics();
Pen bluePen = new Pen(Color.Blue);
g.DrawLine(bluePen, Threshold, 0, Threshold, pictureBox5.Height);
progressBar1.Visible = true;
progressBar1.Value = 0;
int nbyte = DivIm.N_Bits / 8;
int jump = height / 100;
for (int y = 0; y < height; y++)
{
if (y % jump == jump - 1) progressBar1.PerformStep();
for (int x = 0; x < width; x++)
{
int i = x + width * y;
if (nbyte == 1)
{
if (DivIm.Grid[i] > Threshold) BinIm.Grid[i] = 255;
else BinIm.Grid[i] = 0;
}
else
{
if (DivIm.MaxC(DivIm.Grid[2 + 3*i], DivIm.Grid[1 + 3*i],
DivIm.Grid[0 + 3*i]) > Threshold) BinIm.Grid[i] = 255;
else BinIm.Grid[i] = 0;
}
Div_Bitmap.SetPixel(x, y, Color.FromArgb(BinIm.Grid[i], BinIm.Grid[i],
BinIm.Grid[i]));
}
}
pictureBox3.Image = Div_Bitmap;
Threshold = -1;
} //****************** end pictureBox5_MouseClick ***********************
项目WFshadingBin
在改善历史图像的照片方面特别有效。图 4-6 是处理历史图像片段照片的又一个例子。
图 4-6
(a)原始图像,和(b)处理过的图像
五、WFshadBinImpulse
项目
我们还开发了项目WFshadBinImpulse
,其中结合了阴影校正、阈值处理和脉冲噪声抑制等程序。这种组合对于处理旧图纸照片的图像特别有用(参见图 2-8 )。本项目的形式如图 5-1 所示。
图 5-1
项目的形式WFshadBinImpulse
点击打开图像,用对话框OpenFileDialog
启动项目的常规部分。项目的这一部分还定义了七个类别的图像CImage
— OrigIm
、SigmaIm
、GrayIm
、MeanIm
、ShadIm
、BinIm
和ImpulseIm
。
用户应该通过选择四个选项中的一个来指定图像的种类:用深色线条绘制,或用浅色线条绘制,无绘制 Div,或无绘制 Sub。参数Window in per mille of Width
、Lightness
、Threshold
的初始值以及抑制脉冲噪声的参数将根据这些选项的选择自动设置。用户可以通过相应的numericUpDown
工具修正这些值。Window
将被指定为原始图像宽度的一部分,单位为千分之一,如前所述。以图像宽度的千分之一而不是直接以像素来指定是必要的,因为窗口的最佳宽度取决于图像的大小,但是用户通常不知道图像有多大。
在用户指定了图像类型后,他或她应该单击底纹。对于阴影校正,通过前面描述的方法FastAverageM
计算图像的局部平均亮度。然后通过第四章中描述的CorrectShading
方法计算图像SubIm
和DivIm
。阴影校正后的图像显示在右侧的图片框中。用户可以校正参数的建议值,以获得可能的最佳校正图像。
CorrectShading
方法还绘制阴影校正图像的直方图。用户可以通过单击直方图来指定阈值。阈值图像会立即显示。
用于抑制脉冲噪声的方法的参数的建议值也被自动设置。然而,用户应该测试这些参数的一些值。
如果图像是一幅图画,用户应在pictureBox1
(原始图像)中围绕图像中不应消除小斑点(如人的眼睛)的部分绘制小矩形。用户应该用鼠标点击矩形的左上角和右下角。矩形以蓝色显示。在脉冲噪声抑制运行后,可以重新定义这些矩形。最多可以画六个矩形。称为maxNumber
的矩形的加倍最大数量在Form1
的开头定义。
如果用户对阴影校正和阈值处理的结果感到满意,那么他或她可以通过点击脉冲噪声来开始抑制脉冲噪声,该脉冲噪声在阈值图像中被视为小的黑色和白色斑点。如果用户对获得的结果不满意,他或她可以通过相应的numericUpDown
工具尝试更改删除暗和删除亮的值。这些值指定了应该删除的斑点中的最大像素数。
如果用户对最终结果满意,那么他或她可以保存结果。需要点击保存结果,选择正确的目录,用扩展名.bmp
或.jpg
指定结果图像的文件名。
六、边缘检测
在已知的边缘检测方法中,简单的梯度滤波器包含某种图像平滑。索贝尔滤波器是一个常见的例子。它由两个权重矩阵定义,如图 6-1 所示。
图 6-1
Sobel 滤波器的权重
该滤波器计算实际像素的滑动 3 × 3 邻域中的灰度值与图 6-1 所示的相应权重 WH 和 WV 的乘积的两个和 SH 和 SV ,并返回绝对值| SH | + | SV |。该值大于给定阈值的像素属于边缘。由于边缘太厚,结果一般不令人满意。
其他已知的边缘检测方法是拉普拉斯 Kimmel (2003)和 Canny (1986)滤波器的零交叉。下面我将把这些方法与我们新的相当简单有效的方法进行比较。
拉普拉斯算子
一种有效的边缘检测方法是拉普拉斯的零交叉。拉普拉斯算子在数学上被定义为二阶偏导数的和:
Lap( F ( x 、和)=(x,【t】
*因为数字图像是不可微的,所以有必要用有限差分来代替导数。
定义 1: 表达式 D 1 ( x ,δx,F)=F(x+δx)-F(x)称为 F ( x )的第一差就
定义二:表达式Dn(x,δx,F)=D1(x,δx,Dn-1(x相对于 x 和 y 的第二偏差值等于
d(x, x,f=(x-【t】
d(y, y,f=(*,
并且δx =δy*= 1 的有限拉普拉斯算子等于
2【f】(x、和=f(x–1
*可以用图 6-2 所示的滤波器在数字图像中进行计算。
图 6-2
用于计算有限拉普拉斯算子的滤波器
将该滤波器应用于具有灰度值 F ( x , y )的数字图像产生以下值:
2【f】(x、和=f(x–1
*等式 6.1 可以改写如下:
2【f】(x、和=f(x–1 和+f(x、y+1)-5(x、
**= 5 M ( x , y ) - 5 F ( x ,y)= 5(M(x,y)-F(x,y)(6.2)
其中 M ( x , y )为坐标为( x , y )的像素邻域内五个像素的 F ( x , y )的平均值。也可以通过从 P 邻域的局部均值中减去 P 的灰度值,计算出像素 P = ( x , y )中的有限拉普拉斯算子。当使用更大的邻域时,最好使用快速平均滤波器(第二章)。
过零方法
过零方法基于检测拉普拉斯变换符号的位置。考虑图 6-3 所示的一行数字灰度图像的横截面。
图 6-3
一行数字图像的横截面
拉普拉斯算子在边的一侧有正值,在另一侧有负值。与远离边缘的位置上的拉普拉斯值相比,边缘附近的拉普拉斯绝对值较大。使用拉普拉斯的负值更实际——即值( F ( x ,y)——平均值)而不是值(平均值——F(x,y)——因为这些值在更大的灰度值下更大。
拉普拉斯变换其符号的位置被称为零交叉。这些位置中的一些对应于边缘的位置。零交叉总是位于两个像素之间。因此,边缘位置的这些指示总是非常微弱。
拉普拉斯曲线的过零点是闭曲线吗?
在一些图像处理的教科书中提出的拉普拉斯算子的一个优点是,拉普拉斯算子产生的边缘总是封闭的直线。然而,只有当我们设定拉普拉斯值的阈值时,这才是正确的;例如,如果我们用 1 代替拉普拉斯的正值,用 0 代替负值。然而,在这样的图像中具有值 1 的像素组的边界是闭合曲线的事实是二进制图像的优点,而不是拉普拉斯的优点。这些边缘如图 6-4b 所示。用等于 0 的阈值二值化的拉普拉斯图像中的大多数边缘是不相关的。
图 6-4
(a)原始图像,以及(b)用阈值 0 二值化的拉普拉斯算子;边缘显示为白线
如你所见,图 6-4b 中的绝大多数白线完全没有意义,不能作为边缘。这些就是所谓的无关过零点。
图 6-5 显示了拉普拉斯不相关零交叉的解释。黑线显示原始图像的灰度值。绿线代表灰度值的局部均值,红线是拉普拉斯算子。红色箭头表示不相关的过零事件。在这些位置的拉普拉斯值很小,而在相关交叉点的拉普拉斯绝对值很大。
图 6-5
拉普拉斯算子的不相关过零点(红色箭头)
我们将在下一节解释如何消除不相关的过零事件。
如何消除不相关的交叉
不相关的交叉可以通过两个阈值与相关的交叉区分开:只有从大于正阈值的拉普拉斯值到小于负阈值的拉普拉斯值的转变才应该被识别为相关的边缘。大多数不相关的边缘会消失,但一些相关的边缘会因图像中的噪声而中断。应对图像进行滤波以降低噪声,例如使用最简单的均值滤波器(第章第二部分)。图像应该被过滤两次:用一个小的和一个大的滑动窗口。这两个滤波图像之间的差异可以被认为是拉普拉斯算子的良好近似。
考虑图 6-6b ,图中一行的灰度值显示为一条蓝线。拉普拉斯值显示为绿线。黑线是 x 轴;红线显示用于区分相关和不相关过零事件的阈值。如果零交叉位于两个这样的像素之间,其中一个像素的拉普拉斯值大于正阈值,而另一个像素的拉普拉斯值小于负阈值,则零交叉是相关的。
图 6-6
(a)用两个阈值(白线)找到的边缘,以及(b)解释
图 6-7 解释了如何消除不相关的过零事件。
图 6-7
拉普拉斯值的一个例子
使用拉普拉斯算子之前的降噪
使用均值滤波器降噪时,边缘会变得模糊。结果,拉普拉斯的正值和负值之间的差异变小;拉普拉斯值逐渐变化。这使得很难区分相关和不相关的过零事件。我们建议使用 sigma 滤波器(第章第二部分),而不是使用小窗口求平均值。那么边缘保持陡峭;拉普拉斯算子的绝对量变得更大。
图 6-8a 中的黑线显示了通过小窗口平均过滤的灰度值。在图 6-8b 中,黑线代表用西格玛滤波器过滤的灰度值。对于两者,绿线代表具有较大窗口的局部平均值,红线显示拉普拉斯值。
图 6-8
使用(a)平均和(b) sigma 滤波器滤波后的灰度和拉普拉斯值
数字化和极值滤波过程中的模糊
在图像的数字化过程中,即使是理想的边缘也会变得有些模糊。其原因是,投影到一组光敏元件(例如,CCD、电荷耦合器件、各种设备中使用的电子光传感器,包括数码相机)的图像中的亮区和暗区之间的边界偶尔会落在元件的中心附近,如图 6-9 所示。
图 6-9
CCD 矩阵由一个暗区和一个亮区照明
这个元素获得一个光量,我们称之为中间值,位于适合亮区和暗区的值之间。相应的像素位于边缘的中间,并且拉普拉斯算子获得小的值或者甚至零值。检测到的边缘出现间隙,如图 6-10 所示。
图 6-10
灰度值(黑线)、局部平均值(绿线)和拉普拉斯算子(红线)
我们建议使用极值过滤器来避免中间值导致的拉普拉斯间隙。这个应用于灰度图像的滤波器在一个小的滑动窗口中计算最大和最小灰度值。它还计算中心像素的灰度值与最大值和最小值之间的差异,并决定这两个值中的哪一个更接近中心值。更接近的值被分配给输出图像中的中心像素。边缘变得清晰,拉普拉斯间隙消失。这里是灰度图像的极端过滤器的源代码。
int CImage::ExtremVar(CImage &Inp, int hWind)
{ N_Bits=8; width=Inp.width; height=Inp.height;
Grid=new unsigned char[width*height];
int hist[256];
for (int y=0; y<height; y++) // ================================
{ int gv, y1, yStart=__max(y-hWind,0), yEnd=__min(y+hWind,height-1);
for (int x=0; x<width; x++) //============================
{ if (x==0) //-------------------------------------------------------
{ for (gv=0; gv<256; gv++) hist[gv]=0;
for (y1=yStart; y1<=yEnd; y1++)
for (int xx=0; xx<=hWind; xx++)
hist[Inp.Grid[xx+y1*width]]++;
}
else
{ int x1=x+hWind, x2=x-hWind-1;
if (x1<width)for (y1=yStart; y1<=yEnd; y1++)
hist[Inp.Grid[x1+y1*width]]++;
if (x2>=0)
for (y1=yStart; y1<=yEnd; y1++)
{ hist[Inp.Grid[x2+y1*width]]--;
if (deb && hist[Inp.Grid[x2+y1*width]]<0) return -1;
}
} //---------------- end if (x==0) ---------------------------------
int gMin=0, gMax=255;
for (gv=gMin; gv<=gMax; gv++)
if (hist[gv]>0) { gMin=gv; break; }
for (gv=gMax; gv>=0; gv--)
if (hist[gv]>0) { gMax=gv; break; }
if (Inp.Grid[x+width*y]-gMin<gMax-Inp.Grid[x+width*y])
Grid[x+width*y]=gMin;
else Grid[x+width*y]=gMax;
} //=============== end for (int x... ================
} //================ end for (int y... =================
return 1;
} //******************* end ExtremVar *********************
这里是彩色和灰度图像的通用极端方法的源代码。
public int ExtremLightUni(CImage Inp, int hWind,Form1 fm1)
/* The extreme filter for color or grayscale images with variable hWind. The filter finds in the (2*hWind+1)-neighborhood of the actual pixel (x,y) the color "Color1" with minimum and the color "Color2" with the maximum lightness. "Color1" is assigned to the output pixel if its lightness is closer to the lightness of the central pixel than the lightness of "Color2". --*/
{
byte[] CenterColor = new byte[3], Color = new byte[3], Color1 =
new byte[3], Color2 = new byte[3];
int c, k, nbyte = 3, x;
if (Inp.N_Bits == 8) nbyte = 1;
for (int y = 0; y < height; y++) // ====================================
{
if (y % jump == jump - 1) fm1.progressBar1.PerformStep();
for (x = 0; x < width; x++) //======================================
{
for (c = 0; c < nbyte; c++) Color2[c] = Color1[c] = Color[c] =
CenterColor[c] = Inp.Grid[c + nbyte * (x + y * width)];
int MinLight = 1000, MaxLight = 0;
for (k = -hWind; k <= hWind; k++) //==============================
{
if (y + k >= 0 && y + k < height)
for (int i = -hWind; i <= hWind; i++) //==========================
{
if (x + i >= 0 && x + i < width) // && (i > 0 || k > 0))
{
for (c = 0; c < nbyte; c++)
Color[c] = Inp.Grid[c + nbyte * (x + i + (y + k) * width)];
int light;
if (nbyte == 3) light= MaxC(Color[2], Color[1], Color[0]);
else light = Color[0];
if (light < MinLight)
{
MinLight = light;
for (c = 0; c < nbyte; c++) Color1[c] = Color[c];
}
if (light > MaxLight)
{
MaxLight = light;
for (c = 0; c < nbyte; c++) Color2[c] = Color[c];
}
}
} //=============== end for (int i... =======================
} //=================== end for (int k... ======================
int CenterLight = MaxC(CenterColor[2], CenterColor[1], CenterColor[0]);
int dist1 = 0, dist2 = 0;
dist1 = CenterLight - MinLight;
dist2 = MaxLight - CenterLight;
if (dist1 < dist2)
for (c = 0; c < nbyte; c++) Grid[c + 3 * x + y * width * 3] = Color1[c]; // Min
else
for (c = 0; c < nbyte; c++) Grid[c + 3 * x + y * width * 3] = Color2[c]; // Max
} //================== end for (int x... ==========================
} //==================== end for (int y... ==========================
//fm1.progressBar1.Visible = false;
return 1;
} //********************** end ExtremLightUni ***************************
考虑在使用 sigma 滤波器和极值滤波器之后通过拉普拉斯过零方法(阈值= 10)检测的边缘的例子。图 6-11 显示了原始图像的灰度值(图 6-11a )以及用滤波器对图像进行连续步骤处理后的图像:用西格玛滤波器(图 6-11b )、用极值滤波器(图 6-11c )和带边缘的拉普拉斯算子(图 6-11d )处理后的图像。拉普拉斯正值显示为红色,负值显示为蓝色,过零点显示为白色线条。
图 6-11
具有拉普拉斯零交叉的边缘检测的例子:(a)原始图像,(b) sigma 滤波的,(c)极端滤波的,以及(d)具有边缘的拉普拉斯算子(白线)
如你所见,边缘检测相当成功。然而,在下一节中解释了边缘中的一些间隙。
拉普拉斯零交叉法的基本误差
拉普拉斯算子具有一个基本性质,即它的过零点序列在三个或四个序列相交的点的邻域中一定有间隙。图 6-12 提供了一个例子。图 6-12 显示了人工图像(a)、通过过零方法检测到的边缘(b)、理想边缘(c)以及拉普拉斯符号的值(d)。红色像素是拉普拉斯值为正值的像素,蓝色像素是负值的像素。过零点在图 6-12d 中显示为白线。
图 6-12
(a)带有噪声的人工图像,σ= 5;(b)在 sigma 和极端滤波之后图像上的过零点;理想的边缘;以及(d)拉普拉斯算子上的过零点(红色是正值)
注意图 6-12 中由绿色箭头标记的基本误差导致的边缘缺失。这些被认为是基本误差,因为不可能消除它们。出现这些误差是因为拉普拉斯算子是局部平均值和实际灰度值之间的差。零交叉位于拉普拉斯算子的正值像素和负值像素之间。这些像素中的一个像素的灰度值大于局部平均值,而另一个像素的灰度值小于局部平均值。然而,如果在滑动窗口中有两个以上不同的灰度值,则局部平均值只能位于两个灰度值之间。第三灰度值不能有零交叉。因此,边缘有一个间隙。由于这些间隙,通过拉普拉斯零交叉产生的边缘不包含分叉。
这些错误非常重要,因为边通常用于生成表示具有恒定颜色的区域边界的多边形。缺少分叉会使这些多边形不完整,因为它们没有正确描述区域的边界。*********
七、一种新的边缘检测方法
如图 6-11 的示例所示,sigma 和极值滤波器的实施提高了拉普拉斯过零的质量,从而也提高了边缘的质量。需要强调的是,这两种滤波器极大地提高了图像的质量,使得应用最简单的边缘检测方法成为可能。这是灰度值梯度的二值化:
度(、和)=【gv】、和【y】****
度和 ( x 、和 ) = GV x 、和——gv
这里 GV ( x , y )是像素的灰度值( x , y )。必须将| Gradx(x, y )|和| Grady(x, y )|的值与一个阈值进行比较。如果| Gradx(x, y )|的值大于阈值,则有一小块边缘位于像素( x , y )和(x—1,y )之间。对于| Grad(x, y )|:如果该值大于阈值,则边缘在像素( x , y )和( x ,y—1)之间运行。
*在彩色图像的情况下,我们使用像素的色差( x 1 、 y 1 )和( x 2 、 y 2 )。我们称这种方法为二值化梯度。这是像素的颜色通道的强度差的绝对值之和:
difcolor =∑color(I、x、和)–color(I,,
其中 color( i , x , y 1 )是像素的第 i 个颜色通道的强度( x 1 , y 1 ),对于 color( i ,x2
图 7-1 显示了使用拉普拉斯零交叉(a)和二值化梯度(b)的边缘检测结果的比较示例。你可以看到这些图像几乎是一样的。
图 7-1
拉普拉斯零交叉(a)和二值化梯度(b)
比较绿色箭头指示的位置:通过二值化梯度找到的边缘没有由于过零方法的基本误差而在过零边缘出现的间隙。当使用色差时,边缘的进一步处理变得更加精确。
用于编码边缘的装置
对边缘进行编码不是一个简单的问题:例如,如果发现一对色差很大的像素( x 、 y )和(x–1、 y ),那么必须在包含边缘的图像中标记一个指示边缘位置的像素。假设我们决定用边缘来标记图像中的像素( x , y )。在像素对( x 、 y )和( x 、y–1)的情况下,我们决定标记像素( x 、 y )。然后,在图 7-2 所示的具有不同边缘元素的三种情况下,阈值等于 40,同一像素,即阴影像素,将被标记,并且不可能在这三种情况之间进行区分。
图 7-2
边缘不同的三幅图像
如果我们决定用大的色差来标记一对相邻像素中的另一个像素,那么在其他图像的情况下也会发生相同的情况。解决这个问题的唯一可能的方法是引入另一种编码边缘的结构,即包含像素和位于两个相邻像素之间的边缘元素的不同元素的结构。这种结构被称为抽象细胞复合体(ACC 参见 Kovalevsky,1989,2008)。
抽象细胞复合体的概念
我们在这里将数字平面视为一个二维的细胞复合体,而不是一组像素(见图 7-3 )。因此,我们的数字平面除了像素之外还包含裂缝和点,它们被认为是小正方形。裂缝是这些方块的边;点是裂缝的端点,因此是像素的角。点是零维单元,裂缝是一维单元,像素是二维单元。
图 7-3
包含阴影子集裂纹边界的小型二维复合体的示例
将平面视为抽象的细胞复合体有许多优点:不再有第八章中描述的连通性悖论,子集的边界变成了面积为零的细曲线,区域的边界和它的补的边界是相同的,等等。数字曲线尤其是数字直线的定义和处理变得更加简单和清晰。从图像的经济编码和精确重建的角度来看,最重要的优点是能够通过极其简单和快速的算法(参见 Kovalevsky,(1990))来填充裂纹边界的内部,这在将边界表示为像素组时是不能应用的。
本节的其余部分包含对本演示重要的拓扑概念的简短总结。更多细节和拓扑基础请参考 Kovavlevsky (2008)。熟悉细胞复合体的读者可以跳过这一节的其余部分。
单元之间存在一种约束关系:一个较低维度的单元可能会约束一些较高维度的单元。回头参考图 7-3 所示的小型二维复合体的例子。像素被表示为正方形的内部,裂缝被表示为正方形的边,点(即 0 单元)是裂缝的端点,同时也是像素的角。
现在让我们介绍一些我们在续集中需要的概念。复合体的子集 S 的边界裂缝是将属于 S 的像素与不属于 S 的另一个像素分开的裂缝。图 7-3 中阴影子集的边界裂纹绘制为粗线。子集 S 的边界(也称为裂纹边界)是 S 的所有边界裂纹以及这些裂纹的所有端点的集合。边界不包含像素,因此是一个面积为零的稀疏集合。边界的连通子集称为边界曲线。关于连通性的概念,请参考 Kovalevsky (1989)。
我们认为数字平面是一个笛卡尔二维复合体;即作为平面坐标轴的两个一维复形的笛卡尔积。 x 坐标是一行的编号; y 坐标是行号。我们在这里使用计算机图形的坐标系统;即正 x 轴从左向右运行,正 y 轴从上向下运行。
为了保存detected
边缘,我们需要一种特殊的 ACC:Cartesian two-dimensional ACC``.
使用这种 ACC,引入细胞坐标成为可能。与数字图像的情况完全一样, x 坐标是一列的编号,而 y 坐标是一行的编号。
对于数字图像,我们使用通常的坐标,其中 x 从 0 变化到宽度–1, y 从 0 变化到高度–1。我们称这些坐标为标准。ACC 中代表宽 × 高像素图像的单元格坐标面积更大: x 坐标从 0 变为 2* 宽, y 从 0 变为 2* 高。因此,表示宽度为×高度为像素的图像的 ACC 的大小具有(2 宽度为 + 1) × (2* 高度为 + 1)个单元的大小。我们将图像中代表给定数字图像 ACC 的细胞坐标称为组合坐标。我们称包含 ACC 的图像为组合坐标中的*图像。**
*注意,像素的组合坐标 x 和 y 都是奇数,而垂直裂纹的 x 坐标是偶数,其 y 坐标是奇数。在水平裂纹的情况下,情况相反:它的 x 坐标是奇数,而它的 y 坐标是偶数。一个点的两个组合坐标都是偶数。
在我们的一些项目中,我们处理检测到的边缘。例如,为了分析对象的形状,用多边形来近似检测到的边缘是相当方便的。要找到多边形,我们必须沿着边走。与通常只包含像素的图像相比,在组合坐标中跟踪图像的边缘要简单和方便得多。在我们的项目中,组合坐标中的图像被称为CombIm
。
一种简单的边缘编码方法
该方法使用包含单元复合体的图像CombIm
,该单元复合体的尺寸对应于已处理图像的尺寸:宽度CombIm
等于 2 *宽度+ 1,高度等于 2 *高度+ 1,其中宽度和高度是已处理图像的尺寸。添加+ 1 是必要的,以便为右边的点和CombIm
的底部边界留出位置,这有时对处理很重要。
方法LabelCells
一行接一行地读取由极值滤波器处理的图像,并为每个像素( x,y )测试该像素( x,y )与其相邻像素之一(x-1y)和( x,y-1)的颜色的绝对差值是否大于给定的阈值。阈值可以由用户定义,用户应该知道在低阈值下,边缘裂纹的数量变大,并且边缘的某些部分变厚的概率也很大。阈值的正确值应该通过实验找到。
如果像素( x,y )和(x—1,y )的色差的绝对值大于阈值,则位于这些像素之间的垂直裂纹获得标记1
。类似地,如果像素( x,y )和( x,y—1)的色差大于阈值,则相应的水平裂缝被标记为1
。
与裂纹相关的两个点的标记将同时增加。如果一个点与许多裂纹相关联(最多可以有四个关联裂纹),则该点的标签会增加许多倍。有限地,一个点的标号的值等于与该点相关的裂纹的数量。该信息可以在编码边缘的处理过程中使用。
下面是LabelCells
方法的源代码。
public void LabelCells(int th, CImage Image3)
{
int difH, difV, nbyte, NXB = Image3.width, x, y;
byte Lab = 1;
if (Image3.N_Bits == 24) nbyte = 3;
else nbyte = 1;
for (x = 0; x < width * height; x++) Grid[x] = 0;
byte[] Colorh = new byte[3];
byte[] Colorp = new byte[3];
byte[] Colorv = new byte[3];
for (y = 0; y < height; y += 2)
for (x = 0; x < width; x += 2) // through all points
Grid[x + width * y] = 0;
for (y = 1; y < height; y += 2)
for (x = 1; x < width; x += 2) // through the right and upper pixels
{
if (x >= 3) //-- vertical cracks: abs.dif{(x/2, y/2)-((x-2)/2, y/2)} -------
{
for (int c = 0; c < nbyte; c++)
{
Colorv[c] = Image3.Grid[c + nbyte *((x-2)/2)+nbyte*NXB*(y/2)];
Colorp[c] = Image3.Grid[c + nbyte * (x / 2) + nbyte *NXB*(y/2)];
}
if (nbyte == 3) difV = ColorDifAbs(Colorp, Colorv);
else difV = Math.Abs(Colorp[0] - Colorv[0]);
if (difV < 0) difV = -difV;
if (difV > th)
{
Grid[x - 1 + width * y] = Lab; // vertical crack
Grid[x - 1 + width * (y - 1)]++; // point above the crack;
Grid[x - 1 + width * (y + 1)]++; // point below the crack
}
} //------------------------ end if (x>=3) --------------------------
if (y >= 3) //--- horizontal cracks: abs.dif{(x/2, y/2)-(x/2, (y-2)/2)} ---
{
for (int c = 0; c < nbyte; c++)
{
Colorh[c] = Image3.Grid[c + nbyte *(x/2)+nbyte*NXB*((y-2)/2)];
Colorp[c] = Image3.Grid[c + nbyte * (x / 2) + nbyte *NXB*(y/2)];
}
if (nbyte == 3) difH = ColorDifAbs(Colorp, Colorh);
else difH = Math.Abs(Colorp[0] - Colorh[0]);
if (difH > th)
{
Grid[x + width * (y - 1)] = Lab; // horizontal crack
Grid[x - 1 + width * (y - 1)]++; // point left of crack
Grid[x + 1 + width * (y - 1)]++; // point right of crack
}
} //------------------------ end if (y>=3) --------------------------
} //================= end for (x=1;... =====================
} //******************* end LabelCells *************************
二值化梯度方法的改进
二值化梯度方法的最简单版本有一个缺点:对于灰度值或颜色的差异,用小阈值产生的边缘有时太厚。如果图像在某些位置模糊不清,就会出现这种情况,从而产生强度渐变。在斜坡宽度大于 4 个像素的情况下(这种情况很少发生),通常使用的窗口为 5 × 5 像素的极值滤波器(前面源代码中的参数hWind
等于 2)无法完全消除斜坡。然后边缘可以模糊。
为了消除这个问题,我们开发了一个改进版本的方法LabelCells
,我们称之为LabelCellsSign
。这种方法的目标类似于 Canny(1986)的无最大值抑制方法的目标,但是实现的解决方案非常不同。
让我们首先考虑应用于数字图像的梯度概念。人们不能使用梯度的经典概念(在二维情况下)定义为具有两个分量的向量,这两个分量是强度相对于坐标 x 和 y,的导数,因为数字图像不是欧几里德空间。需要用有限差分来代替导数:颜色的强度 I 相对于 x 的导数由 I ( x + 1,y)-I(x, y )和强度 I 相对于 y 的导数亮度 I ( x , y )是像素的属性( x , y ),其中 x 是列的编号, y 是行的编号。但是, I ( x + 1,y)–I(x, y )和 I ( x ,y+1)–I(在这种情况下,ACC 的概念变得非常有用:两个差异中的每一个都可以分配给位于一对像素之间的相应裂纹(一维单元)。然后,应将梯度分配给与两个裂纹相关的点(零维单元)。
首先考虑灰度图像的情况。有必要为图像中的每个位置计算灰度值的梯度,标记梯度的绝对值大于为边缘检测定义的阈值的位置,找到沿着平行于梯度的线的标记位置的连接子集,并找到该子集中梯度的绝对值达到最大值的位置。这个位置属于边缘。
很明显,在梯度的绝对值达到最大值的位置,其分量等于相邻像素中灰度值的差也达到最大值。我们感兴趣的是属于边缘的裂缝。因此,没有必要使用沿着梯度方向在连通子集中寻找位置的复杂过程。测试一行中的所有相邻像素对,以找到灰度值的差大于阈值的像素对,并在这些像素对中寻找差的最大值就足够了。这样,边缘的垂直裂缝将被发现。通过测试一列中的相邻像素对,可以以类似的方式找到边缘的水平裂缝。
然而,应该考虑梯度的方向或灰度值差异的符号。如果我们只考虑绝对值,那么图 7-4 中用红色和绿色箭头表示的情况无法区分,将会找到绝对值的单个最大值。但是,在这种情况下,必须找到两条边,因为有两条斜坡:一条斜坡在暗条上方亮度递减,另一条在暗条下方亮度递增。因此,我们必须考虑灰度值差异的符号,并寻找正差异的最大值和负差异的最小值。
图 7-4
灰度图像中亮度梯度的示例
让我们首先考虑灰度图像情况下的方法LabelCellsSign
,尽管它也适用于彩色图像。
该方法实现了某种有限状态机。它使用一个变量State
,其值对应于方法的不同状态。在方法开始时,State
获得零值。
该方法属于类CImage
并被称为CombIm.LabelCellsSign(threshold, InputImage)
,其中InputImage
是由极端过滤器计算的图像。CombIm
是包含用于编码边缘的细胞复合体的图像。因此,其尺寸为(如之前描述的方法LabelCells
的情况)2*width + 1
和2*height + 1
,其中width
和height
是InputImage
的尺寸。
方法LabelCellsSign
使用两对for
循环,其变量x
和y
扫描包含在图像CombIm
中的 ACC 的二维单元的所有位置,以保存边缘。
第一对线圈用于检测垂直裂缝。变量为y
的外部循环遍历从1
到height - 1
的所有奇数值。带有变量x
的内部循环从值x = 3
开始,因为它使用值x - 2
,该值必须保留在图像区域CombIm
。在x
循环的开始,计算输入图像站的两个相邻像素((x - 1) / 2, y / 2)
和(x / 2, y / 2)
( x
和y
是图像CombIm
中的坐标)的颜色差difV
。在灰度图像的情况下,它是通过减法直接计算的。然而,在彩色图像的情况下,它是用方法ColorDifSign
计算的,这将在后面解释。与方法LabelCells
不同,这里计算的差值不是绝对值,而是一个符号。
变量Inp
表示前面提到的两个像素的色差与边缘检测指定的Threshold
的关系:如果差值大于Threshold
,则Inp
等于1
,其他地方等于0
。变量State
和Inp
组成控制变量Contr
,等于3*State + Inp
控制开关指令。变量xStartP
包含大于Threshold
的色差序列开始的x
的值。类似地,变量xStartM
包含x
的值,其中color
差小于负阈值-Threshold
的序列开始。这里是switch
指令的伪代码。
switch (Contr)
{
case 4: if (x > xStartP && difV > maxDif)
{ maxDiv = difV;
xopt = x;
}
break;
case 3: label the vertical crack at (xopt - 1, y) and its end points;
State = 0;
break;
case 2: label the vertical crack at (xopt - 1, y) and its end points;
minDif = difV;
xStartM = x;
State = -1;
break;
case 1: maxDiv = difV; xopt = x; xStartP = x;
State = 1;
break;
case 0: break;
case -1: maxDiv = difV; xopt = x; xStartP = x;
State = -1;
break;
case -2: label the vertical crack at (xopt - 1, y) and its end points;
maxDif = difV;
xStartP = x;
State = 1;
case -3: label the vertical crack at (xopt - 1, y) and its end points;
State = 0;
break;
case -4: if (x > xStartM && difV < minDif)
{ minDiv = difV;
xopt = x;
}
break;
} //:::::::::::::::::::::: end switch :::::::::::::::::::::::::::::::.:::::
在行首,变量State
、Inp
和Contr
等于0
。如果Inp
变为值1
,那么Contr
也变为1
,并且在case 1:
中,最大值maxDif
被设置为difV
, xopt
和xStartP
被设置为x
,并且State
被设置为1
。数值xStartP
是色差大于Threshold
的裂纹序列的起点坐标。如果Inp
保留1
,则Contr
变为4
,因为State == 1
和difV
的最大值是沿着Inp
保留1
的裂纹顺序计算的。
如果Inp
变为0
,那么Contr
变为3
,在最大位置xopt - 1
标记一条垂直裂纹。这是像素序列中唯一一个difV
大于Threshold
的位置,在此标记了垂直裂缝。因此,在运行值y
处,垂直裂纹序列变细。
水平裂纹及其端点的标记发生在第二对for
环中,其中外环为x
环,内环为y
环。switch
指令看起来与垂直裂纹的指令相似,但是变量difV
被替换为difH
,变量xopt
被替换为yopt
,变量xStartP
被替换为yStartP
,变量xStartM
被替换为yStartM
。下面是方法LabelCellsSign
的源代码:
public int LabelCellsSign(int th, CImage Extrm)
{
int difH, difV, c, maxDif, minDif, nByte, NXB = Extrm.width, x, y, xopt, yopt;
int Inp, State, Contr, xStartP, xStartM, yStartP, yStartM;
if (Extrm.N_Bits == 24) nByte = 3;
else nByte = 1;
for (x = 0; x < width * height; x++) Grid[x] = 0;
byte[] Colorp = new byte[3], Colorh = new byte[3], Colorv = new byte[3];
maxDif = 0; minDif = 0;
for (y = 1; y < height; y += 2) //====== vertical cracks ==============
{
State = 0;
xopt = -1;
xStartP = xStartM = -1;
for (x = 3; x < width; x += 2) //==============================
{
for (c = 0; c < nByte; c++)
{
Colorv[c] = Extrm.Grid[c + nByte * ((x - 2) / 2) + nByte*NXB*(y / 2)];
Colorp[c] = Extrm.Grid[c + nByte * (x / 2) + nByte * NXB * (y / 2)];
}
if (nByte == 3) difV = ColorDifSign(Colorp, Colorv);
else difV = Colorp[0] - Colorv[0];
if (difV > th) Inp = 1;
else
if (difV > -th) Inp = 0;
else Inp = -1;
Contr = State * 3 + Inp;
switch (Contr) //:::::::::::::::::::::::::::::::::::::::::::::::::
{
case 4:
if (x > xStartP && difV > maxDif)
{
maxDif = difV;
xopt = x;
}
break;
case 3:
Grid[xopt - 1 + width * y] = 1; // vertical crack
Grid[xopt - 1 + width * (y - 1)]++; // point above
Grid[xopt - 1 + width * (y + 1)]++; // point below
State = 0;
break;
case 2:
Grid[xopt - 1 + width * y] = 1; // vertical crack
Grid[xopt - 1 + width * (y - 1)]++; // point above
Grid[xopt - 1 + width * (y + 1)]++; // point below
minDif = difV;
xopt = x;
xStartM = x;
State = -1;
break;
case 1: maxDif = difV; xopt = x; xStartP = x; State = 1; break;
case 0: break;
case -1: minDif = difV; xopt = x; xStartM = x; State = -1; break;
case -2:
Grid[xopt - 1 + width * y] = 1; // vertical crack
Grid[xopt - 1 + width * (y - 1)]++; // point above
Grid[xopt - 1 + width * (y + 1)]++; // point below
maxDif = difV;
xopt = x;
xStartP = x;
State = 1;
break;
case -3:
Grid[xopt - 1 + width * y] = 1; // vertical crack
Grid[xopt - 1 + width * (y - 1)]++; // point above
Grid[xopt - 1 + width * (y + 1)]++; // point below
State = 0;
break;
case -4:
if (x > xStartM && difV < minDif)
{
minDif = difV;
xopt = x;
}
break;
} //:::::::::::::::::::::: end switch ::::::::::::::::::::::::::::::
} //================ end for (x=3;... =====================
} //================= end for (y=1;... ======================
for (x = 1; x < width; x += 2) //=== horizontal cracks ===============
{
State = 0;
minDif = 0; yopt = -1; yStartP = yStartM = 0;
for (y = 3; y < height; y += 2) //=============================
{
for (c = 0; c < nByte; c++)
{
Colorh[c] = Extrm.Grid[c + nByte * (x / 2) + nByte*NXB*((y - 2) / 2)];
Colorp[c] = Extrm.Grid[c + nByte * (x / 2) + nByte * NXB * (y / 2)];
}
if (nByte == 3) difH = ColorDifSign(Colorp, Colorh);
else difH = Colorp[0] - Colorh[0];
if (difH > th)
Inp = 1;
else
if (difH > -th) Inp = 0;
else Inp = -1;
Contr = State * 3 + Inp;
switch (Contr) //:::::::::::::::::::::::::::::::::::::::::::::::::
{
case 4:
if (y > yStartP && difH > maxDif)
{
maxDif = difH;
yopt = y;
}
break;
case 3:
Grid[x + width * (yopt - 1)] = 1; // horizontal crack
Grid[x - 1 + width * (yopt - 1)]++; // left point
Grid[x + 1 + width * (yopt - 1)]++; // right point
State = 0;
break;
case 2:
Grid[x + width * (yopt - 1)] = 1; // horizontal crack
Grid[x - 1 + width * (yopt - 1)]++; // left point
Grid[x + 1 + width * (yopt - 1)]++; // right point
yopt = y;
State = -1;
break;
case 1: maxDif = difH; yopt = y; yStartP = y; State = 1; break;
case 0: break;
case -1: minDif = difH; yopt = y; yStartM = y; State = -1; break;
case -2:
Grid[x + width * (yopt - 1)] = 1; // horizontal crack
Grid[x - 1 + width * (yopt - 1)]++; // left point
Grid[x + 1 + width * (yopt - 1)]++; // right point
yopt = y;
State = 1;
break;
case -3:
Grid[x + width * (yopt - 1)] = 1; // horizontal crack
Grid[x - 1 + width * (yopt - 1)]++; // left point
Grid[x + 1 + width * (yopt - 1)]++; // right point
State = 0;
break;
case -4:
if (y > yStartM && difH < minDif)
{
minDif = difH;
yopt = y;
}
break;
} //:::::::::::::::::::::: end switch ::::::::::::::::::::::::::::::
} //================ end for (y=3;... =====================
} //================= end for (x=1;... ======================
return 1;
} //******************** end LabelCellsSign **********************
我们之前解释了这种方法在灰度图像的情况下是如何工作的。现在让我们解释一下它在彩色图像中的作用。
我们应该首先考虑应用于彩色数字图像的渐变概念。首先,在彩色图像的情况下,不存在单一的梯度,而是三个梯度,每个梯度对应于三原色之一:红色、绿色和蓝色。这些梯度中的每一个都由相邻像素中相应颜色的两个强度差来定义:作为 I ( x + 1,y–I(x, y )的 x 分量和作为 I ( x ,y的分量亮度 I ( x , y )是像素的属性( x , y ),其中 x 是列的编号, y 是行的编号。但是,一个 I ( x + 1,y)–I(x, y )或者 I ( x ,y+1)–I(x)在这种情况下,ACC 的概念是非常有用的:两个差异中的每一个可以被分配给位于一对像素之间的两个相应的裂缝(一维单元)中的一个。然后,应将梯度分配给与两个裂纹相关的点(零维单元)。
按照 Canny (1986)的正确思路,需要求梯度绝对值的最大值。在彩色图像的情况下,有必要决定应该使用三种颜色梯度值或它们的分量的哪种组合来正确地表示颜色的变化。一种可能性是使用三原色差的三个绝对值的和,而不是灰度值的差,正如我们在灰度图像的情况下所做的那样。但是,与灰度图像的情况一样,总和必须有符号。如果我们取颜色强度的有符号差的和,不同原色的差可以具有不同的符号,并且相互补偿,使得它们的和变小。我们的研究表明,彩色图像中边缘位置附近的三个颜色梯度具有非常不同的方向,因此它们实际上可以在总和中相互补偿。
我们已经决定使用分配有亮度差符号的绝对差之和。这是通过 ColorDifSign 方法实现的,如下所示。
int ColorDifSign(byte[] Colp, byte[] Colh)
{
int Dif = 0;
for (int c = 0; c < 3; c++) Dif += Math.Abs(Colp[c] - Colh[c]);
int Sign;
if (MaxC(Colp[2], Colp[1], Colp[0])-MaxC(Colh[2], Colh[1], Colh[0])>0)
Sign = 1;
else Sign = -1;
return (Sign * Dif) / 3;
}
在这段代码中,MaxC( )
是第三章中描述的方法,返回像素颜色的亮度。
为了研究方法LabelCellsSign
的功能,我们在项目WFdetectEdges1
中开发了一种工具,用于显示图像ExtremIm
的一条线的亮度变化、色差与边缘检测阈值的关系以及检测到的边缘垂直裂缝。用户可以点击右边图片框中的一个点。然后,在下面的第三个图片框中会出现从单击点开始的被单击线的放大部分。在每个上部图片框中,会出现一条水平线,显示下部框中显示的线条部分。图 7-5 以项目WFdetectEdges1
的形式为例。
图 7-5 中的下方曲线显示了图像SigmaIm
中一条线的亮度。绝对值大于阈值的色差在上部曲线中显示为彩色垂直线。红线表示正差异,绿线表示负差异。红线和绿线都表示检测到的边缘垂直裂纹的位置。
图 7-5
项目的形式WFdetectEdges1
图 7-6 显示了带有图 7-5 图像片段边缘的细胞复合体。这是通过稍后描述的方法DrawComb
的边缘表示。图 7-5 和图 7-6 中所示的边缘是通过LabelCellsSign
在色带中寻找最大绝对色差的方法产生的,色带是两个同质区域之间的一个窄条。这条带子的颜色变化很快。如你所见,这种方法效果很好。
图 7-6
在图 7-5 的图像片段中检测到的边缘
然而,使用我们的方法ExtremLightUni
,滑动窗口的尺寸相对较大,必须大于斜坡的宽度,这产生了在斜坡内看起来像细线的大部分窄边缘。因此,可以成功使用前面描述的更简单的方法LabelCells
。
为了证明我们的边缘检测方法的成功,我们复制了来自维基百科的文章“Canny Edge Detector”中的图像,并将我们的方法应用于该文章中的彩色图像。图 7-7 显示了这个图像的一个片段(a),我们的方法检测到的边缘(b),以及 Canny 边缘检测器检测到的边缘。
图 7-7
(a)图像的片段,(b)通过我们的方法检测的边缘,以及(c)通过 Canny 边缘检测器检测的边缘
二值化梯度方法的进一步改进
通过方法LabelCellsSign
计算出的边缘总是很细很细。然而,由于一些图像的不均匀结构,经常存在小块边缘,这会不必要地干扰边缘的进一步处理。因此,我们开发了方法CleanCombNew
,它由三部分组成。第一部分将看起来像正方形的四个裂缝的每个结构转换成由两个裂缝组成的角;第二部分删除了与分叉点关联的单个裂纹,这些裂纹有一个关联端点;第三部分删除包含的单元数量小于预定阈值的边的分量。这个阈值是CleanCombNew
的一个参数。
第一部分和第二部分的代码很简单,所以我们不做解释。然而,第三部分的代码相当复杂。在解释它之前,我应该提到我们已经开发了一个方法 DrawComb,它显示了一个放大的片段CombIm
,将裂缝表示为短白线,将点表示为小的彩色矩形。对于可被检测为具有单个裂纹的入射点的终点,颜色为红色。带有两条裂纹的点的颜色为绿色,带有三条裂纹的点的颜色为黄色,带有四条裂纹的点的颜色为紫色。用户可以通过用鼠标点击右边的图片框来选择片段的位置。单击点指定片段左上角的位置。片段的大小是标准的;它是以这样的方式选择的,即放大的片段适合左边图片框的窗口。图 7-8 显示了一个例子。所选择的片段在图 7-8 的右侧显示为一个白色小方块。
图 7-8
由DrawComb
表示的边缘片段
方法的第三部分CleanCombNew
必须找到边的连通分量并计算它们的单元数。连通分量的概念在图论中是众所周知的。请注意,表示为单元复合体的边是顶点为点、边为裂缝的图。一条边的连通分量是它的子集,其中任意两个元素存在包含这些元素的关联路径,关联路径是任意两个相邻元素关联的序列a1,a2,a3,… a n 。图的一条边与它的端点关联,而这些端点又与这条边关联。
通过众所周知的广度优先搜索方法来寻找边的连通分量。这是一个遍历图形的算法。它从图中的某个任意顶点开始,在移动到下一级邻居之前,首先探索邻居顶点。
在方法CleanCombNew
中,在一对for
循环中搜索起始点,其中变量x
和y
仅取偶数。请注意,我们的细胞复合体中的任何一点都有两个偶数坐标。
搜索以前未使用的点。使用的点获得标记的最高位7
(标记 128)。当找到一个带有标签1
、3
或4
的未使用点时,方法ComponClean
开始。它使用图像CombIm
的网格副本,因为ComponClean
标记了它使用的网格中的一些单元格,这些标记会干扰CombIm
的进一步使用。方法ComponClean
通过其子例程Trace
跟踪组件的所有单元,并对其进行计数。只要计数的细胞数量小于阈值Size
作为CleanCombNew
的参数,细胞的坐标就作为值x + width * y
保存在数组Index
中。扫描完一个组件的所有单元后,将它的编号与Size
进行比较:如果小于Size
,则删除保存的单元。图 7-9 显示了使用CleanCombNew
前后的边缘示例。
图 7-9
使用参数Size
= 21 的CleanCombNew
之前和之后的边缘(a)和(b)
Canny 边缘检测器
Canny (1986)著名的边缘检测器计算灰度值的梯度 G ( x , y )=|Sobel。x |+|索贝尔。Y|使用 Sobel 滤镜(第六章),计算边缘的方向(作为 45 的倍数),并删除边缘上不具有梯度绝对值| G ( x , y )|的局部最大值的所有候选。这是非最大值抑制 (NMS)的过程:如果像素( x , y )的值为| G ( x , y )|并且其不位于边缘方向的邻居的值更大,那么该值被设置为等于零。之后应用一个特殊的程序:它使用两个阈值T1T2。扫描图像,直到找到| G ( x , y )|的值大于 T 2 的像素。这个像素位于边缘。该边缘在两个方向上被追踪,并且所有值大于TT1的像素被标记为属于该边缘。在这最后一步之后,算法就完成了。
图 7-10 比较了 Canny 算法和我们的二值化梯度的结果。正如你所看到的,二值化梯度提供了更多的细节,并且比 Canny 算法简单。
图 7-10
(Canny 算法和(b)二值化梯度的结果
彩色图像中的边缘
拉普拉斯的过零方法和 Canny 算法都要求将彩色图像转换成灰度图像。然而,在这种转换之后,彩色图像中的一些边缘消失了。如果两个相邻区域颜色不同但亮度相同,就会出现这种情况。
二值化梯度可以检测具有相同亮度但不同颜色的两个区域之间的真实颜色边缘。为此,需要适用于彩色图像的 sigma 滤波器和极限滤波器。第二章介绍了彩色图像的西格玛滤波器。用于彩色图像的极端过滤器具有一个特殊的特征:它必须找到具有最大差异的两种颜色,而不是在小滑动窗口中找到灰度值的最大值和最小值。两种颜色之间的差异是红色、绿色和蓝色通道的差异的平方和。输出图像中的中心像素获得与输入图像的中心像素的颜色具有较小差异的两种最不同颜色的颜色。
如前所述,我们的边缘检测器使用ColorDifSign
方法计算两个相邻像素 P 1 (红色 1 ,绿色 1 ,蓝色 1 和 P 2 (红色 2 ,绿色 2 ,蓝色 2 )之间的色差,作为具有亮度差符号的颜色通道的绝对差的总和:
ColorDifSign =
sign of(light2–light 1)*(|红色2–红色1|+|绿色2–绿色1|+|蓝色2–蓝色 1 |)。
如果两个像素的色差为正且大于预定阈值,或者色差为负且小于(-阈值),则边缘位于两个像素之间。我们的方法的优点可以在具有不同颜色和相等强度的两个区域之间的边缘上看到,如图 7-11 中红色花朵和具有几乎相同亮度的绿色叶子之间的边缘。注意箭头指示的位置。如你所见,图 7-11d 中缺失了许多边缘。
图 7-11
(a)原始图像;(b)彩色图像的边缘;(c)原始转换成灰度值;(d)灰度图像的边缘
结论
我们的边缘检测器(改进的二值化梯度)提供了比众所周知的 Canny 算法更好的结果。我们的方法也检测边缘的彩色图像更精确的方法比使用不同的灰度值。**
八、一种新的图像压缩方法
提出了一种新的基于边缘检测的图像压缩方法。这个想法是基于这样的假设,即图像中的重要信息位于靠近边缘的像素中。所有其他像素仅包含颜色的均匀分布,这可以通过用有限差分法数值求解偏微分方程来重建。这里我们用上一章描述的方法进行边缘检测。我们将边缘表示为 ACC,如第七章所述。这种表示具有优势,因为边缘的元素是由两个相邻像素的颜色差异定义的。因此,边缘的元素既不属于这两个像素,而是属于位于这两个像素之间的空间元素。这种空间元素在数字图像中不存在;但是,它存在于 ACC 中。这是被称为裂缝的一维单元,而像素是二维单元。裂缝的两端是称为点的零维单元。当把像素看成小方块时,裂缝就是这些方块的边;点是他们的角,是裂缝的终点。
这些边缘构成了一个细线网络,这些细线是一系列裂纹和点,其中每条线段要么是一条闭合曲线,要么是一个序列,其起点和终点要么是三条或四条线段相交的分支点(图 8-1 )。
图 8-1
边缘的例子
这种结构可以用所谓的小区列表来描述。单元格列表的概念基于将图像表示为 ACC(参考 Kovalevsky (1989,2008))。
使用单元复合体对边界进行编码
将数字图像视为 ACC 通过对子集的边界进行编码带来了优势:表示为一系列裂缝和点的边界变成了具有零面积的细曲线;区域的边界和它的补集的边界是相同的。数字曲线尤其是数字直线的定义和处理变得更加简单和清晰。从图像的经济编码和精确重建的角度来看,最重要的优点是能够使用极其简单和快速的算法来填充裂纹边界的内部,这种算法在将边界表示为像素组时无法应用。
第七章简要总结了对本演示重要的拓扑概念。更多细节和拓扑基础请参考 Kovalevsky (2008)。
我们经常需要欧几里得坐标来讨论模拟图像的数字化问题。为了保持在细胞复合体的框架中,我们建议将欧几里得坐标视为具有相对较大分母的有理数。这对应于任何计算机模型,因为计算机中的浮点变量是具有大分母的有理数。为了对细胞复合体中的边缘进行编码,我们使用大小为(2 宽度+ 1)(2 高度+1)的图像,其中宽度高度是原始图像的大小。我们称该图像为组合坐标中的图像或CombIm
,因为该图像中被视为坐标的列数和行数指定了所有维度的细胞的位置(
零、一和二)。与通常只包含像素的图像相比,在该图像中找到不同尺寸的细胞和跟踪边缘更加方便。组合坐标中像素的两个坐标都是奇数;一个裂纹有一个奇数和一个偶数坐标;一个点有两个偶数坐标。对比图 8-2 。
图 8-2
零维、一维和二维细胞的组合坐标
边由一系列裂纹和点组成。边缘不包含像素。边缘的裂缝位于不同颜色的两个像素之间。入射到边缘裂缝的两个像素的颜色具有大于预定阈值的差异。
图像的边缘集合由线组成,线是裂缝和点的连接序列。一条线可以关闭;在这种情况下,它不包含端点和分支点。闭合线的每一点都与两条裂纹相关。一条非闭合线的起点和终点与一条、三条或四条裂纹相关,但不与两条裂纹相关。一条线的每个内点恰好与两条裂纹相关(图 8-3 )。
图 8-3
线条(粗体线段)、分支点和短线的示例
要完整地描述一幅图像的边缘,只需要一列线条。为了重建图像,知道与每条线的裂缝相关的像素的颜色就足够了。然而,这些颜色沿着一条线的一边几乎是不变的。如果位于一条线一侧的像素组中的颜色有很大变化,那么也会有垂直于该线的边缘裂纹;但是,没有这样的裂缝。因为线条一侧的颜色变化缓慢,所以将线条每一侧的颜色分别保存在线条开头和结尾的一个像素中就足够了。线的一侧的所有其他颜色可以通过在线的开始和结束处的这些颜色之间的插值来计算。
为了执行这种插值,需要知道线的所有元素的准确位置,这可以通过起点的坐标和线的所有裂纹的方向序列来指定。定向线的裂缝(即指定哪个点是起点)可以有四个方向之一:方向 0 向右,方向 1 向下,方向 2 向左,方向 3 向上。这样,可以非常节省地对图像的边缘线和相邻的颜色进行编码。图像边界的颜色对于重建也是必要的。只知道矩形图像四边的颜色就足够了。边界处的所有其他颜色都可以通过插值来计算。在该插值期间,也可以考虑与边界像素序列交叉的边缘裂缝处的颜色。
在对线侧的颜色和边界处的颜色进行插值之后,在线之间的区域中仍然存在大量像素,这些区域在所有插值之后仍然是空的。这些像素中的颜色可以通过沿着图像的行和列的线性插值来近似计算。这种插值必须从边界或线处的颜色开始,并且可以沿着一行继续,直到已经出现的颜色出现。可以沿着列进行类似的插值。然后每个像素获得两种颜色:一种来自沿行的插值,另一种来自沿列的插值。这两种颜色是平均的。经过这些插值后,颜色的分布不是很均匀。通过拉普拉斯偏微分方程的数字解进行平滑,可以使其更加均匀。经过这种平滑处理后,重建图像看起来与原始图像非常相似。
项目描述WFcompressPal
运行项目的形式如图 8-4 所示。
图 8-4
运行项目的形式WFcompressPal
当用户点击打开图像时,他或她可以选择一个图像,该图像将作为原始图像OrigBmp
打开并显示在左侧图片框中。在项目的这个区块中定义了八个工作图像。必须将OrigBmp
转换成工作图像OrigIm
。软件WindowsForms
建议通过GetPixels
的方法访问位图的像素,这个方法相当慢。我们开发了一个更快的方法BitmapToImage
,它使用了类Bitmap
的方法LockBits
来处理彩色图像。然而,如果原始图像是每像素 8 比特的图像,则意味着打开的图像是索引图像。方法LockBits
不应该用于每像素 8 位的索引位图,因为这些位图包含一个调色板,像素值总是由调色板值定义。因此,LockBits
方法对于 8 位索引位图来说太慢了。对于这样的位图,我们使用标准方法GetPixel
。除了EdgeIm
和MaskIm
之外的所有工作图像将被定义为 24 位图像。图像EdgeIm
和MaskIm
始终是 8 位图像。
现在,用户可以决定是否抑制脉冲噪声。当原始图像不包含脉冲噪声时,该过程也是有用的,因为去除亮度不同于周围像素亮度的小斑点使得图像更加均匀,并且人类感知的图像质量保持不变。使图像更加均匀会提高压缩率。
如果用户决定使用脉冲噪声抑制,他或她可以改变要去除的斑点的最大尺寸,而带有删除暗和删除亮标签的工具的值。只需将“删除暗”和“删除亮”的值设置为零,就可以跳过此过程。用户可以点击脉冲噪声。第二章描述了抑制脉冲噪声的方法。
现在有必要对图像进行分割。当用户点击 Segment 时,脉冲噪声被抑制的图像将被 sigma 滤波器抑制高斯噪声(第二章)和极值滤波器增加相邻像素之间的色差(第六章)处理。极值过滤器的处理结果保存在图像ExtremIm
中。然后,在彩色图像的情况下,将计算代表被处理图像的颜色的 256 种颜色的调色板Palet
,并且创建具有该调色板的索引图像Pal
。图像SegmentIm
是通过将索引图像Pal
转换成真彩色图像而获得的真彩色图像。它显示在右边的图片框中。
调色板的计算通过MakePalette
方法进行。这个方法创建了一个包含 256 种颜色的数组Palette
,并提供了颜色编号,称为indices
。Palette
的颜色近似代表原始图像中出现的所有颜色。
方法MakePalette
首先在空间中指定一个三维立方体,其坐标轴对应于红色、绿色和蓝色通道。立方体有大小
[最小[c],最大[c]],c = 0,1,2
其中Min[c]
是图像中出现的颜色通道c
的最小值,Max[c]
是最大值。因此,这个三维颜色立方体包含了图像中出现的所有颜色。颜色的立方体细分为 10×10×10 = 1000 个小立方体。颜色立方体中小立方体的位置由三个坐标iComp[c], c = 0, 1, 2
指定。每个数字iComp[c]
可以取 0 到 9 的值。根据预定值MaxComp[3]={9, 9, 9}
,通过将像素颜色的三个通道值(红色、绿色和蓝色)除以值Div[c]
,为图像的每个像素计算三个数字iComp[c], c = 0, 1, 2
。三个数字iComp[c]
指定了值index
,该值可以在 0 到 999 之间。index
指定像素颜色所属的小立方体的位置。index
的值为:
并且在方法MakePalette
中计算取决于MaxComp
的值Weight[c]
。方法MakePalette
为每个小立方体计算颜色位于该小立方体中的原始图像的像素数。
非空小立方体的数量指定了调色板中颜色的数量。我们希望这个数字在 200 到 255 之间。如果低于 200,则MaxComp[1]
(绿色通道)的值增加。因此,小立方体的数量增加,并且活动调色板元素的数量也随之增加。如果提到的数字超过最大值 255(我们为索引 0 保留调色板的一个值为(0,0,0)),则MaxComp[1]
减少。因此,活动小立方体的数量获得 200 和 255 之间的值。在小图像的情况下,这种控制过程不起作用。当MaxComp[1]
大于 20 时中断,在这种情况下,活动调色板元素的数量可能超出所需的间隔。
方法MakePalette
为每个非空的小立方体计算属于该立方体的所有颜色的平均值。还计算颜色的标准偏差(这部分代码在下面的文本版本中被省略)。感兴趣的标准偏差只是给出计算的调色板如何精确地表示原始图像的颜色的概念。
平均值用作调色板的值。对非空的小立方体进行计数,计数值是调色板中使用的颜色(或颜色号)的新索引数。调色板保存为数组。MakePalette
创建调色板图像Pal
。颜色索引从图像Pal
复制到图像Comb
的像素中。这里是MakePalette
的源代码。
public int MakePalette(CImage Img, int[] Palet, Form1 fm1)
// Produces a palette of 256 elements optimally representing colors of the image "Img"
{
bool deb = false;
int MaxN = 1000, ResIndex = 10000;
int[] Sum = new int[3 * MaxN];
int[] nPix = new int[MaxN]; // "nPix[Index]" is number of pixels with index
double[] Mean = new double[3 * MaxN];
int[] Sum0 = new int[3 * ResIndex];
int[] nPix0 = new int[ResIndex];
int c, i, jump, nIndex, n, MaxIndex;
int[] iChan = new int[3], Div = new int[3], Weight = new int[3];
int[] NumbInterv = { 11, 12, 9 };
// Computing minimum and maximum of the color channels:
int[] Min = { 256, 256, 256 }, Max = { 0, 0, 0 };
fm1.progressBar1.Visible = true;
fm1.progressBar1.Step = 1;
if (Img.width * Img.height > 300) jump = Img.width * Img.height / 20;
else jump = 3;
for (i = 0; i < Img.width * Img.height; i++) //=======================
{
if (i % jump == jump - 1) fm1.progressBar1.PerformStep();
for (c = 0; c < 3; c++)
{
if (Img.Grid[3 * i + c] < Min[c]) Min[c] = Img.Grid[3 * i + c];
if (Img.Grid[3 * i + c] > Max[c]) Max[c] = Img.Grid[3 * i + c];
}
} //=============================== end for (i... =============
int nIndexMin = 228, nIndexMax = 255;
nIndex = 0;
// Changing NumbInterv[] to get nIndex between nIndexMin and nIndexMax:
do //===================================================
{
Weight[0] = 1;
Weight[1] = NumbInterv[0] + 1;
Weight[2] = NumbInterv[0] + Weight[1] * NumbInterv[1] + 1;
for (i = 0; i < ResIndex; i++) Sum0[i] = 0;
for (i = 0; i < 3 * MaxN; i++) //==================
{
Sum[i] = 0;
Mean[i] = 0.0;
nPix[i / 3] = 0;
} //================== end for (i ... ===========
for (c = 0; c < 3; c++)
{
Div[c] = (int)(0.5 + (double)(Max[c] - Min[c]) / (double)NumbInterv[c]);
if (Div[c] == 0) Div[c] = 1;
if ((Max[c] - Min[c]) / Div[c] > NumbInterv[c]) NumbInterv[c]++;
}
MaxIndex = Weight[0] * NumbInterv[0] + Weight[1] * NumbInterv[1] +
Weight[2] * NumbInterv[2];
int maxIndex = 0;
if (MaxIndex >= ResIndex)
{ MessageBox.Show("MakePalette, Overflow: MaxIndex=" +
MaxIndex + " > ResIndex=" + ResIndex + "; return -1.");
return -1;
}
for (i = 0; i < ResIndex; i++) nPix0[i] = 0;
int Index = 0;
for (i = 0; i < Img.width * Img.height; i++) //=======================
{
Index = 0;
for (c = 0; c < 3; c++)
{
iChan[c] = (Img.Grid[3 * i + c] - Min[c]) / Div[c];
Index += Weight[c] * iChan[c];
}
if (Index > maxIndex) maxIndex = Index;
if (Index > ResIndex - 1)
{
MessageBox.Show("MP, Overflow: Index=" + Index + " too great; return -1.");
return -1;
}
for (c = 0; c < 3; c++) Sum0[3 * Index + c] += Img.Grid[3 * i + c];
nPix0[Index]++;
} //================ end for (i = 0; ... ============================
nIndex = 0;
for (i = 0; i <= MaxIndex; i++) if (nPix0[i] > 0) nIndex++;
int minInd = 0, maxInd = 0;
if (nIndex < nIndexMin)
{
if (NumbInterv[0] <= NumbInterv[1] && NumbInterv[0] <= NumbInterv[2])
minInd = 0;
else
if (NumbInterv[1] <= NumbInterv[0] && NumbInterv[1] <= NumbInterv[2])
minInd = 1;
else
if (NumbInterv[2] <= NumbInterv[0] && NumbInterv[2] <= NumbInterv[1])
minInd = 2;
NumbInterv[minInd]++;
}
if (nIndex > nIndexMax)
{
if (NumbInterv[0] >= NumbInterv[1] && NumbInterv[0] >= NumbInterv[2])
maxInd = 0;
else
if (NumbInterv[1] >= NumbInterv[0] && NumbInterv[1] >= NumbInterv[2])
maxInd = 1;
else
if (NumbInterv[2] >= NumbInterv[0] && NumbInterv[2] >= NumbInterv[1])
maxInd = 2;
NumbInterv[maxInd]--;
}
if (nIndex >= nIndexMin && nIndex <= nIndexMax || NumbInterv[1] > 20)
{
if (deb)
MessageBox.Show("MakePalette: nIndex=" + nIndex + " is OK. break.");
break;
}
} while (nIndex > nIndexMax || nIndex < nIndexMin); //===================
int[] NewIndex = new int[MaxIndex];
if (MaxIndex > 300) jump = MaxIndex / 20;
else jump = 3;
for (i = n = 0; i < MaxIndex; i++) //===================================
{
if (i % jump == jump - 1) fm1.progressBar1.PerformStep();
if (nPix0[i] > 0) //---------------------------------------------------
{
n++;
if (n > MaxN - 1)
{
MessageBox.Show("MP: Overflow in Sum; n=" + n + "< MaxN=" + MaxN);.
return -1;
}
NewIndex[i] = n;
nPix[n] = nPix0[i];
for (c = 0; c < 3; c++)
{
Sum[3 * n + c] = Sum0[3 * i + c];
if (nPix[n] > 0) Mean[3 * n + c] = (double)(Sum[3 * n + c]) / (double)(nPix[n]);
else Mean[3 * n + c] = 0;
}
} //-------------------------- end if (nPix0... -----------------------
} //====================== end for (i... ===========================
int MaxNewIndex = n;
if (Img.width * Img.height > 300) jump = Img.width * Img.height / 20;
else jump = 3;
// Putting NewIndex into "this.Grid":
for (i = 0; i < Img.width * Img.height; i++) //==========================
{
if (i % jump == jump - 1) fm1.progressBar1.PerformStep();
int Index = 0;
for (c = 0; c < 3; c++)
{
iChan[c] = (Img.Grid[3 * i + c] - Min[c]) / Div[c];
Index += Weight[c] * iChan[c];
}
if (Index >= MaxIndex) Index = MaxIndex - 1;
Grid[i] = (byte)NewIndex[Index];
if (Grid[i] == 0) Grid[i] = (byte)MaxNewIndex;
} //=============== end for (i=0; ... ===============================
// Calculating "Palet" and "this.Palette":
byte R = 0, G = 0, B = 0;
jump = MaxNewIndex / 20;
for (n = 0; n <= MaxNewIndex; n++)
{
if (n % jump == jump -1) fm1.progressBar1.PerformStep();
if (n > 0)
{
if (Mean[3 * n + 2] < 255.0) R = (byte)Mean[3 * n + 2];
if (Mean[3 * n + 1] < 255.0) G = (byte)Mean[3 * n + 1];
if (Mean[3 * n + 0] < 255.0) B = (byte)Mean[3 * n + 0];
Palet[n] = RGB(R, G, B);
}
else
{
Palet[n] = 0;
}
}
return 1;
} //******************* end MakePalette ********************************
现在可以执行边缘检测。用户可以选择用于边缘检测的阈值,该阈值指定两个相邻像素的颜色之间的最小差值,在该值处,这些像素之间的裂纹将被标记为属于边缘。用户可以在标签阈值下的工具numericUpDown
的小窗口中输入一个新值,或者点击该设置右侧的两个小箭头之一来增加或减少该值。用户应该考虑,在较高的阈值下,将产生较少量的边缘元素。这导致更高的压缩率,但也降低了恢复图像的质量。
设计用于检测边缘的项目部分使用组合坐标(参见第七章)和更大的尺寸(2 宽度+ 1)(2 *高度+1)定义了每像素一个字节的图像CombIm
。这是必要的,因为在标准尺寸的图像中表示裂缝和点是困难的。图像CombIm
将包含代表检测到的边缘的细胞复合体。在第七章中可以找到对细胞复合体的描述。方法LabelCellsSign
测试图像的每对相邻像素ExtremIm
。如果像素的色差大于阈值,则这些像素之间的裂缝用1
标记。这意味着对应于该裂纹的图像元素CombIm
获得值1
。同时,裂纹端点的标记将增加,从而获得与该点相关的裂纹数量相等的标记。点的标签示例如图 8-5 所示。
图 8-5
维和一维单元格的标签
如前所述,调色板索引从由MakePalette
产生的图像Pal
复制到图像Comb
的像素中。在灰度图像的情况下,灰度值是从Image3
复制的,通过ExtremVar
方法进行处理。
现在可以开始零件Encode
了。当用户点击编码时,使用图像CombIm
作为参数来启动方法SearchLin
。该方法查看CombIm
的所有点,并为相应字节的最低有效位中标记为 1、3 或 4 的每个点调用方法ComponLin
。这些是边缘线的端点或分支点。由 SearchLin 调用的方法ComponLin
跟踪和编码连接组件的所有行。追踪过程中遇到的所有点的标签将被删除。
在SearchLin
处理完标签为 1、3 或 4 的所有点后,它寻找标签为 2 的未删除点。这些点属于没有端点和分支点的组件。这样的组件是一条闭合曲线。
这里是SearchLin
的代码。
public int SearchLin(ref CImage Comb, Form1 fm1)
{ int Lab, rv, x, y;
for (x=0; x<MaxByte; x++) Byte[x]=0;
if (Step[0].Y < 0) x = Step[0].Y;
fm1.progressBar1.Value = 0;
int y1 = Comb.nLoop * CNY / Comb.denomProg;
for (y=0; y<CNY; y+=2)
{
if ((y % y1) == 0) fm1.progressBar1.PerformStep();
for (x=0; x<CNX; x+=2)
{ Lab=Comb.Grid[x+CNX*y] & 3;
if (Lab==1 || Lab==3)
{ rv=ComponLin(Comb.Grid, x, y);
if (rv<0)
{ MessageBox.Show("SearchLin, Alarm! ComponLin returned " + rv);
return -1;
}
}
}
}
// Starting the search for loops:
for (y=0; y<CNY; y+=2)
for (x=0; x<CNX; x+=2)
{ Lab=Comb.Grid[x+CNX*y] & 3;
if (Lab==2)
{ rv=ComponLin(Comb.Grid, x, y);
if (rv<0)
{ MessageBox.Show("SearchLin, Alarm! ComponLin returned " + rv);
return -1;
}
}
}
nByte++;
return nByte;
} //********************* end SearchLin ********************************
方法ComponLin
跟踪并编码每个完整组件;即边的连接部分。这种方法被设计用于追踪连通图,因为边的连通分量是具有点的顶点和具有线的边的连通图。众所周知,遍历或搜索树或图数据结构可以通过广度优先算法完成,该算法使用称为队列的先进先出数据结构。
ComponLin
将组件的起点放入队列。当从队列中获得一个点时,它测试与该点相关的所有四个裂纹。如果这些裂纹中的一条终止于一个端点或一个分支点,那么它被认为是一条由单个裂纹组成的短线。它将立即被编码到数据结构CListLines
中,其中ComponLin
是一个方法。
如果裂纹在标记为 2 的点处结束,则方法TraceLin
开始。该方法跟踪一条长线,保存该线两侧的颜色索引或灰度值,并保存该线所有裂缝的方向。当TraceLin
返回且处理后的图像是彩色图像时,则启动方法FraqInds
,该方法计算该行的两半中最频繁的索引,并将它们分配给该行的存储代码。在灰度图像的情况下,启动AverageGrays
,计算线的两半的平均灰度值。然后将平均灰度值分配给该行的代码。
下面是方法TraceLin
和ComponLin
的代码:
public int TraceLin(byte[] CGrid, int X, int Y, ref iVect2 Pterm, ref int dir)
/* This method traces a line in the image "Comb" with combinatorial coordinates, where the cracks and points of the edges are labeled: bits 0, 1 and 2 of a point contain the label 1 to 4 of the point. The label indicates the number of incident edge cracks. Labeled bit 6 indicates that the point already has been put into the queue; labeled bit 7 indicates that the point should not be used any more. The crack has only one label 1 in bit 0\. This method traces the edge from one end or branch point to another while changing the parameter "dir". ----------*/
{ bool atSt_P=false, BP=false, END=false;
int rv = 0;
iVect2 Crack, P=new iVect2(0,0), PixelP, PixelN, StartPO;
int iShift=-1, iCrack=0, Lab;
P.X=X; P.Y=Y;
StartPO=P;
if (nLine2==0) nByte=0;
else nByte=Line2[nLine2-1].EndByte+1;
int[] Shift={0,2,4,6};
while(true) //================================================
{ Crack=P+Step[dir];
P=Crack+Step[dir];
Lab=CGrid[P.X+CNX*P.Y]&3;
switch(Lab)
{ case 1: END=true; BP=false; rv=1; break;
case 2: BP=END=false; break;
case 3: BP=true; END=false; rv=3; break;
}
PixelP=Crack+Norm[dir];
PixelN=Crack-Norm[dir];
IndPos[iCrack]=CGrid[PixelP.X+CNX*PixelP.Y];
IndNeg[iCrack]=CGrid[PixelN.X+CNX*PixelN.Y];
if (Lab==2) CGrid[P.X+CNX*P.Y]=0;
iShift++;
iCrack++;
if (iShift==4)
{ iShift=0;
if (nByte<MaxByte-1) nByte++;
else
{ return -1;
}
}
Byte[nByte] |= (byte)(dir << Shift[iShift]);
if (P.X == StartPO.X && P.Y == StartPO.Y) atSt_P = true;
else atSt_P = false;
if (atSt_P)
{ Pterm=P;
rv=2;
break;
}
if (!BP && !END) //------------------------------------------
{ Crack=P+Step[(dir+1)%4];
if (CGrid[Crack.X+CNX*Crack.Y]==1)
{ dir=(dir+1)%4;
}
else
{ Crack=P+Step[(dir+3)%4];
if (CGrid[Crack.X+CNX*Crack.Y]==1)
{ dir=(dir+3)%4;
}
}
}
else
{ Pterm=P;
break;
} //----------------- end if (!BP && !END) --------------------
} //=========== end while =====================================
Line2[nLine2].EndByte=nByte;
Line2[nLine2].nCrack=(ushort)iCrack;
return rv;
} //*************** end TraceLin ****************************************
public int ComponLin(byte[] CGrid, int X, int Y)
/* Encodes in "CListLines" the lines of the edge component with the point (X, Y) being a branch or an end point. Puts the starting point 'Pinp' into the queue and starts the 'while' loop. It tests each labeled crack incident with the point 'P' fetched from the queue. If the next point of the crack is a branch or an end point, then a short line is saved. Otherwise the method "TraceLin" is called. "TraceLin" traces a long line, saves the color indices at the sides of the line and ends at the point 'Pterm' with the direction 'DirT'. Then the method "FreqInds" assigns the most frequent from the saved color indices to the line. If the point 'Pterm' is a branch point then it is put to the queue. "ComponLin" returns when the queue is empty. ---------------*/
{ int dir, dirT;
int LabNext, rv;
iVect2 Crack, P, Pinp, PixelN, PixelP, Pnext, Pterm=new iVect2(0, 0);
Pinp=new iVect2(X,Y); // comb. coord.
pQ.Put(Pinp);
while(pQ.Empty()==false) //======================================
{ P=pQ.Get();
for (dir=0; dir<4; dir++) //======================================
{ Crack=P+Step[dir];
if (Crack.X<0 || Crack.X>CNX-1 || Crack.Y<0 || Crack.Y>CNY-1 ) continue;
if (CGrid[Crack.X+CNX*Crack.Y]==1) //----------------------------
{ PixelP=Crack+Norm[dir]; PixelN=Crack-Norm[dir];
Pnext=Crack+Step[dir];
LabNext=CGrid[Pnext.X+CNX*Pnext.Y] & 3; //Ind0
if (LabNext==1 || LabNext==3)
{
Line1[nLine1].x = (ushort)(P.X / 2); Line1[nLine1].y = (ushort)(P.Y / 2);
Line1[nLine1].Ind0=CGrid[PixelN.X+CNX*PixelN.Y];
Line1[nLine1].Ind1=CGrid[PixelP.X+CNX*PixelP.Y];
if (nLine1>MaxLine1-1)
{
MessageBox.Show("ComponLin: Overflow in Line1; return -1");
return -1;
}
}
if (LabNext==3) pQ.Put(Pnext);
if (LabNext==2) //--------------------------------------------
{ Line2[nLine2].x=(ushort)(P.X/2);
Line2[nLine2].y=(ushort)(P.Y/2); //transf. to standard coordinates
dirT=dir;
rv=TraceLin(CGrid, P.X, P.Y, ref Pterm, ref dirT);
if (nBits3==24) FreqInds(nLine2);
else AverageGray(nLine2);
if (rv<0)
{
return -1;
}
if (nLine2>MaxLine2-1)
{ return -1;
}
else nLine2++;
if ((CGrid[Pterm.X+CNX*Pterm.Y] & 64)==0) // '64'= label for visited;
{
if (rv==3) //------------------------------------------
{ CGrid[Pterm.X+CNX*Pterm.Y] |=64;
pQ.Put(Pterm);
}
} //-------- end if ((CGrid[Pterm.X... --------------------
} // ---------- end if (LabNest==2) ---------------------------
if ((CGrid[P.X+CNX*P.Y]&3)==1) break;
} //--------------- end if (CGrid[Crack.X ...==1) ----------------
} //========== end for (dir ... ===================================
CGrid[P.X+CNX*P.Y]=0;
} //============ end while ======================================
return 1;
} //*************** end ComponLin *************************************
当SearchLin
返回并且所有行的代码都保存在CListLines
类的对象List
中时,创建CListCode
类的对象LiCod
。这个类是CListLines
类的简短版本。它只包含重建图像所需的信息。方法Transform
将必要的信息从List
复制到LiCod
,并将数据转换成包含在数组ByteNew
中的字节序列。为了避免使用方法Serialize
在磁盘上保存代码,这个转换是必要的,因为Serialize
产生的磁盘文件有时比系统方法Write
产生的文件长十倍。方法Serialize
因此会破坏压缩,因此我们不使用它。
下面是Transform
的代码。
public int Transform(int nx, int ny, int nbits, int[] Palet, CImage Comb, CListLines L, Form1 fm1)
// Transforms the provisional list "L" to an object of the class "CListCode".
{ int i, ib, il, nCode=0;
width=nx; height=ny; nBits=nbits;
nCode+=3*4;
nLine1=L.nLine1; nLine2=L.nLine2; nByte=L.nByte;
nCode+=3*4;
nCodeAfterLine2 = nCode;
Palette=new int[256];
for ( i=0; i<256; i++) Palette[i]=Palet[i]; nCode+=256*4;
Corner[0]=Comb.Grid[1+(2*width+1)*1]; // this is a gray value or a palette index
Corner[1]=Comb.Grid[2*width-1+(2*width+1)*1];
Corner[2]=Comb.Grid[2*width-1+(2*width+1)*(2*height-1)];
Corner[3]=Comb.Grid[1+(2*width+1)*(2*height-1)];
nCode+=4;
int il1=Comb.nLoop*nLine1/Comb.denomProg;
for (il = 0; il < nLine1; il++)
{
Line1[il] = L.Line1[il];
if (((il + 1) % il1) == 0) fm1.progressBar1.PerformStep();
}
nCode += nLine1*6; // "6" is the sizeof(CCrack);
int il2 = Comb.nLoop * nLine2 / Comb.denomProg;
for (il = 0; il < nLine2; il++)
{ Line2[il]=L.Line2[il];
if (((il + 1) % il2) == 0) fm1.progressBar1.PerformStep();
}
nCode += nLine2 * 14; // sizeof(CLine);
int il3 = Comb.nLoop * nByte / Comb.denomProg;
for (ib = 0; ib < nByte; ib++)
{ Byte[ib]=L.Byte[ib];
if (((ib + 1) % il3) == 0) fm1.progressBar1.PerformStep();
}
nCode += nByte;
// The following code is necessary to avoid "Serialize":
ByteNew = new byte[nCode+4];
for (int ik = 0; ik < nCode + 4; ik++) ByteNew[ik] = 0;
int j = 0;
ByteNew[j] = (byte)(nCode & 255); j++;
ByteNew[j] = (byte)((nCode >> 8) & 255); j++;
ByteNew[j] = (byte)((nCode >> 16) & 255); j++;
ByteNew[j] = (byte)((nCode >> 24) & 255); j++;
ByteNew[j] = (byte)(nx & 255); j++;
ByteNew[j] = (byte)((nx >> 8) & 255); j++;
ByteNew[j] = (byte)((nx >> 16) & 255); j++;
ByteNew[j] = (byte)((nx >> 24) & 255); j++;
ByteNew[j] = (byte)(ny & 255); j++;
ByteNew[j] = (byte)((ny >> 8) & 255); j++;
ByteNew[j] = (byte)((ny >> 16) & 255); j++;
ByteNew[j] = (byte)((ny >> 24) & 255); j++;
ByteNew[j] = (byte)(nbits & 255); j++;
ByteNew[j] = (byte)((nbits >> 8) & 255); j++;
ByteNew[j] = (byte)((nbits >> 16) & 255); j++;
ByteNew[j] = (byte)((nbits >> 24) & 255); j++;
ByteNew[j] = (byte)(nLine1 & 255); j++;
ByteNew[j] = (byte)((nLine1 >> 8) & 255); j++;
ByteNew[j] = (byte)((nLine1 >> 16) & 255); j++;
ByteNew[j] = (byte)((nLine1 >> 24) & 255); j++;
ByteNew[j] = (byte)(nLine2 & 255); j++;
ByteNew[j] = (byte)((nLine2 >> 8) & 255); j++;
ByteNew[j] = (byte)((nLine2 >> 16) & 255); j++;
ByteNew[j] = (byte)((nLine2 >> 24) & 255); j++;
ByteNew[j] = (byte)(nByte & 255); j++;
ByteNew[j] = (byte)((nByte >> 8) & 255); j++;
ByteNew[j] = (byte)((nByte >> 16) & 255); j++;
ByteNew[j] = (byte)((nByte >> 24) & 255); j++;
for (int ii = 0; ii < 256; ii++)
{
ByteNew[j] = (byte)(Palet[ii] & 255); j++;
ByteNew[j] = (byte)((Palet[ii] >> 8) & 255); j++;
ByteNew[j] = (byte)((Palet[ii] >> 16) & 255); j++;
ByteNew[j] = (byte)((Palet[ii] >> 24) & 255); j++;
}
for (int i1 = 0; i1 < 4; i1++) ByteNew[j+i1] = Corner[i1];
j+=4;
for (int i2 = 0; i2 < nLine1; i2++)
{
ByteNew[j] = (byte)(L.Line1[i2].x & 255); j++;
ByteNew[j] = (byte)((L.Line1[i2].x >> 8) & 255); j++;
ByteNew[j] = (byte)(L.Line1[i2].y & 255); j++;
ByteNew[j] = (byte)((L.Line1[i2].y >> 8) & 255); j++;
ByteNew[j] = L.Line1[i2].Ind0; j++;
ByteNew[j] = L.Line1[i2].Ind1; j++;
}
for (int i3 = 0; i3 < nLine2; i3++)
{
ByteNew[j] = (byte)(L.Line2[i3].EndByte & 255); j++;
ByteNew[j] = (byte)((L.Line2[i3].EndByte >> 8) & 255); j++;
ByteNew[j] = (byte)((L.Line2[i3].EndByte >> 16) & 255); j++;
ByteNew[j] = (byte)((L.Line2[i3].EndByte >> 248) & 255); j++;
ByteNew[j] = (byte)(L.Line2[i3].x & 255); j++;
ByteNew[j] = (byte)((L.Line2[i3].x >> 8) & 255); j++;
ByteNew[j] = (byte)(L.Line2[i3].y & 255); j++;
ByteNew[j] = (byte)((L.Line2[i3].y >> 8) & 255); j++;
ByteNew[j] = (byte)(L.Line2[i3].nCrack & 255); j++;
ByteNew[j] = (byte)((L.Line2[i3].nCrack >> 8) & 255); j++;
ByteNew[j] = L.Line2[i3].Ind0; j++;
ByteNew[j] = L.Line2[i3].Ind1; j++;
ByteNew[j] = L.Line2[i3].Ind2; j++;
ByteNew[j] = L.Line2[i3].Ind3; j++;
}
for (int i4 = 0; i4 < nByte; i4++) ByteNew[j + i4] = L.Byte[i4];
j += nByte;
return nCode;
} //************************** end Transform ******************************
当方法Transform
返回时,显示消息“图像已编码”,并显示代码长度和压缩率的值。
项目WFcompressPal
还包含一个通过单击 Restore 调用的部分,这是从代码重建图像所必需的。这是必需的,因为用户必须看到重建图像的质量是否足够。否则,用户可以指定较低的阈值来获得更精细的边缘。这自然会导致压缩率变小。项目的这一部分与下一节描述的项目WFrestoreLin
的相应部分相同。
单击 Save code 调用方法WriteCode
,该方法将数组ByteNew
和代码写入扩展名为*.dat
的文件中。
如果用户没有按正确的顺序点击按钮,他或她会得到一个警告和正确顺序的指示。
项目WFrestoreLin
这个项目用于从保存的代码中重建图像,并将图像存储在磁盘上。该项目从读取一个选择的扩展名为*.dat
的文件开始。系统方法Read
首先读取代码的长度,这使得分配字节数组ByteNew
成为可能,然后将代码读入数组ByteNew
。类CListCode
的构造器将ByteNew
的字节转换成类CListCode
的对象LiCod
。该对象包含要重建的图像的参数;数组Line1
、Line2
和Byte
的长度;调色板;图像四个角的调色板索引;这些数组。
当用户点击恢复时,用LiCod
: RestoreIm
中包含的参数定义两幅图像进行重建,MaskIm
作为辅助图像。两幅图像的所有像素都被设置为零。然后方法Restore
重建显示在pictureBox1
中的图像。在项目WFcompressPal
中也使用这种方法来重建编码图像,以便用户可以评估重建图像的质量。
方法Restore
首先将图像角的索引或颜色放入相应的像素。如果要重建的图像是彩色图像,则Restore
通过调色板将读取的索引转换成颜色。
对应于获得颜色或灰度级的像素Image
的图像像素Mask
总是被设置为LabMask=250
。(选择这个高值是为了能够在调试时显示Mask
的内容。)
然后Restore
读取具有短线代码的数组Line1
并将颜色或灰度值放入短线的单个裂缝两侧的像素中。如果一条线只有一条裂纹,那么这条线就是短线。其他的都是长线。Restore
然后读取带有长行代码的数组Line2
,并将颜色或灰度值放入每一长行开头和结尾的像素中。Restore
下一步沿着长线的边执行颜色或灰度值的插值来完成它的工作。
这里是Restore
的代码。
public int Restore(ref CImage Image, ref CImage Mask, Form1 fm1)
{ int dir, nbyte, x, y;
byte LabMask=250;
if (nBits==24) nbyte=3;
else nbyte=1;
fm1.progressBar1.Value = 0;
fm1.progressBar1.Visible = true;
int denomProg = fm1.progressBar1.Maximum / fm1.progressBar1.Step;
int Sum = nLine1 + nLine2;
int i1 = Sum / denomProg;
for (int i = 0; i < width * height * (nBits / 8); i++) Image.Grid[i] = 0;
for (int i=0; i<width*height; i++) Mask.Grid[i]=0;
if (nBits==24)
{ for (int c=0; c<nbyte; c++)
{ Image.Grid[c]=(byte)((Palette[Corner[0]]>>8*(2-c)) & 0XFF); // left below
Image.Grid[nbyte*(width-1)+c]=
(byte)((Palette[Corner[1]]>>8*(2-c)) & 0XFF); // right below
Image.Grid[nbyte*width*height-nbyte+c]=
(byte)((Palette[Corner[2]]>>8*(2-c)) & 0XFF); // right on top
Image.Grid[nbyte*width*(height-1)+c]=
(byte)((Palette[Corner[3]]>>8*(2-c)) & 0XFF); // left on top
}
}
else
{ Image.Grid[0]=Corner[0];
Image.Grid[width-1]=Corner[1];
Image.Grid[width*height-1]=Corner[2];
Image.Grid[0+width*(height-1)]=Corner[3];
}
Mask.Grid[0]=Mask.Grid[width-1]=Mask.Grid[width*height-1]=
Mask.Grid[width*(height-1)]=LabMask;
// Short lines:
fm1.progressBar1.Value = 0;
for (int il = 0; il < nLine1; il++) //===================================
{
if ((il % i1) == 0) fm1.progressBar1.PerformStep();
dir=((Line1[il].x>>14) & 2) | (Line1[il].y>>15);
x=Line1[il].x & 0X7FFF; y=Line1[il].y & 0X7FFF;
if (nBits==24)
{ switch(dir)
{ case 0:
if (y > 0)
{
for (int c = 0; c < nbyte; c++)
{
int Index = Line1[il].Ind1;
byte col = (byte)(Palette[Index] >> 8 * c);
Image.Grid[nbyte * (x + width * y) + 2 - c] = col;
Image.Grid[nbyte * (x + width * (y - 1)) + 2 - c] =
(byte)((Palette[Line1[il].Ind0] >> 8 * c) & 0XFF);
}
Mask.Grid[x + width * y] = Mask.Grid[x + width * (y - 1)] = LabMask;
}
break;
case 1:
for (int c = 0; c < nbyte; c++)
{ Image.Grid[nbyte*(x+width*y)+2-c]=(byte)((Palette[Line1[il].Ind0]>>8*c) & 0XFF);
Image.Grid[nbyte*(x-1+width*y)+2-c]=(byte)((Palette[Line1[il].Ind1]>>8*c) & 0XFF);
}
Mask.Grid[x+width*y]=Mask.Grid[x-1+width*y]=LabMask;
break;
case 2:
for (int c = 0; c < nbyte; c++)
{ Image.Grid[nbyte*(x-1+width*y)+2-c]=(byte)((Palette[Line1[il].Ind0]>>8*c) & 0XFF);
Image.Grid[nbyte*(x-1+width*(y-1))+2-c]=(byte)((Palette[Line1[il].Ind1]>>8*c) & 0XFF);
}
Mask.Grid[x-1+width*y]=Mask.Grid[x-1+width*(y-1)]=LabMask;
break;
case 3:
for (int c = 0; c < nbyte; c++)
{ Image.Grid[nbyte*(x+width*(y-1))+2-c]=(byte)((Palette[Line1[il].Ind1]>>8*c) & 0XFF);
Image.Grid[nbyte*(x-1+width*(y-1))+2-c]=(byte)((Palette[Line1[il].Ind0]>>8*c) & 0XFF);
}
Mask.Grid[x+width*(y-1)]=Mask.Grid[x-1+width*(y-1)]=LabMask;
break;
} //:::::::::::::::::::::: end switch ::::::::::::::::::::::::::::::::
}
else
{ switch(dir)
{ case 0: Image.Grid[x+width*y]=Line1[il].Ind1;
Image.Grid[x+width*(y-1)]=Line1[il].Ind0;
Mask.Grid[x+width*y]=Mask.Grid[x+width*(y-1)]=LabMask;
break;
case 1: Image.Grid[x+width*y]=Line1[il].Ind0;
Image.Grid[x-1+width*y]=Line1[il].Ind1;
Mask.Grid[x+width*y]=Mask.Grid[x-1+width*y]=LabMask;
break;
case 2: Image.Grid[x-1+width*y]=Line1[il].Ind0;
Image.Grid[x-1+width*(y-1)]=Line1[il].Ind1;
Mask.Grid[x-1+width*y]=Mask.Grid[x-1+width*(y-1)]=LabMask;
break;
case 3: Image.Grid[x+width*(y-1)]=Line1[il].Ind1;
Image.Grid[x-1+width*(y-1)]=Line1[il].Ind0;
Mask.Grid[x+width*(y-1)]=Mask.Grid[x-1+width*(y-1)]=LabMask;
break;
} //:::::::::::::::::::::: end switch :::::::::::::::::::::::::::::::
} //------------------- end if (nBits==24) ---------------------------
} //============ end for (il < nLine1 …============================
int first, last;
int[] Shift = new int[]{0,2,4,6};
for (int il=0; il<nLine2; il++) //======================================
{
if ((il % i1) == 0) fm1.progressBar1.PerformStep();
if (il==0) first=0;
else first=Line2[il-1].EndByte+1;
last=Line2[il].EndByte;
x=Line2[il].x;
y=Line2[il].y;
int iByte=first, iShift=0;
iVect2 P = new iVect2(), PixelP = new iVect2(), PixelN = new iVect2(); // comb. coordinates
byte[] ColN = new byte[3], ColP = new byte[3];
byte[] ColStartN = new byte[3], ColStartP = new byte[3],
ColLastN = new byte[3], ColLastP = new byte[3]; // Colors
for (int c=0; c<3; c++)
ColN[c]=ColP[c]=ColStartN[c]=ColStartP[c]=ColLastN[c]=ColLastP[c]=0;
if (nBits==24)
{ for (int c=0; c<nbyte; c++)
{ ColStartN[2-c]=(byte)((Palette[Line2[il].Ind0]>>8*c) & 255);
ColStartP[2-c]=(byte)((Palette[Line2[il].Ind1]>>8*c) & 255);
ColLastN[2-c]= (byte)((Palette[Line2[il].Ind2]>>8*c) & 255);
ColLastP[2-c]= (byte)((Palette[Line2[il].Ind3]>>8*c) & 255);
}
}
else
{ ColStartN[0]=Line2[il].Ind0;
ColStartP[0]=Line2[il].Ind1;
ColLastN[0]=Line2[il].Ind2;
ColLastP[0]=Line2[il].Ind3;
}
P.X=Line2[il].x; P.Y=Line2[il].y;
int nCrack=Line2[il].nCrack;
int xx, yy; // Interpolation:
for (int iC=0; iC<nCrack; iC++) //=====================================
{ dir=(Byte[iByte] & (3<<Shift[iShift]))>>Shift[iShift];
switch(dir) // Standard coordinates
{ case 0: PixelP=P; PixelN=P+Step[3]; break;
case 1: PixelP=P+Step[2]; PixelN=P; break;
case 2: PixelP=P+Step[2]+Step[3]; PixelN=P+Step[2]; break;
case 3: PixelP=P+Step[3]; PixelN=P+Step[2]+Step[3]; break;
}
if (PixelP.Y<0 || PixelN.Y<0 || PixelP.Y>height-1 || PixelN.Y>height-1)
{ MessageBox.Show("Restore: Bad 'PixelP' or 'PixelN'. 'Byte' is bad. iByte=" +
iByte + "; dir=" + dir + "; Byte=" + Byte[iByte]);
}
for (int c=0; c<nbyte; c++) //====================================
{ ColN[c]=(byte)((ColLastN[c]*iC+ColStartN[c]*(nCrack-iC-1))/(nCrack-1));
ColP[c]=(byte)((ColLastP[c]*iC+ColStartP[c]*(nCrack-iC-1))/(nCrack-1));
} //=============== end for (c... ===============================
// Assigning colors to intermediate pixels of a line:
xx=PixelP.X; yy=PixelP.Y;
if (xx+width*yy>width*height-1 || xx+width*yy<0)
{
MessageBox.Show("Restore: Bad 'xx,yy'="+(xx+width*yy)+"; 'Byte' is bad.");
}
if (xx+width*yy<width*height && xx+width*yy>=0)
{ for (int c=0; c<nbyte; c++) Image.Grid[c+nbyte*xx+nbyte*width*yy]=ColP[c];
Mask.Grid[xx+width*yy]=LabMask;
}
xx=PixelN.X; yy=PixelN.Y;
if (xx + width * yy > width * height - 1 || xx + width * yy < 0) return -1;
if (xx+width*yy<width*height && xx+width*yy>=0)
{ for (int c=0; c<nbyte; c++) Image.Grid[c+nbyte*xx+nbyte*width*yy]=ColN[c];
Mask.Grid[xx+width*yy]=LabMask;
}
P=P+Step[dir];
iShift++;
if (iShift==4)
{ iShift=0;
iByte++;
}
} //=============== end for (iC... ==============================
} //================ end for (il < nLine2 ===========================
Mask.Grid[0]=Mask.Grid[width-1]=Mask.Grid[width*height-1]=
Mask.Grid[width*(height-1)]=LabMask;
return 1;
} //******************* end Restore ***********************************
下一个方法Image.Smooth( )
,属于类CImage
,作为图像RestoreIm
的方法开始。它从平滑图像的边界开始。该方法设计用于处理彩色或灰度图像。如果Image
是彩色图像,变量nbyte
被设置为等于 3,或者如果是灰度图像,变量nbyte
被设置为 1。因此,在第一种情况下,处理三个颜色通道,而在第二种情况下,仅处理一个灰度值。
该方法将颜色或灰度值内插在角和最终存在的跨越边界的线的裂缝之间。这在该方法的四个独立部分中完成,因为四个边界具有完全不同的坐标。
Smooth
然后开始平滑图像Mask
的零像素区域。它首先对行进行平滑。它跟踪图像的一行,并测试图像中成对的后续像素Mask
。如果实际像素( x,y )为零,前一个像素非零,那么xbeg
的值被设置为x - 1
,变量ColorBeg
被设置为Image
的像素( x - 1, y )的颜色(或灰度值)。如果实际像素和先前像素都为零,则计算通过的像素。如果实际像素( x,y )非零,并且前一个像素为零,则已经找到以零开始和结束的行的一段。变量ColorEnd
被设置为Image
的像素( x 、 y )的颜色(或灰度值),所有通过的零像素被填充以ColorBeg
和ColorEnd
之间的插值颜色(或灰度值)。
然后沿着柱执行类似的过程。在这种情况下获得的插值值与沿行平滑期间保存在Image
像素中的值进行平均。
经过这两次插值,恢复的图像已经看起来不错了。它只显示一些比周围环境稍亮或稍暗的垂直和水平线条。方法Smooth
通过平滑图像Mask
具有零值的区域来移除这些线。为此,它实现了拉普拉斯偏微分方程的数字解。它使用在 Press,(1990)中描述为同时过度弛豫(SOR)的方法。这种方法不是最快的,但是非常简单,很容易编程。
这里是Smooth
的代码。
public int Smooth(ref CImage Mask, Form1 fm1)
/* Calculates the average colors between "gvbeg" and "gvend" and saves them in the image "this". This is a digital automation with the states S=1 at Mask>0 and S=2 at Mask==0, but S is not used. The variable "mpre" has the value of "Mask" in the previous pixel. --*/
{
int c, cnt, LabMask = 250, msk, mpre, x, xx, xbeg, xend, ybeg, yend, y, yy;
int[] Col = new int[3], ColBeg = new int[3], ColEnd = new int[3];
int nbyte;
if (N_Bits == 24) nbyte = 3;
else nbyte = 1;
// Smoothing the borders:
// Border at y=0:
y = 0; cnt = 0; xbeg = 0; mpre = 200;
for (c = 0; c < nbyte; c++) ColBeg[c] = Grid[c];
for (x = 0; x < width; x++) //======================================
{
msk = Mask.Grid[x + width * y];
if (mpre > 0 && msk == 0) //----------------------------------------
{
cnt = 1; xbeg = x - 1;
for (c = 0; c < nbyte; c++) ColBeg[c] = Grid[nbyte * (x - 1 + width * y) + c];
}
if (mpre == 0 && msk == 0) //----------------------------------------
{
cnt++;
}
if (mpre == 0 && msk > 0) //----------------------------------------
{
cnt++; xend = x;
for (c = 0; c < nbyte; c++) ColEnd[c] = Grid[nbyte * (x + width * y) + c];
for (xx = xbeg + 1; xx < xend; xx++) //============================
{
for (c = 0; c < nbyte; c++) Grid[nbyte * (xx + width * y) + c] =
(byte)((ColBeg[c] * (xend - xx) + ColEnd[c] * (xx - xbeg)) / cnt);
Mask.Grid[xx + width * y] = (byte)LabMask;
} //============== end for (xx... ==============================
}
mpre = msk;
} //=============== end for (x=0; ... ==============================
// Border at y=height-1:
y = height - 1; cnt = 0; xbeg = 0; mpre = 200;
for (c = 0; c < nbyte; c++) ColBeg[c] = Grid[nbyte * width * y + c];
for (x = 0; x < width; x++) //=======================================
{
msk = Mask.Grid[x + width * y];
if (mpre > 0 && msk == 0) //----------------------------------------
{
cnt = 1; xbeg = x - 1;
for (c = 0; c < nbyte; c++) ColBeg[c] = Grid[nbyte * (x - 1 + width * y) + c];
}
if (mpre == 0 && msk == 0) //----------------------------------------
{
cnt++;
}
if (mpre == 0 && msk > 0) //----------------------------------------
{
cnt++; xend = x;
for (c = 0; c < nbyte; c++) ColEnd[c] = Grid[nbyte * (x + width * y) + c];
for (xx = xbeg + 1; xx < xend; xx++)
{
for (c = 0; c < nbyte; c++) Grid[nbyte * (xx + width * y) + c] =
(byte)((ColBeg[c] * (xend - xx) + ColEnd[c] * (xx - xbeg)) / cnt);
Mask.Grid[xx + width * y] = (byte)LabMask;
}
}
mpre = msk;
} //=============== end for (x=0; ... ===============================
// Border at x=0
x = 0; cnt = 0; ybeg = 0; mpre = 200;
for (c = 0; c < nbyte; c++) ColBeg[c] = Grid[nbyte * (x + width * 0) + c];
for (y = 0; y < height; y++) //======================================
{
msk = Mask.Grid[x + width * y];
if (mpre > 0 && msk == 0) //-------------------------------------------
{
cnt = 1; ybeg = y - 1;
for (c = 0; c < nbyte; c++) ColBeg[c] = Grid[nbyte * (x + width * (y - 1)) + c];
}
if (mpre == 0 && msk == 0) //----------------------------------------
{
cnt++;
}
if (mpre == 0 && msk > 0) //----------------------------------------
{
cnt++; yend = y;
for (c = 0; c < nbyte; c++) ColEnd[c] = Grid[nbyte * (x + width * y) + c];
for (yy = ybeg + 1; yy < yend; yy++)
{
for (c = 0; c < nbyte; c++)
{
Col[c] = (ColBeg[c] * (yend - yy) + ColEnd[c] * (yy - ybeg)) / cnt;
Grid[nbyte * (x + width * yy) + c] = (byte)Col[c];
}
Mask.Grid[x + width * yy] = (byte)LabMask;
}
}
mpre = msk;
} //=============== end for (y=0; ... ==============================
// Border at x=width-1
x = width - 1; cnt = 0; ybeg = 0; mpre = 200;
for (c = 0; c < nbyte; c++) ColBeg[c] = Grid[nbyte * (x + width * 0) + c];
for (y = 0; y < height; y++) //=======================================
{
msk = Mask.Grid[x + width * y];
if (mpre > 0 && msk == 0) //----------------------------------------
{
cnt = 1; ybeg = y - 1;
for (c = 0; c < nbyte; c++) ColBeg[c] = Grid[nbyte * (x + width * (y - 1)) + c];
}
if (mpre == 0 && msk == 0) //----------------------------------------
{
cnt++;
}
if (mpre == 0 && msk > 0) //----------------------------------------
{
cnt++; yend = y;
for (c = 0; c < nbyte; c++) ColEnd[c] = Grid[nbyte * (x + width * y) + c];
for (yy = ybeg + 1; yy < yend; yy++)
{
for (c = 0; c < nbyte; c++)
{
Col[c] = (ColBeg[c] * (yend - yy) + ColEnd[c] * (yy - ybeg)) / cnt;
Grid[nbyte * (x + width * yy) + c] = (byte)Col[c];
}
Mask.Grid[x + width * yy] = (byte)LabMask;
}
}
mpre = msk;
} //=============== end for (y=0; ... ==============================
// End smoothing border; Smooth on "x":
fm1.progressBar1.Visible = true;
fm1.progressBar1.Value = 0;
int Sum = height + width + 50 * 10;
int denomProg = fm1.progressBar1.Maximum / fm1.progressBar1.Step;
int i1 = Sum / denomProg;
for (y = 0; y < height; y++) //=======================================
{
if ((y % i1) == 0) fm1.progressBar1.PerformStep();
cnt = 0; xbeg = 0; mpre = 200;
for (c = 0; c < nbyte; c++) ColBeg[c] = Grid[nbyte * width * y + c];
for (x = 0; x < width; x++) //======================================
{
msk = Mask.Grid[x + width * y];
if (mpre > 0 && msk == 0) //----------------------------------------
{
cnt = 1; xbeg = x - 1;
for (c = 0; c < nbyte; c++) ColBeg[c] = Grid[nbyte * (x - 1 + width * y) + c];
}
if (mpre == 0 && msk == 0) //----------------------------------------
{
cnt++;
}
if (mpre == 0 && msk > 0) //----------------------------------------
{
cnt++; xend = x;
for (c = 0; c < nbyte; c++) ColEnd[c] = Grid[nbyte * (x + width * y) + c];
for (xx = xbeg + 1; xx < xend; xx++)
{
for (c = 0; c < nbyte; c++) Grid[nbyte * (xx + width * y) + c] =
(byte)((ColBeg[c] * (xend - xx) + ColEnd[c] * (xx - xbeg)) / cnt);
}
}
mpre = msk;
} //=============== end for (x=0; ... =============================
} //================ end for (y=0; ... ==============================
// Smooth on "y":
for (x = 0; x < width; x++) //========================================
{
if ((x % i1) == 0) fm1.progressBar1.PerformStep();
cnt = 0; ybeg = 0; mpre = 200;
for (c = 0; c < nbyte; c++) ColBeg[c] = Grid[nbyte * (x + width * 0) + c];
for (y = 0; y < height; y++) //======================================
{
msk = Mask.Grid[x + width * y];
if (mpre > 0 && msk == 0) //----------------------------------------
{
cnt = 1; ybeg = y - 1;
for (c = 0; c < nbyte; c++) ColBeg[c] = Grid[nbyte * (x + width * (y - 1)) + c];
}
if (mpre == 0 && msk == 0) //----------------------------------------
{
cnt++;
}
if (mpre == 0 && msk > 0) //-----------------------------------------
{
cnt++; yend = y; for (c = 0; c < nbyte; c++) ColEnd[c] = Grid[nbyte * (x + width * y) + c];
for (yy = ybeg + 1; yy < yend; yy++)
{
for (c = 0; c < nbyte; c++)
{ Col[c]= (Grid[nbyte*(x+width*yy)+c]+(ColBeg[c]*(yend-yy) +
ColEnd[c]*(yy-ybeg))/cnt)/2;
Grid[nbyte * (x + width * yy) + c] = (byte)Col[c];
}
}
}
mpre = msk;
} //=============== end for (y=0; ... =============================
} //================= end for (x=0; ... =============================
// Solving the Laplace's equation:
int i;
double fgv, omega = 1.4 / 4.0, dMaxLap = 0.0, dTH = 1.0;
double[] dGrid = new double[width * height * nbyte];
double[] Lap = new double[3];
for (i = 0; i < width * height * nbyte; i++) dGrid[i] = (double)Grid[i];
fm1.progressBar1.Visible = false;
fm1.progressBar1.Value = 0;
fm1.progressBar1.Visible = true;
int it1=10;
for (int iter = 0; iter < 50; iter++) //================================
{ // Smooth Math.Abs((x-y))%2==0
if ((iter % it1) == 0) fm1.progressBar1.PerformStep();
for (y = 1; y < height - 1; y++)
for (x = 1; x < width - 1; x++)
{
if (Mask.Grid[x + width * y] == 0 && Math.Abs((x - y)) % 2 == 0)
for (c = 0; c < nbyte; c++)
{
Lap[c] = 0.0;
Lap[c] += dGrid[nbyte * (x + width * (y - 1)) + c];
Lap[c] += dGrid[nbyte * (x - 1 + width * y) + c];
Lap[c] += dGrid[nbyte * (x + 1 + width * y) + c];
Lap[c] += dGrid[nbyte * (x + width * (y + 1)) + c];
Lap[c] -= 4.0 * dGrid[nbyte * (x + width * y) + c];
fgv = dGrid[nbyte * (x + width * y) + c] + omega * Lap[c];
if (fgv > 255.0) fgv = 255.0;
if (fgv < 0.0) fgv = 0;
dGrid[nbyte * (x + width * y) + c] = fgv;
}
}
// Smooth at Math.Abs((x-y))%2==1
for (y = 1; y < height - 1; y++)
for (x = 1; x < width - 1; x++)
{
if (Mask.Grid[x + width * y] == 0 && Math.Abs((x - y)) % 2 == 1)
for (c = 0; c < nbyte; c++)
{
Lap[c] = 0.0;
Lap[c] += dGrid[nbyte * (x + width * (y - 1)) + c];
Lap[c] += dGrid[nbyte * (x - 1 + width * y) + c];
Lap[c] += dGrid[nbyte * (x + 1 + width * y) + c];
Lap[c] += dGrid[nbyte * (x + width * (y + 1)) + c];
Lap[c] -= 4.0 * dGrid[nbyte * (x + width * y) + c];
fgv = dGrid[nbyte * (x + width * y) + c] + omega * Lap[c];
if (fgv > 255.0) fgv = 255.0;
if (fgv < 0.0) fgv = 0;
dGrid[nbyte * (x + width * y) + c] = fgv; //(int)(fgv);
}
}
dMaxLap = 0.0; // Calculating MaxLap:
for (y = 1; y < height - 1; y++)
for (x = 1; x < width - 1; x++) //===================================
{
if (Mask.Grid[x + width * y] == 0) //------------------------------
{
for (c = 0; c < nbyte; c++) //==============================
{
Lap[c] = 0.0;
Lap[c] += dGrid[nbyte * (x + width * (y - 1)) + c];
Lap[c] += dGrid[nbyte * (x - 1 + width * y) + c];
Lap[c] += dGrid[nbyte * (x + 1 + width * y) + c];
Lap[c] += dGrid[nbyte * (x + width * (y + 1)) + c];
Lap[c] -= 4.0 * dGrid[nbyte * (x + width * y) + c];
if (Math.Abs(Lap[c]) > dMaxLap) dMaxLap = Math.Abs(Lap[c]);
} //================= end for (c=0; =====================
} //------------------------------ end if (Mask... ---------------
} //================== end for (x=1; ... ==========================
int ii;
for (ii = 0; ii < width * height * nbyte; ii++) Grid[ii] = (byte)dGrid[ii];
if (dMaxLap < dTH) break;
} //==================== end for (iter... ============================
return 0;
} //********************** end Smooth **********************************
九、图像分割和连通分量
图像分割是图像分析中的一个重要步骤。以下是关于分割的已知信息(参见 Shapiro (2001 年):
在计算机视觉中、、、图像分割、、是将一幅数字图像分割成多个片段的过程(集合像素的,也称为超像素)。分割的目标是简化和/或改变图像的表示,使其更有意义,更易于分析。【1】【2】图像分割通常用于定位物体和边界(直线、曲线等)。)在图像中。更准确地说,图像分割是给图像中的每个像素分配标签的过程,使得具有相同标签的像素共享某些特征。**
在科学文献中有大量关于分割方法的出版物。然而,让我们记住,任何阈值图像可以被认为是一个分段的。量化图像也可以被认为是分割的,其中所有灰度值或颜色被细分为相对较少数量的组,单一颜色被分配给一个组的所有像素。分段问题的困难仅在于定义有意义的分段的可能性;即从人类感知的角度来看有意义的片段。例如,对于一个分段,我们可以说,“这个分段是房子,这个分段是街道,这个分段是汽车,等等”,这将是一个有意义的分段。据我所知,开发一个有意义的分割是一个尚未解决的问题。在这一节中,我们考虑通过将图像的颜色量化成最佳地代表图像颜色的少量组来进行分割。
通过量化颜色进行分割
量化颜色是众所周知的压缩彩色图像的方法:颜色信息不是直接由图像像素数据携带,而是存储在称为调色板的独立数据中,调色板是颜色元素的阵列。数组中的每个元素都代表一种颜色,按其在数组中的位置进行索引。图像像素不包含颜色的完整规格,而仅包含其在调色板中的索引。这是众所周知的产生索引 8 位图像的方法。
我们的方法包括产生一个对具体图像最佳的调色板。为此,我们开发了第八章中描述的方法MakePalette
,并成功用于项目WFcompressPal
。
连接的组件
我们假设这些片段应该是像素的连接子集。连通性是一个拓扑概念,而经典拓扑主要考虑无限点集,其中一个点的每个邻域都包含无限多个点。经典拓扑学不适用于数字图像,因为它们是有限的。
连通性的概念也用在图论中。可以将数字图像视为顶点为像素的图形,图形的边连接任意两个相邻的像素。路径是一个像素序列,其中除了起始处的一个像素和末端的一个像素之外,每个像素都恰好与两个相邻的像素相关联。
众所周知(例如,参考 Hazewinkel,(2001)),在无向图中,如果路径从 x 通向 y,则无序的顶点对{x,y}称为连通的。否则,无序的顶点对称为不连通的。
连通图是一种无向图,其中图中的每一对无序顶点都是连通的。否则,它被称为不连通图。
将图论中的连通性概念应用到图像上是可能的。我们考虑两种邻接:四邻接(图 9-1a )和八邻接(图 9-1b )。在图 9-1a 的情况下,黑色像素和白色像素的子集都是断开的;在图 9-1b 的情况下,它们都被连接。在这两种情况下,都存在一个众所周知的连通性悖论:为什么图 9-1a 中的黑色像素应该是断开的,而它们之间没有任何东西?同样,为什么图 9-1b 中的黑色像素是相连的,尽管它们之间存在从一个白色像素到另一个白色像素的路径?
图 9-1
连通性悖论:(1)四邻接,和(2)八邻接
Rosenfeld (1970)建议在二进制图像中考虑黑色像素的八个邻接和白色像素的四个邻接。这个建议解决了二进制图像的问题,但是它不适用于多色图像。据我所知,多色图像连通性悖论的唯一解决方案是将数字图像视为一个细胞复合体,如第七章所述。然后,连通性可以通过下面指定的等式规则(参见 Kovalevsky (1989))被正确地定义,并且没有悖论。
坐标为(x,y)和(x + 1,y + 1)或(x,y)和(x–1,y + 1)的两个像素组成一对对角线。在 ACCs 理论中,如果两个像素具有相同的颜色,并且位于它们之间的低维单元(一维或零维)也具有该颜色,则一对八个相邻像素被定义为连通的。自然,谈论低维细胞的颜色没有什么意义,因为它们的面积为零。然而,有必要使用这个概念来一致地定义数字图像中的连通性。为了避免使用低维单元的颜色的概念,我们建议当像素的标签等于其颜色时,用标签代替颜色。
考虑图 9-2 的例子。假设有必要以白 V 和黑 V 都连接的方式来定义连通性。这在任何邻接关系下显然都是不可能的。该目的可以通过将隶属标签(例如,对应于颜色的整数)分配给较低维度的单元的等式规则来实现。
图 9-2
一幅图像中的白色和黑色 V 形区域,由于应用了等式规则,这两个区域相互连接
二维图像的等式规则表明,一维单元总是接收与其关联的两个主要(即二维)单元中最大的标签(较浅的颜色)。
零维单元格 c 0 的标签定义如下:
- 如果 c 0 的 2 × 2 邻域恰好包含一对具有相同标号(Equ)的对角像素,则 c 0 接收该标号。如果有多个这样的对,但其中只有一个属于窄(Na)条带,则c0 接收窄条带的标签。否则c0 接收最大值;即c0 邻域内像素的较亮(Li)标号(即较大标号)。
后一种情况对应于当 c 0 的邻域不包含具有相同标号的对角对或者包含多于一个这样的对角对并且其中多于一个对角对属于窄条的情况。
为了决定在 c 0 附近的对角线对是否属于窄条纹,有必要扫描 4 × 4 = 16 像素的阵列,其中 c 0 在中间,并且计数对应于两条对角线的标签。最小的计数表示窄条纹。其他有效成员规则的例子可以在 Kovalevsky (1989)中找到。
对于分割图像的分析来说,找到并区分图像的所有连接部分是很重要的。问题是找到图像的最大连通子集,每个子集包含恒定颜色的像素。然后,有必要对组件进行编号,并为每个像素分配其所属组件的编号。让我们称分配的数字为标签。
如果对于 S 中的任意两个像素,在 S 中存在包含这两个像素的相邻像素序列,则某种颜色的像素子集 S 被称为连通的。我们认为埃奎纳利街区。
为了找到并标记组件,图像必须包含相对较少的颜色。否则,彩色图像的每个像素可能是最大连通分量。分割图像大多满足拥有少量颜色的条件。
简单的方法包括逐行扫描图像,并为在已经扫描的子集中没有相同颜色的相邻像素的每个像素分配下一个未使用的标签。如果下一个像素在已经扫描的子集中具有相同颜色的相邻像素,并且它们具有不同的标签,则将这些不同标签中最小的标签分配给该像素。具有这些不同标签之一的已扫描子集的所有像素必须获得这个最小的标签。在最坏的情况下,替换可能影响高达高度 2 /2 像素,其中高度是图像中的行数。在扫描了整个图像之后,替换的总数可以是大约高度 3 /2。这个数字可能非常大,所以这种幼稚的方法是不实际的。标记一个分量的像素的有效方法是众所周知且广泛使用的图遍历方法。
图的遍历算法及其代码
现在让我们考虑图遍历方法。图像被视为图形,像素被视为顶点。如果对应的像素具有相等的值(例如,颜色)并且相邻,则两个顶点由图的边连接。邻接关系可以是四邻接或八邻接。很容易认识到,图形的分量正好对应于图像的期望分量。
至少有两种众所周知的遍历一个图的所有顶点的方法:深度优先搜索和宽度优先搜索算法。它们彼此非常相似,这里我们只考虑广度优先搜索。这种算法被用于我们抑制脉冲噪声的项目中(第二章)。我们在这里详细描述一下。
广度优先算法采用了众所周知的数据结构,称为 queue,也称为先进先出:值可以放入队列,先放入的值将首先从队列中出来。该算法的过程如下。
-
将组件的编号 NC 设置为零。
-
取任意未标记的顶点 V ,增加 NC ,用值 NC 标记 V ,并将 V 放入队列。顶点 V 是具有索引 NC 的新组件的“种子”。
-
当队列不为空时,重复以下说明:
-
从队列中取出第一个顶点。
-
测试 W 的每个邻居 N 。如果 N 被贴上标签,忽略;否则用 NC 标记 N 并将其放入队列。
-
-
当队列为空时,那么组件 NC 已经被标记。
继续第 2 步,找到下一个组件的种子,直到所有顶点都被标记。
让我们描述一下这个算法的伪代码。给定一个包含N
个元素的多值数组Image[]
以及方法Neighb(i,m)
和Adjacent(i,k)
,第一个返回第i
个元素的第m
个邻居的索引。如果第i
个元素与第k
个元素和FALSE otherwise
相邻,第二个函数返回TRUE
。作为标记的结果,每个元素获得其所属的连接组件的标记。标签保存在一个数组Label[]
中,其大小与Image[]
相同。我们假设所有方法都可以访问数组Image
和Label
以及它们的大小N
。
广度优先算法的伪代码
分配数组Label[N]
。Label
的所有元素现在必须用零初始化,这意味着这些元素还没有被标记。
for (i=0; i<N; i++) Label[i]=0; // setting all labels to 0
int NC=0; // number of components
for (V=0; V<N; V++) // loop through all elements of the image
{
if (Label[V]≠0) continue; // looking for unlabeled elements
NC=NC+1; // index of a new component
Label[V]=NC; // labeling the element with index V with NC
PutIntoQueue(V); // putting V into the queue
while(QueueNotEmpty()) //loop running while queue not empty
{ W=GetFromQueue();
for (n=0; n<Max_n; n++) // all neighbors of W ==========
{ N=Neighb(W,n); // the index of the nth neighbor of W
if (Label[N]==0 AND Adjacent(W,N)==TRUE
AND Image[N]==Image[W])
{ Label[N]=NC; // labeling element with index N with NC
PutIntoQueue(N);
}
} // ========= end for (n=0; ... =================
} // =========== end while ========================
} // ============= end for (V =0;... ======================
算法结束
在这种情况下,值Max_n
是图像元素的所有邻居的最大可能数量,而不管它是否已经被访问过。它等于 8。需要用方法PutIntoQueue(i), GetFromQueue(),
和QueueNotEmpty()
提供队列数据结构。如果队列不为空,最后一个方法返回TRUE
;否则它返回FALSE
。
该算法标记包含起始像素的组件的所有像素。为了标记图像的所有成分,有必要在那些没有被标记为属于已经被处理的成分的像素中选择下一个开始像素。这必须重复,直到图像的所有像素都被标记。
等价类方法
还有另一种方法来解决标记连通分量的问题,即等价类(EC ),它考虑同时标记图像的所有分量。Rosenfeld 和 Pfalz (1966)是关于 EC 方法的最早出版物之一。在二维图像的情况下,该算法的思想如下进行。该算法逐行扫描图像两次。它在每个像素 P 的第一次扫描期间,根据四个或八个相邻像素并具有与 P 相同的颜色,调查与 P 相邻的所有已访问像素的集合 S 。如果没有这样的像素,那么 P 获得下一个未使用的标签。否则 P 得到最小的标签(标签集必须是有序的;整数大多用作 S 中像素的标号。同时,该算法声明 S 的像素的所有标签等同于最小的标签。为此目的,该算法采用等价表,其中每一列对应于所采用的标签之一。等效标签存储在列的元素中。
当图像被完全扫描后,一种特殊的算法处理该表并确定等效标签的类别。每个类别由最小的等价标签表示。重新排列该表,使得在对应于每个标签的列中,保留该类的标签。在第二次扫描期间,图像中的每个标签都被该类的标签所替换。
该算法的缺点之一是难以先验地估计等价表的必要大小。Kovalevsky (1990)独立于 Gallert 和 Fisher (1964)开发了 EC 方法的改进版本,称为根方法 。它采用了一个标签数组L
,其元素数量与获得的图像中的像素数量相同。这是等价表的替代品。为了解释改进的思想,我们首先考虑二维二值图像的最简单情况,尽管该算法也适用于多值和多维图像的情况,而没有本质的改变。
建议的方法基于以下想法。图像的每个像素都有一个标签数组L
的对应元素。我们姑且称之为像素的标签。数组L
的类型必须能够在其元素中保存图像每个像素的索引x + width*y
(类型integer
对于任何大小的图像都足够)。如果一个组件包含一个像素,其标签等于该像素的索引,则该标签称为该组件的根。
在算法开始时,对应于像素( x,y )的数组L
的每个元素被设置为等于索引x+width*y
,其中width
是图像的列数。因此,在开始时,每个像素被认为是一个组件。
该算法逐行扫描图像。如果像素在先前扫描的区域中没有相同颜色的相邻像素,则该像素保留其标签。然而,如果下一个像素 P 在已经访问过的像素中具有相同颜色的相邻像素,并且它们具有不同的标签,这意味着这些像素属于具有不同根的不同的先前发现的分量。这些不同的部件必须结合成一个单一的部件。为此,需要找到这些分量的根,并将所有这些根和标签 P 设置为最小根。重要的是要注意,只有根被改变,而不是与 P 相邻的像素的标签值被改变。像素的标签将在第二次扫描中改变。这就是这种方法如此快速的原因。在运行像素处相遇的所有分量得到相同的根,并且它们被合并成单个分量。不是根的标签保持不变。在前面提到的最坏情况下,替换可能只影响width
/2 个像素。
在第一次扫描之后,每个像素的标签指向具有较小标签的相同组件的像素。因此,有一系列越来越小的标签以组件的根结束。在第二次扫描期间,所有标签都被后续的整数替换。
子集的连通性必须由邻接关系来定义。这可以是公知的( n , m )邻接关系,其仅被定义用于二进制图像,或者是基于由方法EquNaLi
定义的子集中较低维度的单元的成员的邻接关系。
可以看出,在根方法中使用的标签数组L
需要太多的存储空间:为了存储索引,L
的每个元素中的位数必须不小于log2(size)
,其中size
是图像元素的数量。然而,这个大小的L
对于任何标记连通分量的方法都是必要的,如果该方法被认为适用于任何多值图像的话。事实上,图像中的每个元素都是一个组成部分。不同标签的数量等于图像元素的数量。例如图 9-3 所示的四种颜色的二维图像,在任何邻接关系下都具有这种性质。
图 9-3
具有四种颜色的图像示例,其组件数量与像素数量相同
如果已知所用标签的数量小于 size,那么每个元素具有较少比特数的数组 L 足以应用使用等价表的方法。然而,这对于现代计算机来说并不重要,因为现代计算机有充足的内存。因此,例如,在具有 32 位处理器的现代计算机上,数据类型 int 的数组可以用于包含多达 2 个 32 个元素的图像。因此,二维图像可以具有 65,536 × 65,536 像素的大小,而三维图像可以具有 2,048 × 2,048 × 1,024 个体素的大小。
让我们回到算法上来。它逐行扫描图像,并为每个像素 P 调查与 P 相邻的已经访问过的像素的集合 S 。如果在 S 中存在与 P 颜色相同的像素,并且它们具有不同的标号,那么算法找到这些像素的最小根标号,并将其分配给 P 和 S 的所有像素的根。因此,在连续像素处彼此相遇的所有分量获得相同的根,并被合并成单个分量。不是根的像素的标签保持不变。在单个组件的最坏情况下,替换只能影响width
/2 个像素。
每个像素在第一次扫描后属于通向其分量的根的路径。在第二次扫描期间,根和所有其他像素的标记被随后的整数替换。
一个简单的二维例子如图 9-4 所示。给定的是一个N
= 16 像素的二维二进制数组Image[N]
。暗前景的两个像素相邻,当且仅当它们具有共同的入射 1 单元(四个相邻),而亮背景的像素相邻,如果它们具有任何共同的入射单元(八个相邻)。在算法开始时,所有像素的标签等于它们的索引(图 9-4a 中从 0 到 15 的数字)。索引为 0 和 1 的像素在之前访问的像素中没有相同颜色的相邻像素。它们的标签 0 和 1 保持不变。它们是根。索引为 2 的像素具有相邻的像素 1,其根为 1。因此,标签 2 被 1 代替。在扫描第一行之后,标签 3 保持不变。根据前面的规则改变所有像素的标记,直到到达最后一个像素 15。该像素与像素 11 和 14 相邻。像素 11 的标签指向根 3;像素 14 的指向像素 0,像素 0 也是一个根。标签 0 小于 3。因此,根 3 的标签被更小的标签 0 取代(图 9-4b )。
在第二次运行期间,根的标记被随后的自然数 1 和 2 替换。所有其他像素的标签获得其根的新标签(图 9-4c )。EC 算法的代码将在后面描述。
图 9-4
标记连接组件的算法图示
根算法的伪代码
该算法可以应用于组合网格或标准网格中的图像。在组合网格中,其中两个单元的关联关系由它们的组合坐标来定义,从像素值集合中分配一个值(颜色)给任何维度的每个单元就足够了。具有相同值的两个事件单元是相邻的,并且将被分配给相同的组件。在标准网格中,必须给出一种方法来指定给定元素 e 的邻域中的哪些网格元素与 e 相邻,因此如果它们具有相同的值,则必须将其分配给相同的组件。在我们早期的简单二维例子中,我们采用了前景像素的四个相邻像素和背景像素的八个相邻像素。一般来说, n 维复合体的主单元的邻接必须由指定较低维单元的成员的规则来指定(例如,由类似于等式规则的规则来指定),因为众所周知的( m , n )邻接不适用于多值图像。我们在这里考虑标准网格情况下的伪代码。
给定的是一个包含N
个元素的多值数组Image[]
,以及方法Neighb(i,m)
和Adjacent(i,k)
。第一个函数返回第i
个元素的第m
个邻居的索引,如果第i
个元素与第k
个元素相邻,第二个函数返回TRUE
。否则它返回FALSE
。在二维情况下,这可以是实现等式规则的方法。作为标记的结果,每个元素获得它所属的连接组件的标记。标签保存在另一个数组Label[].
数组Label[N]
的大小必须与Image[N].
相同Label
的每个元素必须至少有log2N
位,其中N
是Image
的元素个数,并且必须用自己的索引进行初始化:
for(I = 1;I < N;i++)标签[i] = i。
为了使伪代码更简单,我们假设所有方法都可以访问数组Image and Label
和它们的大小N
。
在算法的第一个循环中,Image
的每个元素都有一个指向其组件根的标签。值Max_n
是已经被访问的元素(像素或体素)的相邻元素的最大可能数量。很容易识别出Max_n
等于(3 d - 1)/2,其中 d 为图像的尺寸。例如,在二维情况下,Max_n
等于 4,而在三维情况下,它等于 13 .
求根算法
for (i=0; i<N; i++)
{ for (m=0; m<Max_n; m++)
{ k=Neighb(i,m); // the index of the mth adjacent element of i
if(Adjacent(i,k) AND Image[k] == Image[i]) SetEqu(i, k);
}
} // end of the first loop
SecondRun();
子程序SetEqu(i,k)
(其伪代码见下)将元素i
和k
的根的索引相互比较,并将具有较大索引的根的标签设置为等于较小索引。因此,两个词根属于同一个成分。方法Root(k)
返回索引序列中的最后一个值,其中第一个索引k
是给定元素的索引,下一个是Label[k],
的值,依此类推,直到Label[k]
等于k
。子程序SecondRun()
根据Label[k]
是否等于k
用元件计数器的值或用k
的根代替Label[k]
的值。
子程序的伪代码
subroutine SetEqu(i,k)
{ if (Root(i)<Root(k)) Label[Root(k)]=Root(i);
else Label[Root(i)]=Root(k);
} // end subroutine.
int Root(k)
{ do
{ if (Label[k]==k) return k;
k=Label[k];
} while(1); // loop runs until condition Label[k]==k is fulfilled
} // end Root
subroutine SecondRun()
{ count=1;
for (i=0; i<N; i++)
{ value=Label [i];
if (value==i )
{ Label[i]=count; count=count+1;
}
else Label[i]=Label[value];
}
} // end subroutine.
项目WFsegmentAndComp
我们已经开发了一个 Windows 窗体项目WFsegmentAndComp
分割一个图像,并标记其连接组件的像素。
有可能以两种方式执行分割:或者通过将颜色聚类成相对小数量(例如,20 个)的聚类并将属于该聚类的所有像素的平均颜色分配给每个聚类,或者通过将彩色图像转换成灰度图像,将灰度值的全范围[0,255]细分成小数量的区间,并将平均灰度值分配给每个区间。这种转换不会导致信息的丢失,因为相连分量的标记是相对较大的数字,与原始颜色的颜色无关:具有某种颜色的像素集合可以由许多分量组成,每个分量具有另一个标记。
连通分量的标记是通过两种方法完成的,即广度优先搜索和根方法,以便用户可以比较这两种方法的速度。标记的结果被表示为具有 256 种颜色的人工调色板的彩色图像,并且调色板的索引是将标签除以 256 的结果。图 9-5 显示了项目的形式。
图 9-5
项目形式WFsegmentAndComp
当用户单击打开图像时,他或她可以选择要打开的图像。该图像将显示在左侧的图片框中,并将定义五个工作图像。
然后,用户应该单击下一个按钮“Segment”。原始图像将被转换为灰度图像。灰度值将除以 24,并且除法的整数结果将乘以 24。这样,新的灰度值变成 24 的倍数,灰度值的标度被量化成 256/24 + 1 = 11 个区间。分割的图像显示在右-右图片框中。
根据经验可知,即使是分割图像,分量的数量也是非常大的。在这些组件中,许多组件由单个像素或几个像素组成。图像分析对这些成分不感兴趣。为了减少小分量的数量,我们决定抑制分割图像的脉冲噪声。这将特定灰度级的许多小点与周围区域合并,并减少了小组件的数量。
用户可以选择要删除的暗点和亮点的最大尺寸,然后单击脉冲噪声。将呈现具有删除的暗点和亮点的图像,而不是分割的图像。
然后,用户可以单击广度优先搜索或根方法。带标签的图像将以错误的颜色显示;也就是说,使用包含 256 种颜色的人工调色板。调色板的索引等于标签除以 256。
第二章介绍了抑制脉冲噪声的代码。
这里,我们提出了用于标记分割图像的连接部分的宽度优先搜索的代码。如前所述,该算法处理包含起始像素的一个分量的像素。为了处理图像的所有组成部分,我们开发了方法LabelC
,它扫描图像并测试每个像素是否被标记。如果不是,那么它是下一个组件的起始像素,并且调用方法Breadth_First_Search
。
所有这些方法都是一个特殊类CImageComp
的元素,它与类CImage
的不同之处在于标签数组Lab
的存在。属性nLoop
和DenomProg
用于控制指示方法LabelC
进程的progressBar
。
public class CImageComp
{
public byte[] Grid;
public int[] Lab;
public Color[] Palette;
public int width, height, N_Bits, nLoop, denomProg;
... // here are the constructors and the methods
}
下面是LabelC
的代码。
public int LabelC(Form1 fm1)
{ int index, lab=0;
for (index=0; index<width*height; index++) Lab[index]=0;
int y1 = nLoop * height / denomProg;
for (int y=0; y<height; y++) //========================
{
if (y % y1 == 0) fm1.progressBar1.PerformStep();
for (int x = 0; x < width; x++) //=================
{ index=x+width*y;
if (Lab[index]==0)
{ lab++;
Breadth_First_Search(index, lab);
}
} //================= end for (int x...============
} //=================== end for (int x...==============
return lab;
} //********************* end LabelC ***************************
可以看出,该方法非常简单:它扫描图像的所有像素,并检查实际像素是否已经被标记。如果不是,则调用方法Breadth_First_Search
。
这里是Breadth_First_Search
的代码。
public int Breadth_First_Search(int root, int lab)
{ int light, gvP, Index, Len=1000, Nei;
bool rv;
iVect2 P = new iVect2(0, 0);
CQue Q = new CQue(Len);
light=Grid[root];
if (Lab[root]==0) Lab[root]=lab;
Q.Put(root);
while(!Q.Empty()) //=============================
{ Index=Q.Get();
gvP=Grid[Index];
P.Y=Index/width; P.X=Index-width*P.Y;
for (int n=0; n<8; n++) //====================
{ Nei=NeighbEqu(Index, n, width, height);
rv=EquNaliDir(P, n, gvP);
if (Nei<0) continue;
if (Grid[Nei]==light && Lab[Nei]==0 && rv)
{
Lab[Nei]=lab;
Q.Put(Nei);
}
} //============== end for (n... ==============
} //================ end while ====================
return 1;
} //****************** end Breadth_First_Search *************
为了显示组件标记的结果,我们使用一个简单的调色板,有 256 种颜色。调色板由方法MakePalette
产生。这是它的代码。
public int MakePalette( ref int nPal)
{ int r, g, b;
byte Red, Green, Blue;
int ii=0;
for (r=1; r<=8; r++) // Colors of the palette
for (g=1; g<=8; g++)
for (b=1; b<=4; b++)
{ Red=(byte)(32*r); if (r==8) Red=255;
Green = (byte)(32 * g);
if (g == 8) Green = 255;
Blue= (byte)(64*b); if (b==4) Blue=255;
Palette[ii] = Color.FromArgb(Red, Green, Blue);
ii++;
}
nPal=4*ii;
return 1;
} //***************** end MakePalette *****************
类别CImageComp
的图像BreadthFirIm
包含组件和调色板的标签。为了使标签可见,我们通过方法LabToBitmap
将图像BreadthFirIm
转换为位图BreadthBmp
,并将该位图显示在右边的图片框中。下面是LabToBitmap
的代码。
public void LabToBitmap(Bitmap bmp, CImageComp Image)
{
if (Image.N_Bits != 8)
{
MessageBox.Show("Not suitable format of 'Image'; N_Bits=" + Image.N_Bits);
return;
}
switch (bmp.PixelFormat)
{
case PixelFormat.Format24bppRgb: nbyte = 3; break;
case PixelFormat.Format8bppIndexed:
MessageBox.Show("LabToBitmap: Not suitable pixel format=" + bmp.PixelFormat);
return;
default: MessageBox.Show("LabToBitmap: Not suitable pixel format=" + bmp.PixelFormat);
return;
}
Rectangle rect = new Rectangle(0, 0, bmp.Width, bmp.Height);
BitmapData bmpData = bmp.LockBits(rect, ImageLockMode.ReadWrite, bmp.PixelFormat);
IntPtr ptr = bmpData.Scan0;
int size = bmp.Width * bmp.Height;
int bytes = Math.Abs(bmpData.Stride) * bmp.Height;
byte[] rgbValues = new byte[bytes];
Color color;
int index = 0;
int y1 = nLoop * bmp.Height / 220; // denomProg;
for (int y = 0; y < bmp.Height; y++)
{
if (y % y1 == 0) progressBar1.PerformStep();
for (int x = 0; x < bmp.Width; x++)
{
color = Image.Palette[Image.Lab[x + bmp.Width * y] & 255];
index = 3 * x + Math.Abs(bmpData.Stride) * y;
rgbValues[index + 0] = color.B;
rgbValues[index + 1] = color.G;
rgbValues[index + 2] = color.R;
}
}
System.Runtime.InteropServices.Marshal.Copy(rgbValues, 0, ptr, bytes);
bmp.UnlockBits(bmpData);
} //**************************** end LabToBitmap ***********************
位图BreadthBmp
用假颜色显示组件。
用户可以通过单击“保存宽度图像”将带有标记组件的图像保存为彩色图像。
如前所述,我开发了另一种标记连接组件的方法。这种方法的优点是不用一个接一个地标记组分,而是同时并行地标记。这个方法的思想在前面已经描述过了。这里我们要呈现的是重要方法ComponentE
的代码,当用户点击 Root method 时,这个方法作为图片RootIm
的方法被调用。
public int ComponentsE(Form1 fm1)
{ /* Labels connected components of "this" in "this->Lab" and returns
the number of components. Uses the EquNaLi connectedness. --*/
int Dir, light, i, nComp=0, rv, x1 = 0, y1 = 0;
bool adjac;
iVect2 P = new iVect2(0, 0);
for (int k=0; k<width*height; k++) Lab[k]=k;
int y2 = nLoop*height / denomProg;;
for (int y=0; y<height; y++) //==========================================
{ if ((y % y2) == 0) fm1.progressBar1.PerformStep();
for (int x = 0; x < width; x++) //=====================================
{ i=x+width*y; // "i" is the index of the actual pixel
light=Grid[i];
P.X=x; P.Y=y;
int nmax;
if (y==0) nmax=0;
else nmax=3;
for (int n=0; n<=nmax; n++) //==== the four preceding neighbors ========
{ switch(n)
{ case 0: x1=x-1; y1=y; break; // West
case 1: x1=x-1; y1=y-1; break; // North-West
case 2: x1=x; y1=y-1; break; // North
case 3: x1=x+1; y1=y-1; break; // North-East
}
if (x1<0 || x1>=width) continue;
Dir=n+4;
int indN=x1+width*y1;
int lightNeib=Grid[indN];
if (lightNeib!=light) continue;
if ((Dir & 1)==0) adjac=true;
else
adjac=EquNaliDir(P, Dir, light);
if (adjac)
rv=SetEquivalent(i, indN); // Sets Lab[i] or Lab[indN] equal to Root
} //==================== end for (int n ... ========================
} //===================== end for (int x ... ========================
} //====================== end for (int y ... ========================
nComp=SecondRun(); // Writes indexes of components from 1 to nComp to "Comp".
return nComp; // nComp+1 is the number of indexes
} //************************* end ComponentsE **************************
类别CImageComp
的图像RootIm
包含组件和调色板的标签。就像在Breadth_First_Search
的情况中一样,方法MakePalette
和LabToBitmap
被调用以使标签可见。图像RootIm
被转换为位图RootBmp
,后者显示在右边的图片框中。然后,用户可以通过单击“根”的保存图像来保存该图像。
结论
根算法和广度优先算法的时间复杂度与图像元素的数量成线性关系。只要定义了邻接关系,这两种算法都可以应用于任何维数的图像。
作者进行的大量实验表明,根算法和广度优先算法的运行时间几乎相等。根算法的优点是它测试图像的每个元素是否已经被标记(3 d - 1)/2 次,其中 d 等于 2 或者等于 3 是图像的维度,而宽度优先算法测试它 3 d 次,这大约是两倍多。广度优先算法可以直接用于只标记包含图像的给定元素的单个分量,而根算法必须同时标记所有分量。
十、拉直绘画照片
我们有时对拍摄展览中展出的画作感兴趣。如果房间是纯照明,那么有必要使用闪光灯。然而,如果你把相机直接放在画的前面,闪光灯会在画的窗格中反射,照片就被破坏了。为了避免反光,有必要从侧面拍照。然后,然而,画的图像是扭曲的(图 10-1 )。
图 10-1
从侧面拍摄的绘画照片
我建议一个对这类照片进行整改的项目。如果我们知道相机与画的距离,从画的中点到相机物镜的光线方向,以及相机的焦距,那么中心投影的几何知识将是有用的。估计所有这些参数是困难的,特别是如果在摄影过程中使用变焦,因为变焦会改变相机的焦距。
因此,我建议一种基于画面框架是矩形这一假设的方法。然后,当使用照片中框架左边缘的长度与其右边缘的长度的关系时,可以估计框架的宽度与其高度的关系。上边缘的长度与下边缘的长度的关系也是有用的。
名为WFrectify
的建议项目工作如下。可以使用.jpg
和.bmp
图像;其他格式的图像应该转换成这些格式之一。使用 IrfanView 程序很容易做到这一点,该程序是免费的,可以从互联网上下载。项目形式如图 10-2 所示。
图 10-2
项目的形式
要打开图像,用户应该单击打开图像,然后单击角。两条白线的直角标记了图像的左下四分之一。然后,用户应该决定他或她是否想要有框或无框的图片。根据这一决定,用户单击框架或框架内图像的左下角。
点选的角落会以小红点标示,白色的角会移到影像的左上角。用户应该单击相应的角,以此类推,直到所有四个角都被单击。然后用红线显示标记的四边形。如果用户单击了白线标记的角度之外的点,将显示一条消息,告诉用户该点的正确位置,用户可以关闭该消息并再次单击。
图像的中点pictureBox1
必须在已处理的绘画内部。否则不可能正确点击所有四个角。在这种情况下,必须对图像进行切割,例如,使用 IrfanView 程序,以使整个图像的中点位于画作内部。四个被点击点的坐标保存在数组Point v[4]
中。
用户应决定是否删除或部分保留标记方角以外的背景。相应地,他或她应选择“删除背景”或“保留背景”选项以及一个用于指示百分比的选项。
然后需要单击“拉直”。拉直的图像出现在右边的图片框中。用户可以通过单击保存结果来保存结果图像。拉直后的图像可以保存为.bmp
或.jpg
文件。也可以覆盖现有图像,包括刚刚打开的图像。让我们探索一下这个项目是如何运作的。
拉直的原理
为了拉直图像,我们需要拉直图像的宽度和高度。然后,我们可以通过双线性变换或中心投影将原始图像转换为拉直的图像,如下所述。
实验表明,双线性变换会产生不准确的结果。图像的不同部分被不同地放大,如图 10-3 所示。
图 10-3
通过双线性变换拉直的图像示例
如图 10-3 所示,图像左边的矩形被绘制成宽度,右边的矩形被绘制成高度。当拉直一幅画时,这个缺点并不明显,因为没有矩形,但它仍然是图像的一个不希望的失真。因此,我们决定不使用双线性变换,并开发了一种使用中心变换的方法,尽管这要困难得多。
通过众所周知的中心投影方程,可以找到变换的必要参数。然而,如前所述,我们需要知道从相机的物镜到画的距离,以及指定相机光轴方向的角度。没有办法估计这些参数。相反,我们可以使用有充分根据的假设,即这幅画的框架是一个长方形。我们在获得的图像(图 10-1 )中看到,帧的左边缘的长度不同于右边缘的长度。这些长度的关系以及上边缘和下边缘的长度的关系可以用来估计照相机与画的不同边缘的距离的关系。这些关系然后可以用于估计变换的参数。
考虑图 10-4 中的示意图,该示意图表示通过摄像机物镜 O 的平面以及在获得的图像中帧的左右边缘上的点 M 0 和 M 1 (图 10-5 )。我们将焦平面的翻转副本(M 0 ,C,M 1 )放置在物镜和被摄物体(图中未显示)之间,以便该平面中的图像不会像焦平面中的图像一样反转。
图 10-4
射线与平面的截面(M 0 ,O,M 1
为了找到变换的参数,有必要估计平行于所拍摄的绘画平面的平面的参数,因为所获得的图像在该平面上的中心投影是期望的拉直图像。该平面在图 10-4 中表示为包含该线段的直线(P 0 ,P 1 )。线段(P 0 ,P 1 )是所述平面与平面(M 0 ,O,M 1 )的截面。同样重要的是用户在所获得的图像中的框架的角上点击的四个点。这些点是非矩形四边形的顶点,代表获得的图像中的帧(图 10-5 )。
图 10-5
点 M 0 ,M 1 ,M 2 ,M 3 的说明
通过求解相应的方程可以找到变换的参数。然而,这些方程不是线性的,并且它们很难求解。因此我们用三角学的方法找到参数。
获得的图像大小为宽×高。大多数现代相机的主焦距(即没有变焦的焦距)等于光敏矩阵的宽度。因此我们假设焦距等于宽度和高度的最大值。我们用 f 表示焦距。首先我们计算角度α和β(图 10-4 ):
α = arctan(F/(宽度/2–m0)。(x)
β= arctan(f/(m1)。x–宽度/2)
其中宽度是获得的图像的宽度。
角度φ可以通过正弦定律从三角形(P 0 ,O,C)计算,其中 C 是所获得图像的中心。它的坐标是(宽/2,高/2,F):
(p【0】–o)/sin(π/2+φ)=f/sin(α–φ)
或者
(p【0】–o)=【f】sin(π/2+φ)/sin(α-φ)。
类似地,我们从三角形(P 1 ,O,C)得到:
(p【1】–o)=【f】sin(π/2–φ)/sin(β+φ)
和
(p【0】–o)= redy(p–o)
其中 RedY 为关系式(P0-O)/(P1-O),等于得到的图像中帧的右边缘长度与左边缘长度的关系。
因此,我们得到:
f【sin(π/2+φ)/sin(α–φ)= redf【sin(π/2–φ)/sin(β+φ)。
根据等式 sin(π/2 + φ) = cos(φ)和 sin(π/2–φ)= cos(φ),我们得到:
cos(φ)/sin(α-φ)= redⅲcos(φ)/sin(β+φ)
或减去 cos(φ)后:
sin(β + φ) = RedY⋅ sin(α – φ)。
现在我们把 sin(β + φ)换成 sin β⋅cos φ + cos β⋅sin φ,把 sin(α–φ)换成 sin α⋅cosφ–cos α⋅sinφ:
无β-cosφ+cosβ-无φ= redyⅲ(无α-cosφ–cosαⅲ无φ)。
除以 cos φ后,我们得到:
无β-1+cosβ-TGφ= redyⅲ(无α-1–cosα-TGφ)
或者
TGφⅲ(cosβ+redyⅲcosα)= redyⅲ无α–无β
和
TGφⅲ=(redyⅲ无α-无β)/(cosβ+redyⅲcosα)
或者
φ⋅ = 奥术(RedY⋅sin β – 其α)/(cos α + RedY⋅cos β)。
φ⋅的值允许我们找到包含拉直图像的平面 p 的方程的参数 CX:
(x–宽度/2)* CX+(y–高度/2)* CY+(z–F)= 0。(10.1)
因为直线(P 0 ,P 1 )是 P 与平面(M 0 ,O,M 1 )的交点,所以 CX 近似等于 tan(φ)。这是过程估计,稍后我们会给出精确的估计。
以类似的方式,我们计算平面中的角度φX(M2,O,M 3 ,其中 M 2 和 M 3 是给定图像中具有 M 2 的帧的上边缘和下边缘的点。X = M 3 。X =宽度/2(见图 10-5 )。角度φX 类似于已经计算出的位于平面内的角度φ(M0,O,M 1 )。值 RedX 是框架的上边缘长度与下边缘长度的关系。
φx = arctan(redx⋅sinβx–sinαx)/(cosαx+red⋅cosβx)。
相应的,CY 的估计值为 tan(φX)。
现在让我们计算拉直图像的宽度。我们根据正弦定律从三角形(P 0 ,O,C)获得线段的长度( P 0 ,C):
|p【0】,c | =【f】sin(π/2–α)/sin(α–φ);
而从三角形(P 1 ,O,C)的长度( P 1 ,C):
| > ??【p】【1】,c | =【f】sin(π/2–β)/sin(β+φ)。
宽度的计算值等于:
宽度= |p【0】,【p】| = |,【c | >
**f【sin(π/2–α)/sin(α–φ)+sin(π/2–β)/sin(β+φ)】
f(cosα/sin(α-φ)+cosβ/sin(β+φ))。
以类似的方式,我们在考虑平面(M 2 ,O,M 3 时获得高度值,其中 M 2 和 M 3 是给定图像中具有 M 2 的帧的上边缘和下边缘的点。X = M 3 。X =宽度/2(见图 10-5 )。
height =f⋅(cosαx/sin(αx–φx)+cosβx/sin(βx+φx))
其中αX,βX,φX 是平面内的角度(M 2 ,O,M 3 )类似于平面内计算的α,β,φ(M0,O,M 1 )。
我们计算了平面 P 的参数 CX 和 CY 为
CX = tan(φ);CY = tan(φX)。
实验表明,这些值是近似值。为了找到 CX 和 CY 的精确值,我们开发了方法Optimization
,该方法在前面提到的近似值附近测试 CX 和 CY 的许多值。对于 CX 和 CY 的每一对值,该方法计算作为四元组 V 在平面 P 上的中心投影而获得的四元组 Q 与期望的矩形形状的三个偏差。这些偏差如下:
-
偏差“dev1”等于 Q 的水平边到其垂直边的投影。如果 Q 是一个矩形,这个投影应该等于零。
-
偏差“dev2”等于 q 的垂直边的长度差。
-
偏差“dev3”等于 q 的水平边的长度差。
这三个偏差的平方和的平方根是在所有测试对 CX 和 CY 中最小化的标准。有 11 × 11 = 121 对 CX 和 CY 的值被测试,CX 和 CY 的值改变一个步长= 0.08。在步长为 0.01 的 11 × 11 = 121 对变化值中,再次测试与标准最小值相对应的 CX 和 CY 的最佳值。实验表明,根据点击图像角落的用户指定的矢量 v 的精度,该标准的最小值达到 1.0 和 10.0 之间的值。这种优化精度是足够的。甚至通过三角计算获得的 CX 和 CY 的起始值也提供了无法从理想图像中光学区分的拉直图像。
在计算 CX 和 CY 的值之后,方法Rect_Optimal
计算四个点 PC[4],这四个点 PC[4]是四个点v[4]
在平面 p 上的中心投影。然后,用尺寸Width×Height
定义拉直的图像ResultIm
。该图像通过两个for
循环扫描,坐标为像素的 X 和 Y。对于每个像素(X,Y ),通过双线性变换计算位于具有拐角PC[4]
的四元组中并且对应于像素(X,Y)的点的坐标(xp,yp,zp)。坐标为(xp,yp,zp)的点被中心投影到给定图像的平面上。这个投影是一个坐标为(xf,yf,F)的点。给定图像的像素(xf,yf)的颜色被分配给拉直图像的像素(X,Y)。
一种类似的方法Rect_Retain
用于产生拉直的图像,该图像包含围绕绘画框架的一部分背景。该方法与Rect_Optimal
的不同之处仅在于存在一个将平面 P 中的点PC[4]
从坐标为(宽度/2,高度/2,F)的中心 C 移动的过程,使得每个向量的长度(PC[i],C),I = 0,1,2,3;乘以参数Rel
,用户可以选择值 1.1、1.15 或 1.2。作为这种倍增的结果,拉直的图像包括围绕绘画框架的原始背景的窄条。
最重要方法的代码
下面是最重要的方法Rect_Optimal
的源代码。
public void Rect_Optimal(Point[] v, bool CUT, ref CImage Result)
// Calculates the corners of the straightened image and then makes a central projection.
{
bool deb = false;
int MaxSize;
double alphaX, alphaY, betaX, betaY, F, Height, phiX, phiY,
RedX, RedY, Width, M0X, M1X, M3Y, M2Y;
M0X = (double)v[1].X + (v[0].X - v[1].X) *((double)height/2 -
v[1].Y)/((double)v[0].Y-v[1].Y);
M1X = (double)v[2].X + (v[3].X - v[2].X) *((double)height/2 -
v[2].Y)/((double)v[3].Y-v[2].Y);
M3Y = (double)v[0].Y + (v[3].Y - v[0].Y) * ((double)width/2 -
v[0].X)/((double)v[3].X-v[0].X);
M2Y = (double)v[1].Y + (v[2].Y - v[1].Y) * ((double)width/2 -
v[1].X)/((double)v[2].X-v[1].X);
RedY = (double)(v[3].Y - v[2].Y) / (double)(v[0].Y - v[1].Y);
RedX = (double)(v[3].X - v[0].X) / (double)(v[2].X - v[1].X);
if (width > height) MaxSize = width;
else MaxSize = height;
F = 1.0 * (double)(MaxSize);
alphaY = Math.Atan2(F, (double)(width / 2 - M0X));
betaY = Math.Atan2(F, (double)(M1X - width / 2));
phiY=Math.Atan2(RedY*Math.Sin(betaY) -
Math.Sin(alphaY),Math.Cos(alphaY)+RedY*Math.Cos(betaY));
alphaX = Math.Atan2(F, (double)(M3Y - height / 2));
betaX = Math.Atan2(F, (double)(height / 2 - M2Y));
phiX = Math.Atan2(RedX * Math.Sin(betaX) -
Math.Sin(alphaX), Math.Cos(alphaX) + RedX * Math.Cos(betaX));
double P0X = F * Math.Cos(alphaY) / Math.Sin(alphaY - phiY);
double P1X = F * Math.Cos(betaY) / Math.Sin(betaY + phiY);
double P0Y = F * Math.Cos(alphaX) / Math.Sin(alphaX + phiX);
Width = F * (Math.Cos(alphaY) / Math.Sin(alphaY - phiY) +
Math.Cos(betaY) / Math.Sin(betaY + phiY));
Height = F * (Math.Cos(alphaX) / Math.Sin(alphaX - phiX) +
Math.Cos(betaX) / Math.Sin(betaX + phiX));
if (Width < 0.0 || Height < 0.0)
{
MessageBox.Show("The clicked area does not contain the center of the image");
return;
}
double OptCX=0.0;
double OptCY=0.0;
double CX = Math.Tan(phiY);
double CY = Math.Tan(phiX);
Optimization(F, v, CX, CY, ref OptCX, ref OptCY);
CX = OptCX;
CY = OptCY;
CImage Out;
if (CUT)
Out = new CImage((int)Width, (int)Height, N_Bits);
else
Out = new CImage(width, height, N_Bits);
Result.Copy(Out, 0);
double A = 0.0, B, C, D, Det, E, G;
double[] xc = new double[4];
double[] yc = new double[4];
double[] zc = new double[4];
for (int i = 0; i < 4; i++)
{
A = B = C = D = 0.0;
A = (F / (v[i].X - width / 2) + CX);
B = CY;
C = width / 2 * F / (v[i].X - width / 2) + CX * width / 2 + CY * height / 2 + F;
D = CX;
E = (F / (v[i].Y - height / 2) + CY);
G = height / 2 * F / (v[i].Y - height / 2) + CX * width / 2 + CY * height / 2 + F;
Det = A * E - B * D;
xc[i] = (C * E - B * G) / Det;
yc[i] = (A * G - C * D) / Det;
zc[i] = F - CX * (xc[i] - width / 2) - CY * (yc[i] - height / 2);
}
double zz;
double xp, yp, xp0, xp1, yp0, yp1, xf, yf;
for (int Y = 0; Y < Result.height; Y++) //= over the straightened image
{
xp0 = xc[1] + (xc[0] - xc[1]) * Y / (Result.height - 1);
xp1 = xc[2] + (xc[3] - xc[2]) * Y / (Result.height - 1);
for (int X = 0; X < Result.width; X++) //=============================
{
yp0 = yc[1] + (yc[2] - yc[1]) * X / (Result.width - 1);
yp1 = yc[0] + (yc[3] - yc[0]) * X / (Result.width - 1);
xp = xp0 + (xp1 - xp0) * X / (Result.width - 1);
yp = yp0 + (yp1 - yp0) * Y / (Result.height - 1);
zz = F - CX * (xp - width / 2) - CY * (yp - height / 2); // corrected
xf = width / 2 + (xp - width / 2) * F / (F - CX * (xp - width / 2) -
CY * (yp - height / 2));
yf = height / 2 + (yp - height / 2) * F / (F - CX * (xp - width / 2) -
CY * (yp - height / 2));
if ((int)xp >= 0 && (int)xp < width && (int)yp >= 0 && (int)yp < height)
if (N_Bits == 24)
{
for (int ic = 0; ic < 3; ic++)
Result.Grid[ic+3*X+3*Result.width*Y]=Grid[ic+3*(int)xf+3*width*(int)yf];
}
else
Result.Grid[X+Result.width*(Result.height-1-Y)]=Grid[(int)xf+width*(int)yf];
} //================= end for (X... ==============================
} //================== end for (Y... ==============================
} //********************* end Rect_Optimal ******************************
这里是Optimization
的代码。
private void Optimization(double F, Point[] v, double CX, double CY,
ref double OptCX, ref double OptCY)
{
bool deb = false;
double A = 0.0, B, C, D, Det, E, G;
double[] xc = new double[4];
double[] yc = new double[4];
double[] zc = new double[4];
double[] xopt = new double[4];
double[] yopt = new double[4];
double[] zopt = new double[4];
double dev1, dev2, dev3, Crit;
double MinCrit = 10000000.0;
int OptIterX = -1, OptIterY = -1, IterY;
OptCX = 0.0;
OptCY = 0.0;
CX -= 0.40; CY -= 0.40;
double CX0 = CX, Step = 0.08;
for (IterY = 0; IterY < 11; IterY++)
{
CX = CX0;
for (int IterX = 0; IterX < 11; IterX++)
{
for (int i = 0; i < 4; i++)
{
A = (F / (v[i].X - width / 2) + CX);
B = CY;
C = width / 2 * F / (v[i].X - width / 2) + CX * width / 2 + CY * height / 2 + F;
D = CX;
E = (F / (v[i].Y - height / 2) + CY);
G = height / 2 * F / (v[i].Y - height / 2) + CX * width / 2 + CY * height / 2 + F;
Det = A * E - B * D;
xc[i] = (C * E - B * G) / Det;
yc[i] = (A * G - C * D) / Det;
zc[i] = F - CX * (xc[i] - width / 2) - CY * (yc[i] - height / 2);
}
dev1 = dev2 = dev3 = 0.0;
dev1 = ((xc[0] - xc[1]) * (xc[2] - xc[1]) + (yc[0] - yc[1]) * (yc[2] - yc[1]) + (zc[0] - zc[1]) * (zc[2] - zc[1])) /
Math.Sqrt(Math.Pow((xc[0] - xc[1]), 2.0) + Math.Pow((yc[0] - yc[1]), 2.0) + Math.Pow((zc[0] - zc[1]), 2.0));
dev2 =
Math.Sqrt(Math.Pow((xc[3] - xc[2]), 2.0) + Math.Pow((yc[3] - yc[2]), 2.0) + Math.Pow((zc[3] - zc[2]), 2.0)) -
Math.Sqrt(Math.Pow((xc[0] - xc[1]), 2.0) + Math.Pow((yc[0] - yc[1]), 2.0) + Math.Pow((zc[0] - zc[1]), 2.0));
dev3 =
Math.Sqrt(Math.Pow((xc[2] - xc[1]), 2.0) + Math.Pow((yc[2] - yc[1]), 2.0) +
Math.Pow((zc[2] - zc[1]), 2.0)) -
Math.Sqrt(Math.Pow((xc[3] - xc[0]), 2.0) + Math.Pow((yc[3] - yc[0]), 2.0) + Math.Pow((zc[3] - zc[0]), 2.0));
Crit=Math.Sqrt(Math.Pow(dev1, 2.0)+Math.Pow(dev2, 2.0)+Math.Pow(dev3, 2.0));
if (Crit < MinCrit)
{
MinCrit = Crit;
OptIterX = IterX;
OptIterY = IterY;
OptCX = CX;
OptCY = CY;
for (int i = 0; i < 4; i++)
{
xopt[i] = xc[i];
yopt[i] = yc[i];
zopt[i] = zc[i];
}
}
CX += Step;
} //============= end for (int IterX... ========================
CY += Step;
} //============== end for (int IterY... ========================
CX = OptCX; CY = OptCY;
CX -= 0.05; CY -= 0.05;
CX0 = CX;
double step=0.01;
for (IterY = 0; IterY < 11; IterY++)
{
CX = CX0;
for (int IterX = 0; IterX < 11; IterX++)
{
for (int i = 0; i < 4; i++)
{
A = (F / (v[i].X - width / 2) + CX);
B = CY;
C = width / 2 * F / (v[i].X - width / 2) + CX * width / 2 + CY * height / 2 + F;
D = CX;
E = (F / (v[i].Y - height / 2) + CY);
G = height / 2 * F / (v[i].Y - height / 2) + CX * width / 2 + CY * height / 2 + F;
Det = A * E - B * D;
xc[i] = (C * E - B * G) / Det;
yc[i] = (A * G - C * D) / Det;
zc[i] = F - CX * (xc[i] - width / 2) - CY * (yc[i] - height / 2);
}
dev1 = dev2 = dev3 = 0.0;
// deviation from a 90° angle:
dev1 = ((xc[0] - xc[1]) * (xc[2] - xc[1]) + (yc[0] - yc[1]) * (yc[2] - yc[1]) + (zc[0] - zc[1]) * (zc[2] - zc[1])) /
Math.Sqrt(Math.Pow((xc[0] - xc[1]), 2.0) + Math.Pow((yc[0] - yc[1]), 2.0) + Math.Pow((zc[0] - zc[1]), 2.0));
dev2 = // difference |pc[3] - pc[2]| - |pc[0] - pc[1]|:
Math.Sqrt(Math.Pow((xc[3] - xc[2]), 2.0) + Math.Pow((yc[3] - yc[2]), 2.0) + Math.Pow((zc[3] - zc[2]), 2.0)) -
Math.Sqrt(Math.Pow((xc[0] - xc[1]), 2.0) + Math.Pow((yc[0] - yc[1]), 2.0) + Math.Pow((zc[0] - zc[1]), 2.0));
dev3 = // difference |pc[2] - pc[1]| - |pc[3] - pc[0]|:
Math.Sqrt(Math.Pow((xc[2] - xc[1]), 2.0) + Math.Pow((yc[2] - yc[1]), 2.0) + Math.Pow((zc[2] - zc[1]), 2.0)) -
Math.Sqrt(Math.Pow((xc[3] - xc[0]), 2.0) + Math.Pow((yc[3] - yc[0]), 2.0) + Math.Pow((zc[3] - zc[0]), 2.0));
Crit =Math.Sqrt(Math.Pow(dev1,2.0)+Math.Pow(dev2,2.0)+Math.Pow(dev3,2.0));
if (Crit < MinCrit)
{
MinCrit = Crit;
OptIterX = IterX;
OptIterY = IterY;
OptCX = CX;
OptCY = CY;
for (int i = 0; i < 4; i++)
{
xopt[i] = xc[i];
yopt[i] = yc[i];
zopt[i] = zc[i];
}
}
CX += step;
} //============== end for (int IterX... ========================
CY += step;
} //=============== end for (int IterY... ========================
} //****************** end Optimization *******************************
结论
这个项目带来了良好的效果,并且易于使用:它为用户提供了图形支持,并在用户出错时给出提示。**
十一、区域边界和边的多边形近似
本章描述了一种将二维数字图像中的曲线表示为多边形的方法。这种曲线表示对于图像分析是有用的,因为多边形的形状可以通过简单的几何方法(例如测量长度和角度)来容易地研究。多边形近似也提出了一种估计数字曲线曲率的新方法。为此,多边形可以用圆弧和直线段的平滑序列来代替。平滑是指每条直线段都是前一条和后一条圆弧的切线。
本章包含相关出版物的简短回顾、已知多边形近似方法的简短描述、Schlesinger (2000)提出的数字曲线相似性新度量的定义以及使用该度量的新近似方法。文中还介绍了新的曲率估计方法的原理和一些实验结果。
多边形逼近问题
给出的是二维图像中的数字曲线 C 。我们希望用一个边尽可能少的多边形来近似 C ,并且与 C 相似。相似性的一个可能标准是 Hausdorff 距离:设S1 和S2 为两组点。用 d ( p 、 q )表示点 p 和 q 之间的距离。那么下列定义成立:
-
D ( p ,s)= mind(p,q)∀q∈s是从点 p 到设定 S 的距离。
-
D ( S 1 ,s2)= maxd(p,s2)∀p∈s1是距离设定值 S 的距离
-
D ( S 2 ,s1)= maxd(q,s1)∀q∈s2是距离设定值 S 的距离
-
H ( S 1 ,S2)= max(D(S1, S 2 ),D(S2, S
然而,Hausdorff 距离并不总是两条曲线相似性的合适度量。图 11-1 中红色和黑色多边形之间的 Hausdorff 距离小于 2 个像素;然而,真实的偏差要大得多:多边形并不相似。Schlesinger (2000)提出了一种更合适的曲线相似性度量方法。
图 11-1
Hausdorff 距离小于 2 的两个多边形的示例;所有箭头的长度都是 2
施莱辛格曲线相似性度量
给出了两条数字曲线C?? 1 和C2。沿C1 取 m 等间距点,沿C2 取 n 点。这样一来就得到两个点了套M1 和M2 的分。沿着曲线的后续点之间的距离越小,相似性的估计就越精确。让我们将两点 p 和 q 之间的距离记为 d ( p , q )。
求对应点间最大距离最小值的单调映射f:m1➔m2。一个有序集 M 1 到另一个有序集 M 2 上的映射称为单调,如果对于任意两点P1<P2的 M 1 的条件 F (对应点之间的最大距离的最小值就是曲线C1 和C2 之间的施莱辛格距离 DS 。
DS ( M 1 , M 2 ) =最大值(d【p】
Schlesinger (2000)还提出了一种计算任意两条数字曲线的距离 DS 的有效算法。在我们的例子中, DS 的值可以计算如下。设黑色曲线为 B ,红色曲线为 R 。曲线被细分为相应的线段( a 、 b )、 b 、 d )、 d 、 f 、 f 、 a )。(点 a 、 b 、 d、和 f 位于两个多边形上。)如图 11-2 所示,将 B 段的每一点映射到 R 对应段的最近点上。
图 11-2
施莱辛格相似性的定义
图 11-2 示例中 DS 的值由( c,d )(或( e,d )的长度定义,等于 3.6。它本质上大于等于 2 的 Hausdorff 距离,并且更精确。
近似问题的陈述
给定一条数字曲线 C 和一个公差值ε,找出边数最少的折线 P ,使得施莱辛格距离 DS ( C , P )不大于ε。
在下一节中,我们将考虑解决这个问题的各种方法。
多边形逼近算法
我们在这里介绍一些从文献中已知的算法和一些新的算法。
拆分-合并方法
这种方法的优点是非常容易理解和编程。给定一条封闭曲线 C 和从 C 点到多边形的最大允许距离ε。在 C 上任意取一点V1。找到与V1 距离最大的点V??。现在曲线细分为两段:( V 1 , V 2 )和( V 2 , V 1 )。每个线段( V i , V , k )都有它的弦(在算法开始时两个线段有相同的弦)。对于每一段,找到与相应弦距离最大的点 V m 。如果 D > ε,将线段( V i , V k )分割成两段( V i , V 重复此操作,直到所有分段的 D ≤ ε。分相结束了。
现在检查每对相邻线段( V i , V m )和( V m , V k )的距离DV如果是,合并段( V i , V m )和( V m , V k )。当所有对满足条件Dε时,停止合并阶段和算法。ε = 1.5 的例子如图 11-3 所示。
*
图 11-3
拆分和合并方法的示例
新点的顺序是 a,b,c,d,e。线段(e,b)和(b,d)已经合并。给定曲线由粉色区域周围的黑色步进线表示。起点为 a。距离 a 最远的点为 b。距离弦(a,b)最远的上段点为 c,该点与 a 和 b 相连。距离弦(a,b)最远的下段点为 D。该点也与 a 和 b 相连。距离弦(c,b)最远的点为 e。该点与 c 和 b 相连。在此步骤之后,所有线段都满足 D ≤ ε且ε = 1.5 的条件。分相结束了。对于相邻线段对(e,b)和(b,d ),它们的公共顶点 b 到弦(e,d)的距离小于ε = 1.5。因此,这些部分应该合并。最终的近似多边形具有顶点 a,c,e,d。
扇形法
拆分-合并方法相当慢,因为每个点都必须测试多次,这取决于它所属段的拆分次数。这种方法使豪斯多夫距离最小,而不是施莱辛格距离。
威廉姆斯(1978)提出了一种有效的方法,称为扇形法。从曲线 C 的某点 V 1 (图 11-4 )开始。测试所有后续点。对于每个点 V i 用 i > 1 和D(V1、VI)>ε,画一个半径等于公差ε的圆,从 V 画两条切线
图 11-4
扇形法
切线之间的空间是一个扇形,对于位于扇形内的每条直线 L 到V1,圆心到 L 的距离小于公差ε。如果下一个点VI(V3或 V 4 在图 11-4 中)位于扇形内,则圆心在 V i 和扇形 S 的一个新圆新扇区是旧扇区与新扇区的交集。如果下一个点 V i (例如图 11-4 中的 V 5 位于新扇区之外,则没有直线 L 通过VV1VI 因此,线段( V 1 , V i )不能作为多边形边。 V i 之前的点VI-1则是当前线段的最后一个点;[ V 1 ,VI-1]是多边形的边。下一条多边形边的构造从点VI-1开始。
扇形法的改进
扇形法很快,因为它只测试给定曲线的每个点一次。但是,它仅保证曲线和多边形之间的 Hausdorff 距离小于规定的公差ε。引入以下附加条件是为了中断应由多边形的当前边近似的曲线的当前段。当前段的点 V max 距离该段的起点最远,必须在该段的加工过程中定义。如果实际点 V i 投影到直线段上的距离 V 1 ,Vmax小于| V 1 ,Vmax|-ε这个测试保证了曲线的点在多边形上的投影顺序是单调的,因此满足了定义施莱辛格距离的条件。
在图 11-5 所示的例子中,近似多边形有顶点 a 、 b 、 c 、 d、和 e 。红点是算法决定中断当前段的点。公差ε = 1.5。
图 11-5
扇形法的改进
本章后面介绍的项目WFpolyArc
使用了改进的扇形法。
用圆弧和直线序列替换多边形
定义和问题陈述
定义 11.1: 如果满足以下条件,一系列圆弧和直线段称为平滑:
-
每条直线段 L 跟随一条圆弧Ap并且跟随另一条圆弧 A f 。
-
线段 L 与Ap和 A f 在其公共端点相切。
-
如果一条弧后面跟着另一条弧,那么它们在公共端点处有一条公共切线。
定义 11.2: 闭合折线 P 的 ε公差带是另外两条折线P1 和P2 之间的区域,使得PI, i = 1,2,平行于的相应边闭合折线P1 内部包含 P 且 P 内部包含P2。
我们考虑以下问题:给定已知以容差ε逼近数字曲线 C 的多边形 P ,找出圆弧和直线段的平滑序列 S ,使得其以容差ε逼近的多边形等于 P ,并且圆弧曲率的绝对值之和最小。
我们假设序列 S 的弧的曲率是曲线 C 的曲率的良好估计,因为 S 和 C 之间的距离小于ε。圆弧曲率的绝对值之和必须最小,因为否则可以找到具有更高曲率值的小圆弧的另一平滑序列 S ,其中到 C 的距离小于 S 和 C 之间的距离,而 S 的圆弧曲率值可以任意高,并且与曲率无关
这个问题的确切解决方案还不知道。我们建议采用以下近似解决方案。
近似解
定义 11.3: 公差带 T 边界的一个顶点 W 若与 W 相交的 T 的边之间的钝角位于 T 之外,则称为凹入。
在下文中,我们描述了计算曲率值的过程。该程序为近似多边形的每个顶点 V (图 11-6 )计算与关联于 V 的多边形边相切的两个圆弧的曲率C1 和C2 的两个候选值。
首先考虑弧 A 穿过顶点 W 并与多边形边相切。如果圆弧 A 相切的点T1 和T2 的D1 和D2 距离 V 小于或等于半长S 然后从三角形OP1W(图 11-6 )计算圆弧半径如下:
(p【1】或=【r】【r】1】
或者
r【1】–【cosβ=ε
*或者
r1=ε/(1–cosβ)(11.1)
其中β是多边形边之间的角度γ的一半。
图 11-6
曲率值的计算
然而,如果距离 D 1 和 D 2 大于从 V 到与 V 相关的两条多边形边中较短的一条边的中点 M 的距离,则不可能使用该圆弧,因为它可能与下一个多边形顶点处的圆弧重叠。在这种情况下,有必要构建另一个不经过点 W 的圆弧,并在其中点 M 处与较短的边相切,在另一边上与 M 对称的点处与另一边相切(图 11-7 )。
图 11-7
曲率值的计算
该圆弧的半径 R 2 应从三角形 OMV 开始计算(图 11-7 ):
R2= ctgβSmin/2;(11.2)
所述计算通过接下来描述的方法Curvature
进行。该方法不计算半径,而是计算作为半径倒数的曲率值:
C1= 1/R1;以及C2= 1/R2。(11.3)
该方法同时计算 C 1 和 C 2 。很容易看出,如果距离 D 1 和 D 2 比短边的半长短(如图 11-6 ,那么R1<R2, C 1 >在这种情况下,曲率的结果值 C 被设置为 C 1 并且所寻找的圆弧的两个切点到顶点 V 的距离D=R1tanβ被设置为D=R1。
在图 11-7 所示的情况下,半径R2<R1曲率的结果值 C 被设置为 C 2 并且被求圆弧的两个切点到顶点 V 的距离 D 被设置为 D
计算曲率后,该方法会计算弧的端点,这些端点等于切点。为此,在 V 之前的多边形边的长度 S 1 被细分为比例a1:(1-a1)与a1=D/S1。 V 后沿的长度 S 2 细分为比例a2:(1-a2)与a2=D/S2。
最后,该方法测试弧的长度是否为零。如果一个小的闭合多边形只包含三个顶点,就会发生这种情况。在这种情况下,程序会返回一条错误消息,并且不会保存 arc。下面是方法Curvature
的代码。
public int Curvature(int x1, int y1, int x2, int y2, int x3, int y3,
double eps, ref CArc arc)
/* Calculates the curvature 'k' of an arc lying in the tolerance tube around the given two polygon edges [(x1,y1), (x2,y2)] and [(x2,y2), (x3,y3)] and having the outer boundary of the tube as its tangent. The tangency point should not be farther than the half-length of the shorter edge from (x2,y2). The radius of the arc should be as large as possible. */
{
double a1, a2, lp, // Variables for calculating the projections
dx1, dy1, dx2, dy2, len1, len2, // Increments, lengths of edges
cosgam, // cosine of the angle between the edges
sinbet, cosbet, // cosine and sine of the half-angle between the edges
strip = 0.6, // correcture of the deviation
k, cru1, cru2; // curvature for long and short edges
dx1 = (double)(x2 - x1); dy1 = (double)(y2 - y1); // first edge
len1 = Math.Sqrt(dx1 * dx1 + dy1 * dy1);
dx2 = (double)(x3 - x2); dy2 = (double)(y3 - y2); // second edge
len2 = Math.Sqrt(dx2 * dx2 + dy2 * dy2);
if ((len1 == 0.0) || (len2 == 0.0)) return (-1);
cosgam = (dx1 * dx2 + dy1 * dy2) / len1 / len2; // cosine of angle between the edges
if (Math.Abs(cosgam) <= 1.0)
sinbet = Math.Sqrt((1.0 - cosgam) * 0.5); // sine of half-angle
else sinbet = 0.0;
cosbet = Math.Sqrt((1.0 + cosgam) * 0.5);
cru1 = (1.0 - cosbet) / (eps - strip); // long edges, it is important
// that the arc goes throught the vertex
double min_len = len1;
if (len2 < min_len) min_len = len2;
if (min_len != 0.0 && cosbet != 0.0)
cru2 = 2.0 * sinbet / cosbet / min_len; // short edges, it is important
else cru2 = 0.0; // that the tangency is in the middle of the shortest edge
if ((Math.Abs(cru1) > Math.Abs(cru2)) && cru1 != 0.0)
{
if (cosbet != 0.0 && cru1 != 0.0)
lp = sinbet / cosbet / cru1; // distance of the point of tangency from (x2, y2)
else lp = 100.0;
k = cru1; // first curvature
}
else
{
lp = min_len / 2.0;
k = cru2 * 0.95; // second curvature
}
if (dx1 * dy2 - dy1 * dx2 > 0.0) k = -k; // the sign
// the first edge is divided in relation a1 : a2
a1 = lp / len1;
a2 = 1.0 - a1;
if (k != 0.0) arc.rad = 1.0F / (float)k;
else arc.rad = 0.0F;
arc.xb = (float)(x1 * a1 + x2 * a2); // first tangency (begin of the arc)
arc.yb = (float)(y1 * a1 + y2 * a2);
a1 = lp / len2; a2 = 1.0 - a1;
arc.xe = (float)(x3 * a1 + x2 * a2); // second tangency (end of the arc)
arc.ye = (float)(y3 * a1 + y2 * a2);
double AbsR, R, chord_X, chord_Y, Length, Lot_x, Lot_y;
R = arc.rad;
AbsR = R;
if (R < 0.0) AbsR = -R;
chord_X = arc.xe - arc.xb; chord_Y = arc.ye - arc.yb; // the chord
Length = Math.Sqrt(chord_X * chord_X + chord_Y * chord_Y); // length of chord
if (R < 0.0) // 'Lot' is orthogonal to chord
{
Lot_x = chord_Y; Lot_y = -chord_X;
}
else
{
Lot_x = -chord_Y; Lot_y = chord_X;
}
if (2 * AbsR < Length) return -1;
if (Length < 0.1) return -2;
double Lot = Math.Sqrt(4 * R * R - Length * Length);
arc.xm = (float)((arc.xb + arc.xe) / 2 - Lot_x * Lot / 2 / Length);
arc.ym = (float)((arc.yb + arc.ye) / 2 - Lot_y * Lot / 2 / Length);
if ((arc.xe == arc.xb) && (arc.ye == arc.yb))
{ // can be a closed line of three points ??
return -2;
}
return 0;
} // ******************* end Curvature **********************************
项目WFpolyArc
本项目的形式如图 11-8 所示。
图 11-8
项目的形式WFpolyArc
正如我们的其他项目一样,该表单包含一个打开图像按钮,使用户能够打开图像。
下一个按钮是检测边缘。点击此按钮启动第七章所述的边缘检测程序。单击多边形启动边的多边形近似过程。近似精度由近似精度设置中显示的值定义。显示值预设最大近似误差,以像素为单位。多边形显示在右边的图片框中。测试每个多边形以确定它是否平滑。这对弧的定义很重要:只为平滑多边形确定弧是有利的。平滑度的定义如下。子例程为多边形的每个顶点定义该顶点处两条边之间的角度符号是否不同于多边形的第一和第二条边之间的角度符号。角度值与平滑多边形允许的最大角度进行比较。如果角度过大或符号错误的节点的百分比超过预定阈值,则认为多边形不平滑。
单击弧启动定义平滑多边形弧的方法。绘制从弧的端点开始到曲率中心结束的半径向量。半径向量的颜色表示曲率的符号:黄色半径表示正曲率的圆弧,紫色表示负曲率的圆弧。多边形和圆弧的参数可以保存在一个文本文件中,如后面所述。用户可以通过单击保存来保存多边形和圆弧列表。当读取文本文件时,可以看到参数值(例如,使用 Microsoft Word)。
项目中使用的方法WFpolyArc
用于边缘检测的方法在章节 6 和 7 中介绍。
当在细胞复合体中使用检测到的边缘的表示时,实现边缘的多边形近似的方法可以变得更简单和更容易理解(Kovalevsky,2008)。第七章描述了细胞复合体的概念。
为了使检测到的边缘的处理更简单和更容易理解,我们使用图像CombIm
,其中边缘被表示为裂纹序列。通过方法LabelCellsNew
填充图像CombIm
,如项目WFcompressPal
(第章第节)中一样。该方法读取由方法ExtremVarColor
或灰度图像的类似方法产生的图像EdgeIm
。它寻找颜色差异超过预定阈值的相邻像素对。对于每一对这样的像素,位于它们之间的裂缝由1
标记。此外,在这种裂纹末端的点(0-单元)被标记,并且该标记等于属于该边缘并且与该点关联的裂纹的数量。
下面是方法LabelCellsNew
的代码。
public void LabelCellsNew(int th, CImage Image3)
/* Looks in "Image3" for all pairs of adjacent pixels with color differences greater than "th" and labels the corresponding cracks in "this" with "1". The points incident with the crack are also provided with labels indicating the number and locations of incident cracks. This method works both for color and gray value images. ---*/
{ int difH, difV, nComp, NXB=Image3.width, x, y;
byte Lab = 1;
if (Image3.N_Bits==24) nComp=3;
else nComp=1;
for (x=0; x<width*height; x++) Grid[x]=0;
byte[] Colorh = new byte[3];
byte[] Colorp = new byte[3];
byte[] Colorv = new byte[3];
for (y = 0; y < height; y += 2)
for (x = 0; x < width; x += 2) // through the points
Grid[x + width * y] = 0;
for (y=1; y<height; y+=2)
for (x=1; x<width; x+=2) // through the right and upper pixels
{ if (x>=3) //-------------- vertical cracks: abs.dif{(x/2, y/2)-((x-2)/2, y/2)} ----------
{ for (int c=0; c<nComp; c++)
{ Colorv[c]=Image3.Grid[c+nComp*((x-2)/2)+nComp*NXB*(y/2)];
Colorp[c]=Image3.Grid[c+nComp*(x/2)+nComp*NXB*(y/2)];
}
if (nComp==3) difV=ColorDif(Colorp,Colorv);
else difV=(Colorp[0]-Colorv[0]);
if (difV < 0) difV = -difV;
if (difV>th)
{
Grid[x - 1 + width * y] = Lab; // vertical crack
Grid[x - 1 + width * (y - 1)]++; // point above the crack;
Grid[x - 1 + width * (y + 1)]++; // point below the crack
}
} //--------------------- end if (x>=3) -----------------------------
if (y>=3) //------------ horizontal cracks: abs.dif{(x/2, y/2)-(x/2, (y-2)/2)} ---------
{ for (int c=0; c<nComp; c++)
{ Colorh[c]=Image3.Grid[c+nComp*(x/2)+nComp*NXB*((y-2)/2)];
Colorp[c]=Image3.Grid[c+nComp*(x/2)+nComp*NXB*(y/2)];
}
if (nComp==3) difH=ColorDif(Colorp,Colorh);
else difH=Math.Abs(Colorp[0]-Colorh[0]);
if (difH>th)
{
Grid[x + width * (y - 1)] = Lab; // horizontal crack
Grid[x - 1 + width * (y - 1)]++; // point left of crack
Grid[x + 1 + width * (y - 1)]++; // point right of crack
}
} //---------------------------- end if (y>=3) ----------------------
} //================== end for (x=1;... ============================
} //********************** end LabelCellsNew ****************************
边缘的多边形近似从方法SearchPoly
开始。该方法扫描图像中的点CombIm
。当它找到一个标签等于 1、3 或 4 的点时,它调用方法ComponPoly
。该方法在类CListLines
的对象中编码包含由SearchPoly
找到的起始点(X,Y)的边组件的多边形。该方法使用先进先出队列。该方法将起始点Pinp
放入队列并开始一个while
循环。它测试从队列中取出的与点 P 相关的每个标记裂纹。如果裂纹的下一个点是分支或终点,则裂纹被忽略。否则调用函数TraceApp
。TraceApp
跟踪边,直到下一个端点或分支点,并计算近似多边形的顶点。追踪在一个叫做Pterm
的点结束。如果点Pterm
是分支点,则将其放入队列。ComponPoly
队列为空时返回。
这里是ComponPoly
的代码。
public int ComponPoly(CImage Comb, int X, int Y, double eps)
{ int dir, dirT;
int LabNext, rv;
iVect2 Crack, P, Pinp, Pnext, Pterm;
Crack = new iVect2();
P = new iVect2();
Pinp = new iVect2();
Pnext = new iVect2();
Pinp.X = X;
Pinp.Y = Y; // comb. coord.
int CNX = Comb.width;
int CNY = Comb.height;
pQ.Put(Pinp);
while(!pQ.Empty()) //============================================
{
P = pQ.Get();
if ((Comb.Grid[P.X + CNX * P.Y] & 128) != 0) continue;
for (dir=0; dir<4; dir++) //======================================
{
Crack.X = P.X + Step[dir].X;
Crack.Y = P.Y + Step[dir].Y;
if (Crack.X < 0 || Crack.X > CNX - 1 || Crack.Y < 0 ||
Crack.Y > CNY - 1) continue;
if (Comb.Grid[Crack.X+CNX*Crack.Y]==1) //---- ------------ --------
{
Pnext.X = Crack.X + Step[dir].X;
Pnext.Y = Crack.Y + Step[dir].Y;
LabNext = Comb.Grid[Pnext.X + CNX * Pnext.Y] & 7;
if (LabNext==3) pQ.Put(Pnext);
if (LabNext==2) //-------------------------------------------------
{
Polygon[nPolygon].firstVert = nVert;
dirT = dir;
Pterm = new iVect2();
rv=TraceApp(Comb, P.X, P.Y, eps, ref Pterm, ref dirT);
if (rv<0)
{ MessageBox.Show("ComponPoly, Alarm! TraceApp returned " + rv);
return -1;
}
if (nPolygon>MaxPoly-1)
{ MessageBox.Show("ComponPoly: Overflow in Polygon; nPolygon=" +
nPolygon + " MaxPoly=" + MaxPoly);
return -1;
}
else nPolygon++;
if ((Comb.Grid[Pterm.X+CNX*Pterm.Y] & 128)==0 && rv>=3)
pQ.Put(Pterm);
} // ------------- end if (LabNest==2) ----------------------------
if ((Comb.Grid[P.X+CNX*P.Y] & 7)==1) break;
} //--------------- end if (Comb.Grid[Crack.X ...==1) --------------
} //========= end for (dir ... =====================================
Comb.Grid[P.X+CNX*P.Y] |=128;
} //========== end while =======================================
return 1;
} //************* end ComponPoly **************************************
当计算出近似多边形时,用户可以开始计算圆弧。方法FindArcs
读取多边形列表,调查每个多边形的三个后续顶点,并通过已经描述的方法Curvature
计算对应于三元组的中间顶点的弧的曲率和端点。这些计算的原理在本章前面已经描述过了。方法FindArcs
产生弧的列表。列表中的每个条目都包含端点的坐标、曲率中心的坐标以及弧的曲率半径值,所有这些都以像素为单位。这里是FindArcs
的代码。
public int FindArcs(PictureBox pictureBox, CImage EdgeIm, double eps, Form1 fm1)
/* The method calculates the parameters of the arcs contained in the polygons. Fills the array 'Arc []' of structures "CArc". Shows the contents of this array graphically. */
{
bool deb = false, disp = true;
int j, ip, first, last, Len, rv,
x1, y1, x2, y2, x3, y3, // three polygon vertices composing an arc
xb, yb, xe, ye, old_xe = 0, old_ye = 0; // "int" boundaries of arcs
nArc = 0;
Pen linePen = new System.Drawing.Pen(Color.LightBlue);
int marginX = fm1.marginX;
int marginY = fm1.marginY;
double Scale1 = fm1.Scale1;
for (ip = 0; ip < nPolygon; ip++) // ===========================
{
int cntArc = 0;
Polygon[ip].firstArc = -1;
Polygon[ip].lastArc = -2;
if (!Polygon[ip].smooth) continue;
first = Polygon[ip].firstVert;
last = Polygon[ip].lastVert;
old_xe = -1;
old_ye = -1;
if (last > first + 1) // ------- are there sufficient vertices?--------
{
if (disp) // here are three points of a polygon calculated but not drawn
{
x1 = (int)(Scale1 * Vert[first].X + 0.5); // starting point
y1 = (int)(Scale1 * Vert[first].Y + 0.5);
}
x2 = Vert[first].X; // will become the first point of the arc
y2 = Vert[first].Y;
x3 = Vert[first + 1].X; // second point
y3 = Vert[first + 1].Y;
Polygon[ip].firstArc = nArc;
// Points from the second one until the one before the last
Len = last - first + 1;
for (j = 2; j <= Len; j++) // =============================
{
x1 = x2; y1 = y2;
x2 = x3; y2 = y3;
x3 = Vert[first + j % Len].X;
y3 = Vert[first + j % Len].Y;
CArc arc = new CArc();
// 'Curvature' calculates and saves parameters of an arc in 'arc'.
rv = Curvature(x1, y1, x2, y2, x3, y3, eps, ref arc);
if (rv < 0) continue;
if (Math.Abs(arc.xb - arc.xe) < 1.0 &&
Math.Abs(arc.yb - arc.ye) < 1.0 || Math.Abs(arc.rad) < 2.0) continue;
// The arc is saved in the list of arcs:
Arc[nArc] = arc;
if (cntArc == 0) Polygon[ip].firstArc = nArc;
cntArc++;
nArc++;
if (disp) //-------------------------------------------------------
{
xb = marginX + (int)(Scale1 * arc.xb + 0.5);
yb = marginY + (int)(Scale1 * arc.yb + 0.5);
if (j == Len)
{
xb = marginX + (int)(Scale1 * Vert[last].X + 0.5);
yb = marginY + (int)(Scale1 * Vert[last].Y + 0.5);
}
xe = marginX + (int)(Scale1 * arc.xe + 0.5);
ye = marginY + (int)(Scale1 * arc.ye + 0.5);
rv = DrawArc(arc, true, fm1);
if (rv < 0) return -1;
Pen bluePen = new System.Drawing.Pen(Color.LightBlue);
if (j > 2 && j < Len && last - first > 2 && old_xe > 0)
fm1.g2Bmp.DrawLine(bluePen, old_xe, old_ye, xb, yb);
old_xe = xe; old_ye = ye;
} //---------------------- end if (disp) --------------------------
if (j == Len - 1) Polygon[ip].lastArc = nArc - 1;
} // ============== end for (j... ================================
} // ------------------------- end if (last > first+1 ) ---------------
} // ================ end for (ip... ================================
return 0;
} // ******************* end FindArcs ***********************************
点击保存,可将多边形和圆弧列表保存在硬盘上,生成一个扩展名为.txt
的文本文件,其中包含多边形列表和圆弧列表。它包含每个多边形的参数,如firstVertex
、lastVertex
、firstArc
和lastArc
。多边形列表之后是圆弧列表。对于每个圆弧,它包含起点的坐标(xb,yb)、终点的坐标、曲率中心的坐标和半径。该文件可以通过任何文本处理程序(例如,Microsoft Word)来读取。目录TEXT
和文本文件的名称根据打开的图像名称自动计算。TEXT
目录应该位于包含打开图像的目录附近。在第十二章中描述的项目WFcircleReco
中可以看到一个版本的保存,允许用户选择目录和文本文件的名称。第十二章中也描述了保存圆圈。
半径计算的精度
我们使用一个具有 328 和 95 像素半轴的理想椭圆的人造图像研究了所述方法的精度(图 11-9 )。
图 11-9
实验中使用的椭圆的人工图像
因为我们知道椭圆的参数,所以可以计算边界上每一点的曲率半径。计算值与FindArcs
产生的值的比较表明,在真实曲率半径小于 174 像素的边界点,产生值的误差小于 15%。在更大的半径下,误差小于 20%。这种精度对于某些应用来说已经足够了。
为了测试小半径情况下的误差值,我们使用了另一个半径为 100 和 200 像素的人造图像(图 11-10 )。在两个人造图像的测试中,曲率半径的估计误差在 20%以下。这与理论研究(Kovalevsky,2001)一致,理论研究表明,数字图像中曲线导数的估计误差由数字化噪声的强度指定,使得像素坐标相当不精确。
图 11-10
实验中使用的自行车链条链节的数码照片(孔被人工填充为黑色,以使图像更简单)
这幅图像的边界包含一个膨胀点,在这里曲率改变了它的符号。该点附近的平均曲率接近于零,这对应于相当大的半径值。在其他点上,估计曲率半径的误差在 20%以下。
结论
本文报道的结果表明,该方法的精度是令人满意的。**
十二、圆形物体的识别和测量
本章介绍了一种识别和测量边界形状类似于圆形的物体参数的方法。我们使用我几年前开发的一种改进的最小二乘法来检查晶片中焊料凸点的质量。在这里,我们描述了这种方法的发展,它可以应用于任何彩色或灰度图像,以识别具有近似圆形的边界的对象。该方法计算半径和中心坐标的估计值。图 12-1 显示了根据苹果大小对其进行分类的示例。
图 12-1
(a)图像和(b)结果的示例
该方法的数学基础
该方法类似于经典的最小二乘法,其本质区别在于,我们最小化的不是给定点与所寻找圆的偏差的平方和,而是半径未知的圆 R 和中心未知的圆( x c 的面积差的平方和, y c 通过给定点(xI,y i ):
deviat =[r-x【I】-(12.1)
*根据积分的指数 i 计算总和,其中 i 得到从 0 到 N - 1 的 N 值。这个标准不同于传统的标准
crit =[r-x【I】-【c】
*我们使用半径差 2 -距离 2 来代替半径差(一个点到中点的距离)。这使得求解变得更加容易和快速,并且得到的圆与经典圆之间的差异非常小。
让我们推导出必要的方程式。我们取方程 12.1 在 x c , y c , R 之后的偏导数。
★deviat/x【c】=【4 (**【r】-)
★deviat/和【c】=【4 (**【r】-)
★deviat/【r =【4 ,【r】-【我】 -】***
并将它们设置为 0。我们得到三个方程:
r-x【I】-
r-x【I】-
r-x【I】-
**左边部分是包含变量xcT5、 y c 和 R 的多项式,幂为 1、2 和 3。我们将在下文中演示如何将方程 12.6 和 12.7 转换成线性方程。
我们打开方程 12.8 中的圆括号,得到
r--【I】+2 **【x】*
或者
r--【c】和【t】
或者
n (r-【c】*和(12.9)
对从 0 到 N -1 的 i 的 N 值进行求和计算。总和∑(R2-xc2-yc2)可以替换为N **(R2-x 既然(R2-xc2-yc*2)不依赖于 i 。
现在让我们变换方程 12.6 来分离表达式R2–xc2–yc2。
r-x【I】-
r-x【I】- x 【我】-【r】-**
r-x【I】- x 【我】-**(r*
第二项xc*∑(。。。)根据等式 12.8 等于 0。因此:
r-x【I】-
**打开圆括号后,我们得到:
(r--【c】和 x【c】+和【I】*【2】-2 )
根据等式 12.9, 我们将(R2-xc2-yc2)替换为∑(xI2–2 *x yc**yI)/N 第一项与 x i 相乘第二项:
x【I】【I】【2】 n-(x【I】-2 **【I】*)*
让我们用 S mn 来表示每一笔总和∑xImyIn*。然后:
s10(s20–2 **x【c】 s+s*
或者
2** xc(N * S20–S102+2 *yc(N * S11–S10 S01=(S34(12.6a)
这是一个线性方程在xc和 y c 。经过类似的变换,方程 12.7 也变成一个线性方程:
2** xc(N * S11–S10* S01+2 **yc(N * S02–S022=(S04(12.7a)
方程 12.6a 和 12.7a 很容易求解。这些解可以在用 S mn 符号重写的方程 12.9 中设定:
r--【c】和
*以获得 R 的值。
我们在这里展示了执行这些解决方案的方法MinAreaN2
的源代码。另外两个类似的稍微简单一点的方法,MinArea2
和MinAreaN
,也在这个项目中使用。MinAreaN2
是这两种方法的结合。
public double MinAreaN2(int ia, iVect2[] P, int Start, int np, ref double radius, ref double x0, ref double y0)
/* Calculates the estimates "x0" and "y0" of the coordinates of the center and the estimate "radius" of the radius of the optimal circle with the minimum deviation from the given set "P[np]" of points. The found values and the 13 sums used for the calculation are assigned to the arc with the index "ia". -- */
{
double SumX, SumY, SumX2, SumY2, SumXY, SumX3, SumY3,
SumX2Y, SumXY2, SumX4, SumX2Y2, SumY4;
double a1, a2, b1, b2, c1, c2, Crit, det, fx, fy, mx, my, N, R2;
int ip;
N = (double)np;
SumX = SumY = SumX2 = SumY2 = SumXY = SumX3 = SumY3 = 0.0;
SumX2Y = SumXY2 = SumX4 = SumX2Y2 = SumY4 = 0.0;
for (ip = Start; ip < Start + np; ip++) //==== over the set of points ====
{
fx = (double)P[ip].X;
fy = (double)P[ip].Y;
SumX += fx;
SumY += fy;
SumX2 += fx * fx;
SumY2 += fy * fy;
SumXY += fx * fy;
SumX3 += fx * fx * fx;
SumY3 += fy * fy * fy;
SumX2Y += fx * fx * fy;
SumXY2 += fx * fy * fy;
SumX4 += fx * fx * fx * fx;
SumX2Y2 += fx * fx * fy * fy;
SumY4 += fy * fy * fy * fy;
} //============ end for (ip...) =============================
a1 = 2 * (SumX * SumX - N * SumX2);
b1 = 2 * (SumX * SumY - N * SumXY);
a2 = 2 * (SumX * SumY - N * SumXY);
b2 = 2 * (SumY * SumY - N * SumY2);
c1 = SumX2 * SumX - N * SumX3 + SumX * SumY2 - N * SumXY2;
c2 = SumX2 * SumY - N * SumY3 + SumY * SumY2 - N * SumX2Y;
det = a1 * b2 - a2 * b1;
if (Math.Abs(det) < 0.00001) return -1.0;
mx = (c1 * b2 - c2 * b1) / det;
my = (a1 * c2 - a2 * c1) / det;
R2 = (SumX2 - 2 * SumX * mx - 2 * SumY *my + SumY2) / N + mx*mx + my*my;
if (R2 <= 0.0) return -1.0;
x0 = mx;
y0 = my;
radius = Math.Sqrt(R2);
elArc[ia].Mx = mx;
elArc[ia].My = my;
elArc[ia].R = radius;
elArc[ia].nPoints = np;
Crit = 0.0;
for (ip = Start; ip < Start + np; ip++) //======== point array =========
{
fx = (double)P[ip].X; fy = (double)P[ip].Y;
Crit += (radius - Math.Sqrt((fx - mx) * (fx - mx) + (fy - my)* (fy - my))) *
(radius - Math.Sqrt((fx - mx) * (fx - mx) + (fy - my) * (fy - my)));
} //============== end for (ip...) ==========================
elArc[ia].SUM[0] = N;
elArc[ia].SUM[1] = SumX;
elArc[ia].SUM[2] = SumY;
elArc[ia].SUM[3] = SumX2;
elArc[ia].SUM[4] = SumY2;
elArc[ia].SUM[5] = SumXY;
elArc[ia].SUM[6] = SumX3;
elArc[ia].SUM[7] = SumY3;
elArc[ia].SUM[8] = SumX2Y;
elArc[ia].SUM[9] = SumXY2;
elArc[ia].SUM[10] = SumX4;
elArc[ia].SUM[11] = SumX2Y2;
elArc[ia].SUM[12] = SumY4;
return Math.Sqrt(Crit / (double)np);
} //*************** end MinAreaN2 *********************************/
项目WFcircleReco
相应的项目WFcircleReco
包含以下内容:
-
包含必要数据结构和处理方法调用的类
Form1
。 -
第七章中描述的边缘检测方法。
-
第十一章中描述的多边形线近似边缘的方法。
-
一种方法
MakeArcs3
,用于将多边形线溶解在称为arcs
的具有近似恒定曲率的子集中。 -
为每组经过的圆弧寻找最佳匹配圆的方法
MakeCirclesEl
。 -
方法
GetAngle
为每个圆弧计算从匹配圆的中心到圆弧端点的半径向量之间的角度。 -
方法
MakeCircles
根据圆弧点与圆的最小偏差,为每组曲率中心相近的圆弧计算最佳圆。 -
显示面、弧和最佳圆的服务方法。
我们从项目WFcircleReco
的描述开始。
项目的形式WFcircleReco
项目WFcircleReco
的形式如图 12-2 所示。
表单的代码由五部分组成,每一部分都在用户单击相应的按钮时开始。
图 12-2
项目的形式WFcircleReco
打开图像部分允许用户选择目录和图像。用户可以打开.bmp
或.jpg
图像。图像将被打开并显示在左侧的图片框中。这部分代码定义了五个工作图像,并显示原始图像。
单击边缘检测后,用户可以定义边缘检测的阈值。他或她可以看到右边图片框中的边缘,并在必要时更改阈值(标准为 20)。
边缘检测根据第七章描述的二值化梯度法调用边缘检测所需的SigmaSimpleUni
和ExtremLightUni
方法。图像ExtremIm
中的边缘被定义为色差或灰度值差大于阈值设置的相邻像素对的序列。色差是具有亮度差符号的颜色通道的强度的绝对差之和。
为了使边缘的处理更简单、更方便,图像CombIm
是用组合坐标(见第七章)和双倍的值width
和height
创建的。
在这个项目中,我们稍微改变了点的标记:我们现在只使用位 0、1 和 2 来指定与点相关的边缘裂纹的数量;不使用位 3、4、5 和 6。位 7 用于标记已经放入队列的点。为了指定边缘的裂缝和点,我们使用第七章中描述的方法LabelCellsSign
。检测到的边缘显示在右边的图片框中。
当用户对检测到的边的质量感到满意时,他或她可以单击多边形。因为要识别的圆的种类非常多,所以我们考虑允许半径的间隔的八种变化:最小的圆的半径可以在 10 和 20 像素之间,而最大的圆的半径可以在 180 和 360 像素之间。对于这些间隔中的每一个,定义多边形近似精度的参数值epsilon
被指定:它从最小圆的 1.05 像素变化到最大圆的 2.00 像素。
零件多边形包含一个贯穿所有八个变量的for
循环。对于每个变量,方法SearchPoly
再次开始。该方法测试包含检测到的边缘的图像CombIm
,并在每个端点或分支点开始方法ComponPoly
。这两个方法是类CListLines
(文件CListLines.cs
)的成员。当ComponPoly
处理完边的所有结束点和分支点后,再次调用它,从不是结束点或分支点的点开始。然后ComponPoly
对所有闭合边曲线进行近似。
方法ComponPoly
与 WFcompressPal 项目(第章第八部分)中的方法几乎相同,但有以下微小差异:由方法ComponLin
检测并保存的包含单个裂纹的短线现在被忽略。它们对图像压缩很重要;然而,它们对于圆识别并不重要。项目WFcompressPal
中ComponLin
调用的方法TraceLin
在这里被方法TraceAppNew
替换,它也跟踪边缘的一条线;然而,它使线的多边形近似。该方法的设计方式使其永远不会产生共线点的序列:如果三个连续的近似点似乎共线,则不保存中间点。为此,使用了一个小方法ParArea
(参见下面的源代码)。它计算多边形的三个连续顶点Vert
上的平行四边形的面积。
下面是TraceAppNew
的源代码。省略了调试所需的指令。
public int TraceAppNew(CImage Comb, int X, int Y, double eps, ref iVect2 Pterm, ref int dir)
/* This method traces a line in the image "Comb" with combinatorial coordinates, where the cracks and points of the edges are labeled: bits 0, 1, and 2 of a point contain the label 1 to 4 of the point. The label indicates the number of incident edge cracks. Bits 3 to 6 are not used. Labeled bit 7 indicates that the point should not be used any more. The crack has only one label 1 in bit 0\. This function traces the edge from one end or branch point to another one while changing the parameter "dir". It makes polygonal approximation with precision "eps" and saves STANDARD coordinates in "Vert". ----------*/
{
int br, Lab, rv = 0;
bool BP = false, END = false, deb = false;
bool atSt_P = false;
iVect2 Crack, P, P1, Pold, Pstand, StartEdge, StartLine, Vect;
Crack = new iVect2();
P = new iVect2();
P1 = new iVect2();
Pold = new iVect2();
Pstand = new iVect2();
StartEdge = new iVect2();
StartLine = new iVect2();
Vect = new iVect2();
int iCrack = 0;
P.X = X; P.Y = Y;
Pstand.X = X / 2;
Pstand.Y = Y / 2;
P1.X = Pold.X = P.X;
P1.Y = Pold.Y = P.Y;
StartEdge.X = X / 2;
StartEdge.Y = Y / 2;
StartLine.X = X / 2;
StartLine.Y = Y / 2;
int[] Shift = { 0, 2, 4, 6 };
int StartVert = nVert;
Vert[nVert].X = Pstand.X;
Vert[nVert].Y = Pstand.Y;
nVert++;
Vect = new iVect2();
int CNX = Comb.width;
int CNY = Comb.height;
CheckComb(StartEdge, Pstand, eps, ref Vect);
while (true) //==================================================
{
Crack.X = P.X + Step[dir].X;
Crack.Y = P.Y + Step[dir].Y;
if (Comb.Grid[Crack.X + CNX * Crack.Y] == 0)
{
MessageBox.Show("TraceAppNew, error: dir=" + dir + " the Crack=(" + Crack.X
+ "," + Crack.Y + ") has label 0; ");
}
P.X = P1.X = Crack.X + Step[dir].X;
P.Y = P1.Y = Crack.Y + Step[dir].Y;
Pstand.X = P.X / 2;
Pstand.Y = P.Y / 2;
br = CheckComb(StartEdge, Pstand, eps, ref Vect);
Lab = Comb.Grid[P.X + CNX * P.Y] & 7; // changed on Nov. 1
switch (Lab)
{
case 1: END = true; BP = false; rv = 1; break;
case 2: BP = END = false; break;
case 3: BP = true; END = false; rv = 3; break;
case 4: BP = true; END = false; rv = 4; break;
}
if (Lab == 2) Comb.Grid[P.X + CNX * P.Y] = 0; // deleting all labels of P
iCrack++;
if (br > 0) //---------------------------------------------------------
{
if (nVert >= MaxVert - 1)
{
MessageBox.Show("Overflow in 'Vert'; X="+X+" Y="+Y + " nVert=" + nVert);
return -1;
}
if (br == 1)
{
if (nVert > (StartVert + 1) && ParArea(nVert, Pold) == 0.0)
{
Vert[nVert - 1].X = Pold.X / 2;
Vert[nVert - 1].Y = Pold.Y / 2;
}
else
{
Vert[nVert].X = Pold.X / 2;
Vert[nVert].Y = Pold.Y / 2;
}
}
else
{
if (nVert>(StartVert + 1) && ParArea(nVert, Pold)==0.0) Vert[nVert-1] = Vect;
else Vert[nVert] = Vect;
} //------------------- end if (br == 1) ----------------------------
if (nVert > (StartVert + 1) && ParArea(nVert, Pold) == 0.0)
{
StartEdge = Vert[nVert - 1];
}
else
{
StartEdge = Vert[nVert];
if (StartEdge.X > 0 || StartEdge.Y > 0) nVert++; // very important!
}
br = 0;
} //---------------- end if (br > 0) ----------------------------------
atSt_P = (Pstand == StartLine);
if (atSt_P)
{
Pterm.X = P.X; // Pterm is a parameter of TraceAppNew
Pterm.Y = P.Y;
Polygon[nPolygon].lastVert = nVert - 1;
Polygon[nPolygon].closed = true;
rv = 2;
break;
}
if (!atSt_P && (BP || END))
{
Pterm.X = P.X;
Pterm.Y = P.Y;
Vert[nVert].X = Pstand.X;
Vert[nVert].Y = Pstand.Y;
Polygon[nPolygon].lastVert = nVert;
Polygon[nPolygon].closed = false;
nVert++;
if (BP) rv = 3;
else rv = 1;
break;
}
if (!BP && !END) //---------------------------
{
Crack.X = P.X + Step[(dir + 1) % 4].X;
Crack.Y = P.Y + Step[(dir + 1) % 4].Y;
if (Comb.Grid[Crack.X + CNX * Crack.Y] == 1)
{
dir = (dir + 1) % 4;
}
else
{
Crack.X = P.X + Step[(dir + 3) % 4].X;
Crack.Y = P.Y + Step[(dir + 3) % 4].Y;
if (Comb.Grid[Crack.X + CNX * Crack.Y] == 1) dir = (dir + 3) % 4;
}
}
else break;
Pold.X = P.X;
Pold.Y = P.Y;
} //================== end while ==============================
Polygon[nPolygon].nCrack = iCrack;
return rv;
} //********************** end TraceAppNew ****************************
现在Form1
调用方法CheckSmooth
,检查所有多边形以确定它们是否平滑。如果多边形的边之间的大角的相对数量小于预定义的比例,则该多边形是平滑的。
然后开始方法MakeArcs3
,该方法将多边形线细分成称为arcs
的具有近似恒定曲率的子集。方法ThreePoints
为多边形线的每三个后续顶点计算通过这三个点的圆的曲率。它的值是 1 除以这个圆的半径。该方法计算曲率中心的坐标,并返回曲率的值。在这个项目中,一个弧可以包含任意数量的顶点,这与项目WFpolyArc
不同,在项目WFpolyArc
中,一个弧总是正好有三个顶点。
方法MakeArcs3
实现了弧的如下定义:
-
弧的所有顶点处的局部曲率的所有值具有相同的符号。曲率等于零不能出现是因为前面提到的
TraceAppNew
的性质。 -
取决于弧的局部曲率和近似的精度,后续多边形的边之间的角度的绝对值具有小于阈值的近似值。
-
多边形边的长度必须小于阈值。
方法MakeArcs3
为多边形线中的每个位置计算四个逻辑变量——bAngle
、bCurve
、bEdge
和bSign
——指定是否满足刚刚指示的条件。如果曲率的符号改变,变量bSign
为true
。
如果所有四个逻辑变量都正常,则某些值被分配给变量Start[iSign]
和End[iSign]
,指定弧的起点和终点位置。变量bSign
指定了局部曲率的符号。圆弧的参数保存在数组elArc
中,该数组是类CListLines
的成员。
方法MakeCirclesEl
然后计算具有相似参数的多组圆弧的最佳圆。它测试所有成对的圆弧,并检查它们的中心点彼此之间的距离是否小于预定义的距离MD
以及它们的曲率半径是否近似相等:关系必须在 0.5 和 2.0 之间。满足这些条件的弧的索引保存在一个数组中,并用于计算包含所有这些弧的最佳圆。
为了计算最佳圆,该项目使用了通过MinAreaN2
方法实现的最小二乘法修正程序。本章前面介绍了程序和方法。该过程非常快速和鲁棒。
项目WFcircleReco
有一个重要特性,即使用多个具有相似圆心和相似半径的圆弧来计算一个圆。它们被放在一个组中,并且为该组计算最佳圆。为了找到所有具有相似中心和相似半径的弧,所有的弧对都被测试。
该项目在许多不同的图像上进行了测试,其中一些是为检查晶片中的焊料凸点而设计的原始项目的旧图像,一些新图像包含苹果、蘑菇、坚果和其他圆形物体。图 12-3 和图 12-4 是已经识别出所有圆形物体并正确估计其参数的图像示例。
图 12-4
认可苹果的项目形式
图 12-3
具有 93 个公认的晶圆凸块圆圈的表单
在左侧的pictureBox1
中,识别出的圆圈用红色画在原始图像上。在pictureBox2
的右侧,检测到的多边形以绿色绘制。
单击保存圆将所有已识别圆的参数列表保存在文本文件中。通过自动和定义选项,用户可以选择两种可能的方式来指定该文件的名称和位置。如果选择自动,文本文件的名称基于打开图像的名称,同时添加位于包含打开图像的目录images
附近的目录的名称TEXT
。打开的图像名称的扩展名.bmp
或.jpg
被替换为扩展名.txt
。
如果选择了“已定义”,则用户可以查看目录列表,并通过单击其中一个可见文件来选择目录和文本文件的名称。如果没有合适的名称,用户可以在相应的字段中输入所需的名称。文本文件保存后,其内容通过MessageBox
显示。******************
十三、交通中自行车的识别
由于所提出的圆识别方法的良好特性,我有了使用该方法来识别自行车车轮的想法,自行车车轮是理想的圆。然而,如果自行车的位置使其框架平面与观察方向成锐角,那么车轮看起来就像椭圆形而不是圆形。因此,我们也需要一种识别椭圆的方法。不幸的是,我们还没有成功地将我们的圆识别方法推广到椭圆上(见第十二章)。我们已经尝试使用众所周知的共轭梯度法,但是我们的实验表明,这种方法并不可靠:当要拟合椭圆的点不在椭圆附近时,这种方法有时会失败。
因为椭圆仅由少数几个参数定义,即五个参数,所以可以使用下一节所述的经典最小二乘法。
椭圆识别的数学基础
轴平行于笛卡尔坐标系的坐标轴且中心位于原点的椭圆具有众所周知的等式
x2/a2+y2/b2= 1。
然而,我们需要考虑移动和倾斜椭圆的一般情况。我们使用圆锥曲线的一般方程:
ax+bxy+cy**+dx+ey+f【注
我们的目标是找到一个椭圆的参数,对于该参数,一组给定点到该椭圆的距离的平方和是最小的。因此,我们的目标函数是
(13.1)
括号中的表达式与椭圆上的点( x i , y i )的距离近似成正比。它包含六个未知系数 A 、 B 、 C 、 D 、 E、和 F 。然而,众所周知,椭圆由五个参数唯一定义。因此,我们将方程 13.1 的所有项除以 A ,并将新系数表示如下:
B/A =2k1
C/A = k2
D/A =2k3
E/A =2k4
F/A = k5
转换后的目标函数是
(13.1a)
f 对k1 的偏导数为
将括号中的所有项乘以xI**yI*后,我们得到
我们将它除以 4,并用 S ( m , n 表示每个,得到
通过将它设置为零,我们获得了未知数k1k2k3k4和 k 5 的五个方程中的第一个。
以类似的方式,我们得到其他四个方程:
该方程组通过使用方法Gauss_K
在方法GetEllipseNew
中实现的众所周知的高斯方法求解。总和S(xm, y n )通过MakeSums2
方法计算。
以下是GetEllipseNew
的源代码。
public int GetEllipseNew(Point[] Vert, int iv1, int nPoints1, int iv2, int nPoints2,
ref double Delta, ref double f, ref double a, ref double b, ref double c, ref double d)
{
bool deb = false;
double[,] A = new double[5, 5];
double[,] B = new double[5, 1];
int nSum = 15;
double[] Sum = new double[nSum];
MakeSums2(Vert, iv1, nPoints1, iv2, nPoints2, Sum);
A[0, 0] = 2.0 * Sum[12];
A[0, 1] = Sum[11];
A[0, 2] = 2.0 * Sum[8];
A[0, 3] = 2.0 * Sum[7];
A[0, 4] = Sum[4];
A[1, 0] = 2.0 * Sum[11];
A[1, 1] = Sum[10];
A[1, 2] = 2.0 * Sum[7];
A[1, 3] = 2.0 * Sum[6];
A[1, 4] = Sum[3];
A[2, 0] = 2.0 * Sum[8];
A[2, 1] = Sum[7];
A[2, 2] = 2.0 * Sum[5];
A[2, 3] = 2.0 * Sum[4];
A[2, 4] = Sum[2];
A[3, 0] = 2.0 * Sum[7];
A[3, 1] = Sum[6];
A[3, 2] = 2.0 * Sum[4];
A[3, 3] = 2.0 * Sum[3];
A[3, 4] = Sum[1];
A[4, 0] = 2.0 * Sum[4];
A[4, 1] = Sum[3];
A[4, 2] = 2.0 * Sum[2];
A[4, 3] = 2.0 * Sum[1];
A[4, 4] = Sum[0];
B[0, 0] = -Sum[13];
B[1, 0] = -Sum[12];
B[2, 0] = -Sum[9];
B[3, 0] = -Sum[8];
B[4, 0] = -Sum[5];
Gauss_K(A, 5, B, 1);
f = -0.5 * Math.Atan2(2.0 * B[0, 0], 1.0 - B[1, 0]);
c = (B[0, 0] * B[3, 0] - B[1, 0] * B[2, 0]) / (B[1, 0] - B[0, 0] * B[0, 0]);
d = (B[0, 0] * B[2, 0] - B[3, 0]) / (B[1, 0] - B[0, 0] * B[0, 0]);
Delta = B[1, 0] - B[0, 0] * B[0, 0];
double BigDelta = B[1, 0] * B[4, 0] + B[0, 0] * B[3, 0] * B[2, 0] +
B[0, 0] * B[3, 0] * B[2, 0] - B[2, 0] * B[1, 0] * B[2, 0] - B[3, 0] * B[3, 0] - B[4, 0] * B[0, 0] * B[0, 0];
double S = 1.0 + B[1, 0];
double a2, b2;
double aprim = (1.0 + B[1, 0] + Math.Sqrt((1.0 - B[1, 0]) * (1.0 - B[1, 0]) + 4.0 * B[0, 0] * B[0, 0])) * 0.5;
double cprim = (1.0 + B[1, 0] - Math.Sqrt((1.0 - B[1, 0]) * (1.0 - B[1, 0]) + 4.0 * B[0, 0] * B[0, 0])) * 0.5;
a2 = -BigDelta / aprim / Delta;
b2 = -BigDelta / cprim / Delta;
a = Math.Sqrt(a2);
b = Math.Sqrt(b2);
if (Delta > 0.0) return 1;
return -1;
} //***************** end GetEllipseNew *********************************
项目WFellipseBike
本项目的形式如图 13-1 所示。
图 13-1
项目的形式WFellipseBike
用户单击打开图像,然后选择文件夹和图像。图像出现在左边的图片框中。然后用户单击检测边缘。有一个数字上下工具,用于选择边缘检测的阈值。但是,预选值 20 适用于所有图像,不应更改。程序自动运行。它使用第十二章中描述的方法进行边缘检测和边缘的多边形近似。然后使用方法MakeArcsTwo
将多边形细分成圆弧。该方法与第十二章中描述的MakeArcs3
方法略有不同。
然后调用方法FindEllipsesMode
来寻找自行车车轮的椭圆。该方法使用根据点数排序的圆弧。排序是通过方法SortingArcs
将排序后的弧的索引写入数组SortArcs
来执行的。点数最多的圆弧停留在SortArcs[0]
。因此,圆弧可以按照点数递减的顺序来取。
椭圆的识别不像圆那么简单:如果圆弧转移到方法GetEllipseNew
寻找包含该圆弧的椭圆包含的点少于十个,有时会计算出椭圆的假参数。因此,我们开发了其他方法来解决这个问题。方法QualityOfEllipseNew
计算位于椭圆附近的圆弧的点数,如图 13-2 所示。
图 13-2
带有已识别椭圆的自行车图像片段
在图 13-2 中,黑线是圆弧,蓝点是圆弧包含的多边形顶点,红线是识别出的椭圆。椭圆的质量估计为椭圆周围试管中蓝色点的数量Sum
。这个数字与值maxPoints
进行比较,该值是作为参数传递给方法GetEllipseNew
的弧ia
中的点数乘以 2π并除以弧的角度。因此maxPoint
是封闭曲线中最大可能的点数。如果值Sum
大于0.5*maxNumber
,则认为椭圆是好的。这里是QualityOfEllipseNew
的代码。
public int QualityOfEllipseNew(int ia, Ellipse Ellipse, int[] SortArcs, Form1 fm1)
// Returns the sum of the numbers of points of arcs near the ellipse.
{
bool deb = false, Disp = false; //true; //
int Dif, goodDartIa, locDart, i, iv, ivm, ive, ja, Sum = 0, x, y, xm, ym, xe, ye;
double angleDart, a = Ellipse.a, b = Ellipse.b, c = Ellipse.c, d = Ellipse.d;
double maxPoints = elArc[ia].nPoints * 6.28 / elArc[ia].Angle;
int ivStart, ivMid, ivEnd, xMain, yMain;
ivStart = elArc[ia].Start;
ivMid = ivStart + elArc[ia].nPoints / 2;
ivEnd = ivStart + elArc[ia].nPoints - 1;
x = Vert[ivStart].X;
y = Vert[ivStart].Y;
double AngleStart = Math.Atan2(y - d, x - c);
xe = Vert[ivEnd].X;
ye = Vert[ivEnd].Y;
double AngleEnd = Math.Atan2(ye - d, xe - c);
xMain = Vert[ivMid].X;
yMain = Vert[ivMid].Y;
double AngleMid = Math.Atan2(yMain - d, xMain - c);
double minAngle = Math.Min(AngleStart, AngleEnd);
double maxAngle = Math.Max(AngleStart, AngleEnd), help;
bool Plus2PI = false;
if (minAngle < 0.0 && maxAngle > 0.0 && !(AngleMid >= minAngle &&
AngleMid < maxAngle))
{
Plus2PI = true;
help = maxAngle;
maxAngle = minAngle + 2 * Math.PI;
minAngle = help;
}
angleDart = 57.3 * Math.Atan2(yMain - elArc[ia].My, xMain - elArc[ia].Mx) + 15.0;
if (angleDart < 0.0) angleDart += 360.0;
goodDartIa = 6 + (int)angleDart / 30;
if (goodDartIa > 11) goodDartIa -= 12;
double AngleJa, Fx, Fxe;
for (i = 0; i < nArcs; i++) //=============================
{
ja = SortArcs[i];
if (ja == ia || elArc[ja].nPoints < 5) continue;
iv = elArc[ja].Start;
ivm = iv + elArc[ja].nPoints / 2;
ive = iv + elArc[ja].nPoints - 1;
x = Vert[iv].X;
y = Vert[iv].Y;
xm = Vert[ivm].X;
ym = Vert[ivm].Y;
xe = Vert[ive].X;
ye = Vert[ive].Y;
Fx = (x - c) * (x - c) / a / a + (y - d) * (y - d) / b / b;
Fxe = (xe - c) * (xe - c) / a / a + (ye - d) * (ye - d) / b / b;
if (Fx < 0.6 || Fx > 1.67 || Fxe < 0.6 || Fxe > 1.67) continue;
angleDart = 57.3 * Math.Atan2((ym - d) * a * a, (xm - c) * b * b);
if (angleDart < 0.0) angleDart += 360.0;
locDart = (int)angleDart / 30;
if (locDart > 11) locDart -= 12;
Dif = Math.Abs(elArc[ja].Dart - locDart);
if (Dif > 6) Dif = 12 - Dif;
if (Disp) DrawOneLongArc(ja, fm1);
if (Dif < 2)
{
if (Disp) DrawOneLongArc(ja, fm1);
for (iv = elArc[ja].Start; iv < elArc[ja].Start + elArc[ja].nPoints; iv++)
{
x = Vert[iv].X;
y = Vert[iv].Y;
AngleJa = Math.Atan2(y - d, x - c);
if (AngleJa < 0.0 && Plus2PI) AngleJa += 6.28;
if (!(AngleJa > minAngle && AngleJa < maxAngle)) Sum += elArc[ja].nPoints;
}
}
} //================== end for (i = 0; ... ===========================
return Sum;
} //********************* end QualityOfEllipseNew ************************
如果椭圆不好,则调用方法HelpArcNew
。该方法获取圆弧ia
作为参数,并找到位于圆弧ia
的曲率圆内的不同圆弧ja
。如果圆弧ja
方向正确且不太靠近圆弧ia
,则与圆弧ia
形成一对。对每一对圆弧(ia, ja
)计算一个椭圆并评估其质量。具有最佳质量的椭圆作为结果返回。这里是HelpArcNew
的代码。
public int HelpArcNew(int ia, int[] SortArcs, ref Ellipse Ellipse, int SumStart, Form1 fm1)
{
bool disp = false;
int Dif, i, ivMid, ivm, ja, xMain, yMain, xm, ym;
ivMid = elArc[ia].Start + elArc[ia].nPoints / 2;
xMain = Vert[ivMid].X;
yMain = Vert[ivMid].Y;
double angleDart, a = Ellipse.a, b = Ellipse.b, c = Ellipse.c, d = Ellipse.d;
int goodDartIa;
angleDart = 57.3 * Math.Atan2(yMain - elArc[ia].My, xMain - elArc[ia].Mx) + 15.0;
if (angleDart < 0.0) angleDart += 360.0;
goodDartIa = 6 + (int)angleDart / 30;
if (goodDartIa > 11) goodDartIa -= 12;
double R = elArc[ia].R, Mx = elArc[ia].Mx, My = elArc[ia].My;
CBox Box = new CBox();
Box.minX = (int)(Mx - R) - 10;
if (c - a < Box.minX) Box.minX -= 20;
Box.maxX = (int)(Mx + R) + 10;
if (c + a > Box.maxX) Box.maxX += 20;
Box.minY = (int)(My - R) - 10;
if (d - b < Box.minY) Box.minY -= 20;
Box.maxY = (int)(My + R) + 10;
if (d + b > Box.maxY) Box.maxY += 20;
DrawRectangleSmart(Box, fm1);
int Dist2 = 0, minDist2 = (int)(1.5 * elArc[ia].R * elArc[ia].R);
int[] jBest = new int[100];
int nBest = 0;
for (i = 0; i < nArcs; i++) //===================================
{
ja = SortArcs[i];
if (!ArcInBox(ja, Box)) continue;
if (elArc[ja].nPoints < 4) continue;
if (disp) DrawOneLongArc(ja, fm1);
Dif = Math.Abs(elArc[ja].Dart - goodDartIa);
if (Dif > 6) Dif = 12 - Dif;
if (Dif > 3) continue;
ivm = elArc[ja].Start + elArc[ja].nPoints / 2;
xm = Vert[ivm].X;
ym = Vert[ivm].Y;
Dist2 = (xm - xMain) * (xm - xMain) + (ym - yMain) * (ym - yMain);
if (Dist2 > minDist2)
{
jBest[nBest] = ja;
nBest++;
}
if (nBest >= 5) break;
} //================= end for (i = 0; =============================
double Delta = 0.0, F = 0.0;
Ellipse Ellipse1 = new Ellipse();
int jbestOpt = -1, maxSum = SumStart, Sum = 0;
for (i = 0; i < nBest; i++) //=========================================
{
if (disp) DrawRedArc(jBest[i], fm1);
GetEllipseNew(Vert, elArc[ia].Start, elArc[ia].nPoints, elArc[jBest[i]].Start,
elArc[jBest[i]].nPoints, ref Delta, ref Ellipse1.f,
ref Ellipse1.a, ref Ellipse1.b, ref Ellipse1.c, ref Ellipse1.d);
Sum = QualityOfEllipseNew(ia, Ellipse1, SortArcs, fm1);
if (disp) DrawEllipse(Ellipse1, fm1);
if (!(Ellipse1.a > 5.0 && Ellipse1.b > 5.0) ||
Ellipse1.d - Ellipse1.b < fm1.height * 2 / 5) Sum = 0;
else
{
if (Sum > maxSum)
{
if (disp) DrawEllipse(Ellipse1, fm1);
if (disp) DrawRedArc(jBest[i], fm1);
maxSum = Sum;
jbestOpt = jBest[i];
Ellipse = Ellipse1;
}
}
} //================== end for (i ... i < nBest; ======================
DrawRedArc(jbestOpt, fm1);
Pen pen = new Pen(Color.Red);
DrawEllipsePen(Ellipse, pen, fm1);
return jbestOpt;
} //******************** end HelpArcNew *******************************
自行车的后轮有时会被骑自行车的人的腿遮住,因此无法检测到椭圆。如果已经识别出前轮的高质量椭圆,则将该椭圆的副本分配给后轮。为了正确完成这一分配,必须计算后轮圆弧ia1
中点MP
处的切线。前轮的椭圆上有一个点P
,与圆弧ia1
的方向相切且方向相同。放置前轮椭圆的副本,使点P
位于点MP
上。然后,用方法HelpArcNew
指定椭圆副本的参数。在调用HelpArcNew
之前和之后,使用方法CminusCel
检查两个椭圆的相对位置,尤其是它们彼此之间的距离。所有这些程序都在方法FindEllipsesMode
中执行。下面是这个方法的代码。
public int FindEllipsesMode(CImage SigmaIm, Ellipse[] ListEllipse, ref int nEllipse, Form1 fm1)
{
int[] SortArcs = new int[nArcs];
int maxNP = 0, k = SortingArcs(SortArcs, ref maxNP);
int i, ia, ia1, i0, i1;
nEllipse = 0;
double a = 0.0, b = 0.0, c = 0.0, d = 0.0; //, fret = 0.0;
int[,] List = new int[20, 1200];
int[] nArcList = new int[20];
SCircle[] Circle = new SCircle[20];
for (i = 0; i < 20; i++)
{
Circle[i] = new SCircle();
Circle[i].goodCirc = true;
}
Ellipse[] smalList = new Ellipse[20];
for (i = 0; i < 20; i++) smalList[i] = new Ellipse();
int Sum1 = 0;
double AnglePerPoint = 0.0, maxPoints = 0.0;
fm1.progressBar1.Visible = true;
fm1.progressBar1.Step = 1;
int jump, Len = nArcs, nStep = 20;
if (Len > 2 * nStep) jump = Len / nStep;
else
jump = 2;
double Delta = 0.0, f = 0.0, F = 0.0;
Ellipse Ellipse1 = new Ellipse();
Ellipse Ellipse2 = new Ellipse();
int[] Pattern = new int[100000];
double aa = 0.0, bb = 0.0, cc = 0.0, dd = 0.0;
for (i0 = 0; i0 < nArcs; i0++) //======================================
{
if ((i0 % jump) == jump - 1) fm1.progressBar1.PerformStep();
ia = SortArcs[i0];
DrawRedArc(ia, fm1);
if (elArc[ia].nPoints <= 5) break;
GetEllipseNew(Vert, elArc[ia].Start, elArc[ia].nPoints, 0, 0, ref Delta, ref f,
ref a, ref b, ref c, ref d);
DrawEllipse(f, a, b, c, d, fm1);
if (b < 20.0 || a < 6.0 || d + b > fm1.height || d - 4 * b < 0.0) continue;
int jbestOpt = -1;
Ellipse1.a = a;
Ellipse1.b = b;
Ellipse1.c = c;
Ellipse1.d = d;
Point P1 = new Point(0, 0);
Point P2 = new Point(0, 0);
if (a > 5.0 && b > 5.0)
{
Sum1 = QualityOfEllipseNew(ia, Ellipse1, SortArcs, fm1);
AnglePerPoint = elArc[ia].Angle / elArc[ia].nPoints;
maxPoints = 2 * Math.PI / AnglePerPoint;
if (b > fm1.height / 4 || elArc[ia].nPoints < 10) Sum1 = 0;
Pen pen = new Pen(Color.Red);
if (elArc[ia].nPoints < 10 || d + b > fm1.height * 2 / 5)
{
jbestOpt = HelpArcNew(ia, SortArcs, ref Ellipse1, Sum1, fm1);
DrawEllipse(Ellipse1, fm1);
}
}
for (i1 = i0 + 1; i1 < nArcs; i1++) //=================================
{
ia1 = SortArcs[i1];
if (!Position(ia1, Ellipse1, fm1)) continue;
if (elArc[ia1].nPoints <= 5)
{
MessageBox.Show("Finishing the search for ia1");
break;
}
CBox BoxP1 = new CBox();
CBox BoxP2 = new CBox();
int iv = elArc[ia].Start, x = Vert[iv].X, y = Vert[iv].Y;
int iv1 = elArc[ia1].Start, x1 = Vert[iv1].X, y1 = Vert[iv1].Y;
GetEllipseNew(Vert, elArc[ia1].Start, elArc[ia1].nPoints, 0, 0, ref Delta, ref f,
ref a, ref b, ref c, ref d);
DrawEllipse(f, a, b, c, d, fm1);
if (!(a > 5.0 && b > 5.0)) continue;
DrawRedArc(ia1, fm1);
double K2 = DrawTangent(ia1, ref P2, fm1);
P1 = PointWithTangent(Ellipse1, K2, elArc[ia1].Dart, fm1);
if (P1.X == 0 && P1.Y == 0) continue;
Ellipse2 = Ellipse1;
Ellipse2.c = P2.X + Ellipse1.c - P1.X;
Ellipse2.d = P2.Y + Ellipse1.d - P1.Y;
DrawEllipse(Ellipse2, fm1);
if (d + b > fm1.height || d - b < fm1.height * 2 / 5) continue;
jbestOpt = -1;
if (Ellipse2.a > 5.0 && Ellipse2.b > 5.0)
{
Sum1 = QualityOfEllipseNew(ia1, Ellipse2, SortArcs, fm1);
AnglePerPoint = elArc[ia1].Angle / elArc[ia1].nPoints;
maxPoints = 2 * Math.PI / AnglePerPoint;
Pen pen = new Pen(Color.Red);
if ((elArc[ia1].nPoints < 10 || d + b > fm1.height * 2 / 5) &&
CminusCel(Ellipse1, Ellipse2, fm1))
{
jbestOpt = HelpArcNew(ia1, SortArcs, ref Ellipse2, Sum1, fm1);
DrawEllipse(Ellipse2, fm1);
}
}
bool CMINC = CminusCel(Ellipse1, Ellipse2, fm1);
if (!CMINC) continue;
ListEllipse[nEllipse] = Ellipse1;
nEllipse++;
ListEllipse[nEllipse] = Ellipse2;
nEllipse++;
if (nEllipse >= 2)
{
fm1.progressBar1.Step = fm1.progressBar1.Maximum - Len / (100 / 6) -
fm1.progressBar1.Value;
fm1.progressBar1.PerformStep();
return 1;
}
} //============== end for (i1 = 0; ... ============================
} //=============== end for (i0 = 0; ... =============================
MessageBox.Show("FindEllipsesMode: no bike recognized");
return -1;
} //****************** end FindEllipseMode ******************************
车轮的识别是自行车识别中最重要的部分。当车轮的两个椭圆都被识别时,方法RecoFrame
被调用。它能识别自行车的行驶方向。这种方法包含几种不同类型的框架的简单模型。每种车架出现两次:一次是自行车前轮的前叉在左边,另一次是前叉在右边。这些模型被一个接一个地测试。每个模型都经过变换,使得轮轴的位置与椭圆的中心相匹配。在框架的每个直线段周围形成一个狭窄的矩形。然后,经过处理的图像中的所有多边形都被通过,并且适合矩形的多边形边的长度被求和。具有最大总和的模型获胜。这样,自行车的运动方向就被识别出来了。图 13-3 中显示了框架的模型示例。
图 13-3
框架模型的示例
识别方向的另一种方法
刚刚描述的方法在框架平面与观察方向成锐角时有效,例如图 13-1 。此外,框架的某些部分经常被骑车人的腿遮挡。因此,我们开发了另一种方法黑斑,返回自行车运动的方向:0 为右,2 为左。该方法在每个车轮内制作一个框,计算亮度直方图,并对这些框中几乎水平的多边形边的长度求和,并决定自行车的绘制方向。通过比较低于阈值的直方图值的总和,即框中暗像素的数量加上两个框中的 6 *总和,来做出决定。较大的值表示后轮,也表示绘制方向:如果后轮在右手边,则自行车向左移动。
当识别出车轮的椭圆和运动方向时,车轮的椭圆和车架的型号会显示在右侧的图片框中。显示屏中间的消息框显示消息“识别到向左行驶的自行车”如果用户单击保存结果,将在磁盘上保存一个文本文件,其上下文对应于以下示例:
“向左的自行车有椭圆形的轮子。
第一:a = 113b = 173c = 864d = 796。
第二:a = 114b = 173c = 469d = 790。"
记法如下:a 是椭圆的水平半轴,b 是其垂直半轴,c 是车轮中心的 x 坐标,d 是其 y 坐标。
我们测试了大约 100 张显示自行车在城市街道上行驶的照片。除了大约 2000 × 1500 像素的大图像和大约 150 像素长的小自行车(图 13-4 )之外,自行车在所有照片中都被识别出来。
图 13-4
带有未识别自行车的图像示例
图 13-5 提供了一些已识别自行车的图像示例。
图 13-5
识别自行车的图像示例
十四、细胞分化的计算机模型
本章不属于图像处理领域。之所以把它收入本书,是因为作者的兴趣圈广泛而多才多艺。这一章的主题与生物学有关。
人们可能会问,尽管事实上不同类型的细胞都具有完全相同的脱氧核糖核酸(DNA),但它们如何可能在有机体的生长过程中发育成不同类型的细胞。可以假定,每个细胞都必须获得一些信息,表明它在生长的有机体中的位置,也许是某种坐标。然后,DNA 必须包含不同的指令,用于具有不同坐标的细胞的发育。坐标可以在单元划分时产生:与具有坐标集( X,Y,Z )的旧单元相邻的新单元必须得到旧单元的其中一个坐标增加或减少了 1 的坐标集。改变三个坐标中的哪一个取决于向量从旧单元到新单元的方向。因此,这一章的两个主要观点是,细胞必须获得某种坐标,而指定细胞特性的 DNA 指令必须依赖于它的坐标。
我们已经开发了一个项目WFcellDivision
,其中多细胞生物由 63 × 63 个细胞的阵列Org
(生物)建模(奇数更好地精确定义中间的细胞)。
通过指定和保存其特征来模拟产生新单元的过程。每个细胞可以拥有坐标 X 和 Y 作为其特征,一个变量Property
(这是一个颜色索引),以及数组DNA[63×63]
作为其遗传信息。单元格是CCelln
类的一个对象。
int const Cwidth=63, Cheight=63;
class CCelln
{ public:
int X, Y, Property;
unsigned char DNA[Cwidth*Cheight];
}
该项目的目的是模拟一个接一个细胞的生成,并为每个细胞的变量Property
赋值。该值取决于该单元格的坐标以及保存在该单元格中的DNA
的内容。DNA``to Properties of all cells of Org is supposed to be impossible. It is only possible to copy the DNA from one cell to the neighboring new originating cell and to copy inside the originating cell one certain value from the DNA of the cell to the Property of this cell. The aim of the project is to demonstrate that in spite of these limitations, it is possible to assign correct values of Property to all cells of Org.
让我们描述一下这个项目的运作。方法Form1
从定义和初始化拥有 63 × 63 个单元的存储器的数组Org
开始。初始化Org
包括将Org
的每个单元的Property
和数组DNA[63×63]
的元素设置为−1
,这意味着该单元不存在。
然后数组Org
中间的一个单元格,即坐标为(31,31)的单元格被“填充”:它获得坐标 X = 31 和 Y = 31。它的数组DNA
填充了一个 63 × 63 像素的小数字Image
的颜色索引。该图像是任意选择的,以证明具体的内容可以分配给细胞的属性。DNA
的内容成为数字图像的副本,其中每个元素是一个颜色索引,一个 0 到 255 之间的数字,可以通过颜色表Palette
转换成一种颜色。在Palette
中,为每个颜色索引保存一种颜色(红、绿、蓝)。数组Org
的所有其他单元最初的坐标等于−1
,数组DNA
为空(即,用−1
填充)。
接下来调用方法Grow
。它模拟生物体的生长,同时从与中央细胞(31,31)相邻的细胞开始生成细胞坐标。坐标以这样的方式生成,即所有原始单元都位于围绕中心单元旋转的螺旋中。在生长过程中,越来越多的单元从相邻单元之一获得数组DNA
的副本。它们还根据DNA
和单元坐标的内容获得它们的坐标和某些Property
值。
坐标为(31,31)的单元格的Property
获取数组DNA
(见图 14-1 、DNA[31, 31]
的单个元素的值,即像素(31,31)的颜色索引。
图 14-1
将图像复制到Org[31, 31].DNA
生长生物体的形状由代表生长生物体的颜色区域周围的DNA
的一些元素分配的黑色区域的边界指定。该数组在对应于应该存在于生物体中的细胞的每个元素中包含不同于(0,0,0)的颜色索引,并且在对应于不应该存在于生物体中的细胞的每个元素中包含指向(0,0,0)(黑色)的索引。
如果数组Org
的一个单元格具有非零坐标,值Property
指向不同于(0,0,0)的颜色,并且DNA
的内容不同于-1,则该单元格被填充。
当生长过程开始时,数组Org
中与填充单元格相邻的每个单元格都获得数组DNA
的精确副本,该副本对所有单元格都是相同的。该单元的坐标获得的值与填充的相邻单元的坐标相差 1。例如,如果新单元格位于坐标为( X,Y )的已填充单元格的右侧,则它的 X 坐标获得值 X 1 = X + 1,它的 Y 坐标 Y 1 获得值 Y 。新单元获得了DNA
的标准副本,其Property
等于DNA[X1, Y1]
,其中 X 1 和 Y 1 是新单元的坐标。
这里是Grow
的代码。
int Grow(CCelln[] Org, int width, int height)
{ int cnt=0, i, k, x=(width-1)/2, y=(height-1)/2, nCount=1, X, Y;
do
{ for (i=0; i<nCount && x<width-1; i++)
{ x++;
X=Org[x+width*y].X=Org[x-1+width*y].X+1;
Y=Org[x+width*y].Y=Org[x-1+width*y].Y;
for (k=0; k<width*height; k++) Org[x+width*y].DNA[k] =
Org[x-1+width*y].DNA[k];
Org[x+width*y].Property=Org[x+width*y].DNA[X+width*Y];
}
cnt+=nCount;
if (cnt==width*height) break;
for (i=0; i<nCount && y<height-1; i++)
{ y++;
X=Org[x+width*y].X=Org[x+width*(y-1)].X;
Y=Org[x+width*y].Y=Org[x+width*(y-1)].Y+1;
for (k=0; k<width*height; k++) Org[x+width*y].DNA[k] =
Org[x+width*(y-1)].DNA[k];
Org[x+width*y].Property=Org[x+width*y].DNA[X+width*Y];
}
cnt+=nCount;
if (cnt==width*height) break;
nCount++;
for (i=0; i<nCount && x>0; i++)
{ x--;
X=Org[x+width*y].X=Org[x+1+width*y].X-1;
Y=Org[x+width*y].Y=Org[x+1+width*y].Y;
for (k=0; k<width*height; k++)
Org[x+width*y].DNA[k]=Org[x+1+width*y].DNA[k];
Org[x+width*y].Property=Org[x+width*y].DNA[X+width*Y];
}
cnt+=nCount;
if (cnt==width*height) break;
for (i=0; i<nCount && y>0; i++)
{ y--;
X=Org[x+width*y].X=Org[x+width*(y+1)].X;
Y=Org[x+width*y].Y=Org[x+width*(y+1)].Y-1;
for (k=0; k<width*height; k++)
Org[x+width*y].DNA[k]=Org[x+width*(y+1)].DNA[k];
Org[x+width*y].Property=Org[x+width*y].DNA[X+width*Y];
}
cnt+=nCount;
if (cnt==width*height) break;
nCount++;
} while(1);
return 1;
}
当数组Org
的所有单元都被填充时,该过程结束。然后将所有单元格的Property
值复制到一个 63 × 63 像素的新数组(图像)中,目的是使结果可见。该图像与颜色表Palette
一起显示,用户可以看到该图像与复制到DNA[31, 31]
的原始图像Image
相同,除了数组DNA
中的像素具有指向黑色的索引。这些像素显示为黑色(参见图 14-2 )。
图 14-2
从Org[*].Property
复制的结果图像
因此,数组Org
的所有单元都是不同的:它们包含不同的Property
值,尽管它们都具有数组DNA
的完全相同的副本。
结论
我们已经证明,尽管生物体的所有细胞都具有完全相同的遗传信息拷贝,并且尽管禁止将数组DNA
直接复制到数组Org[x, y].Property
中,但是产生具有不同细胞的生物体是可能的。