0. 完整仓库 & 教程:
这一章节的完整代码在:Chapter 8. Erosion, Dilation, Opening, Closing, and Connection
如果你喜欢这个系列的文章或者感觉对你有帮助,请给我的仓库一个⭐️。
1. 腐蚀、膨胀、开运算和闭运算
1.1 腐蚀
算法:
当 中的集合 𝐴 和 集合𝐵 ,𝐵 对 𝐴 的侵蚀表示为 𝐴⊖𝐵 ,定义为:
在代码的实现中,该算法使用卷积内核(convolution kernel) 𝐵 对图像 𝐴 与进行卷积。内核 𝐵 具有可定义的锚点,即中心点。然后在图像上滑动内核 𝐵,提取内核 𝐵 覆盖区域的最小像素值,并替换锚点处的像素。
图像和结果对比(noisy_fingerprint, noise_rectangle):
结果分析:
腐蚀操作将减少图像中的高光区域(在我们的例子中为纯白色)。 它本质上是沿着图中物体的边界移除像素并减小物体的大小。 它具有减少零星高光噪点的效果,并且其效果随着卷积元素尺寸的增大而增加,同时消耗的时间也会急剧增加。
代码实现(完整代码见顶部GitHub):
for(int i = 0; i < image->Height; i++) {
for(int j = 0; j < image->Width; j++) {
int min = 255;
// the size of structual element can be changed values of x and y:
for(int x = -1; x <= 1; x++) {
for(int y = -1; y <= 1; y++) {
int temp = tempin[(image->Width)*(i+x) + (j+y)];
if(temp < min) min = temp;
}
}
tempout[image->Width * i + j] = min;
}
}
1.2 膨胀
算法:
当 中的集合 𝐴 和 集合𝐵 ,𝐵 对 𝐴 的膨胀表示为 𝐴⊕𝐵 ,定义为:
与腐蚀算法类似,它也使用卷积内核 𝐵 对图像 𝐴 与进行卷积。 然后在图像上滑动核𝐵,提取核𝐵覆盖区域的最大像素值,并替换中心点的像素。
图像和结果对比(noisy_fingerprint, noise_rectangle):
结果分析:
膨胀与腐蚀相反,它将放大图像中的高光区域(在我们的例子中为纯白色)。 它具有放大细节的效果,放大的程度取决于卷积元素的大小。 但膨胀操作会放大图像中不需要的零星噪声。
代码实现(完整代码见顶部GitHub):
for(int i = 0; i < image->Height; i++) {
for(int j = 0; j < image->Width; j++) {
int max = 0;
// the size of structual element can be changed values of x and y:
for(int x = -1; x <= 1; x++) {
for(int y = -1; y <= 1; y++) {
int temp = tempin[(image->Width)*(i+x) + (j+y)];
if(temp > max) max = temp;
}
}
tempout[image->Width * i + j] = max;
}
}
1.3 开运算
算法:
结构元素 𝐵 在集合 𝐴 上的开运算,记为 𝐴 ∘ 𝐵,定义为:
由公式可见,它可以通过先腐蚀,再膨胀来实现:
图像和结果对比(noisy_fingerprint, noise_rectangle):
结果分析:
开运算可用于消除小物体、在细点处分离物体,以及平滑较大物体的边界又不显着改变其面积。 它可以抑制小于结构元素的明亮细节。 又可以避免在仅使用腐蚀操作时图像中物体尺寸的损失。
代码实现(完整代码见顶部GitHub):
void Opening(Image *image) {
unsigned char *tempout;
Image *outimage;
outimage = CreateNewImage(image, (char*)"#testing function");
tempout = outimage->data;
outimage = Dilation(Erosion(image));
SavePNMImage(outimage, (char*)"Opening.pgm");
}
1.4 闭运算
算法:
结构元素 𝐵 在集合 𝐴 上的闭运算,记为 𝐴 ∙ 𝐵,定义为:
由公式可见,它可以通过先膨胀,再腐蚀来实现:
图像和结果对比(noisy_fingerprint, noise_rectangle):
结果分析:
闭合运算可用于填充对象中的小的(暗)孔、连接相邻对象、平滑其边界而不显着改变其面积,以及抑制小于结构元素的暗处细节。
代码实现(完整代码见顶部GitHub):
void Closing(Image *image) {
unsigned char *tempout;
Image *outimage;
outimage = CreateNewImage(image, (char*)"#testing function");
tempout = outimage->data;
outimage = Erosion(Dilation(image));
SavePNMImage(outimage, (char*)"Closing.pgm");
}
2. 边界提取
算法:
图像内物体边界的提取可以表示为集合𝛽(𝐴)(图𝐴内的边界),可以先用结构𝐵腐蚀𝐴,然后对𝐴与腐蚀的结果进行集合差运算,即:
其中 𝐵 应该是一个适当的结构元素。
图像和结果对比(licoln, U):
结果分析:
图像内边界的提取是通过从原始图像中减去被结构元素侵蚀的图像而获得的。 当结构单元的尺寸增加时,边界的宽度也增加。
代码实现(完整代码见顶部GitHub):
void ExtractBoundaries(Image *image) {
unsigned char *tempin, *tempout;
Image *outimage;
outimage = CreateNewImage(image, (char*)"#testing function");
int size = image->Width * image->Height;
outimage = Erosion(image);
tempin = image->data;
tempout = outimage->data;
for(int i = 0; i < size; i++) {
tempout[i] = tempin[i] - tempout[i];
}
SavePNMImage(outimage, (char*)"Boundaries.pgm");
}
3. 统计连通分量的个数
算法:
教科书上的算法是:
其中𝐵是适当的结构元素,当时迭代操作将停止。 不过,深度优先搜索(Deep First Search)算法非常适合且高效地解决这个问题。 其简化伪代码如下所示:
这部分需要少量的算法基础。如果检测到某个像素是白色的,则使用递归计算与该像素相连的所有白色分量的总和(使用8 邻接:8-adjacent),并保存该区域的总和值。 还需要建立真值表,记录每个白色分量是否被遍历过,并跳过已遍历过的区域或者为0(黑色)或者超出图像边界的区域。
结果(请参阅库里随附的txt文件“Connected Components.txt”):
结果分析:
深度优先搜索(DFS)算法被证实非常有效,因为它只使用递归遍历一次图像中的所有像素。 因此它的时间复杂度为 𝑂(𝑀𝑁),其中 𝑀, 𝑁 是边长。 输出包含图像中每个白色分量的像素计数(参阅文件“Connected Components.txt”)。
代码实现(完整代码见顶部GitHub):
(1) 计数的主函数:
void connectedComponent(Image *image) {
int size = image->Width * image->Height;
int checkBoard[size];// mark the traversed pixels
FILE *fp;
fp = fopen("Connected Components.txt", "w");
int index = 1;
for(int i = 0; i < size; i++) {
checkBoard[i] = 0;// initialize the checkboard
}
fprintf(fp, " No. Count\n");
for(int i = 0; i < image->Height; i++) {
for(int j = 0; j < image->Width; j++) {
int count = DFS(image, checkBoard, j, i);// use Deep First Search here
if(count != 0) fprintf(fp, "%3d: %8d\n", index++, count);
}
}
fclose(fp);
}
(2) 深度优先搜索算法:
int DFS(Image * image, int *checkBoard, int x, int y) {
unsigned char *tempin;
tempin = image->data;
int currPosition = image->Width * y + x;
// Base Case:
if(x < 0 || y < 0 || x >= image->Width || y >= image->Height || checkBoard[currPosition] == 1 || tempin[currPosition] == 0) {
return 0;
}
// Recursive Steps:
checkBoard[currPosition] = 1;
int tempSum = 0;
// use 8-adjacent checking:
for(int m = -1; m <= 1; m++) {
for(int n = -1; n <= 1; n++) {
tempSum += DFS(image, checkBoard, x+m, y+n);
}
}
return (1 + tempSum);
}
4. 区分出3组白色气泡
任务要求:
图中含有一定数量的大小相等的白色气泡
(1)提取出所有和边界相连的气泡
(2)提取出所有单独的(不和其他气泡重叠)气泡
(3)提取出所有彼此重叠的气泡,忽略单独的气泡
算法:
此算法使用 8 邻接(8-adjacent)来确定连通分量。
(1) 使用与上面 3 中类似的方法找到图像的白色边界。 只需要从边界上的某一个白色像素开始。 使用标签表将所有遍历的像素标记为1,即为第一组的结果图像。
(2) 使用DFS计算图像中连通分量的像素数。 由于发现每个气泡的面积在350~450像素左右。 所以这个尺寸内的所有连通分量都是单个气泡,将它们标记为2。此外,尺寸大于450的组件将是重叠气泡,将其标记为3。
(3)输出包含不同种类标签的像素的图像。
图像和结果对比(bubbles_on_black_background):
结果分析:
该代码通过深度优先搜索算法标记不同种类的连通分量,实现了三种气泡集合的一次性分离和输出。 该算法的时间复杂度也是𝑂(𝑀𝑁)。
代码实现(完整代码见顶部GitHub):
(1) 气泡分离的主函数:
void SeparateBubbles(Image *image) {
unsigned char *tempout_boundary, *tempout_single, *tempout_overlap;
Image *boundary_image, *single_image, *overlap_image;
boundary_image = CreateNewImage(image, (char*)"#testing function");
single_image = CreateNewImage(image, (char*)"#testing function");
overlap_image = CreateNewImage(image, (char*)"#testing function");
tempout_boundary = boundary_image->data;
tempout_single = single_image->data;
tempout_overlap = overlap_image->data;
int size = image->Width * image->Height;
int checkBoard[size], labelBoard[size];
// initialize the checkboard and labelboard,
// and set the background of the output images to black:
for(int i = 0; i < size; i++) {
tempout_boundary[i] = 0;
tempout_single[i] = 0;
tempout_overlap[i] = 0;
labelBoard[i] = 0;
checkBoard[i] = 0;
}
// 1: Extract all the particles that merged with boundaries:
DFS_MarkPixels(image, labelBoard, 1, 1, 1);
DFS_MarkPixels(image, labelBoard, 200, 781, 1);
// 2 & 3: Extract all the single, and overlapping particles:
for(int i = 7; i < image->Height-7-1; i++) {
for(int j = 7; j < image->Width-7-3; j++) {
int area = DFS(image, checkBoard, j, i);// record the size of a connected area
if(area > 350 && area < 450) DFS_MarkPixels(image, labelBoard, j, i, 2);
else DFS_MarkPixels(image, labelBoard, j, i, 3);
}
}
// output the images:
for(int i = 0; i < size; i++) {
if(labelBoard[i] == 1) tempout_boundary[i] = 255;
if(labelBoard[i] == 2) tempout_single[i] = 255;
if(labelBoard[i] == 3) tempout_overlap[i] = 255;
}
SavePNMImage(boundary_image, (char*)"Boundary_image.pgm");
SavePNMImage(single_image, (char*)"Single_image.pgm");
SavePNMImage(overlap_image, (char*)"Overlap_image.pgm");
}
(2) 标记图像中所有像素的函数:
void DFS_MarkPixels(Image *image, int *labelBoard, int x, int y, int label) {
unsigned char *tempin;
tempin = image->data;
int currPosition = image->Width * y + x;
// Base Case:
if(x < 0 || y < 0 || x >= image->Width || y >= image->Height || labelBoard[currPosition] != 0 || tempin[currPosition] == 0) {
return;
}
// Recursive Steps:
labelBoard[currPosition] = label;
// use 8-adjacent marking:
for(int m = -1; m <= 1; m++) {
for(int n = -1; n <= 1; n++) {
DFS_MarkPixels(image, labelBoard, x+m, y+n, label);;
}
}
}
(3)DFS函数(同3)
int DFS(Image * image, int *checkBoard, int x, int y) {
unsigned char *tempin;
tempin = image->data;
int currPosition = image->Width * y + x;
// Base Case:
if(x < 0 || y < 0 || x >= image->Width || y >= image->Height || checkBoard[currPosition] == 1 || tempin[currPosition] == 0) {
return 0;
}
// Recursive Steps:
checkBoard[currPosition] = 1;
int tempSum = 0;
// use 8-adjacent checking:
for(int m = -1; m <= 1; m++) {
for(int n = -1; n <= 1; n++) {
tempSum += DFS(image, checkBoard, x+m, y+n);
}
}
return (1 + tempSum);
}
5. 完整代码
这一章节的完整代码在:Chapter 8. Erosion, Dilation, Opening, Closing, and Connection
更多关于数字图像处理的章节,以及所有的原图像在:Introduction to Digital Image Processing
整理代码、翻译原理,和可视化每一个章节的工作非常耗时耗力,并且不会有任何收入和回报。如果你喜欢这个系列的文章或者感觉对你有帮助,请给我的仓库一个⭐️,这将是对独立作者最大的鼓励。
-END-