bwlabel 是 MATLAB 中的一个核心函数,用于对二值图像(黑白图像)中的连通区域进行标记,它会扫描图像,找到所有互相连接的白色像素组,并为每个组分配一个唯一的整数标签。

在 C 语言中,没有内置的 bwlabel 函数,但我们可以通过经典的连通域标记算法来手动实现它,最常用和最经典的算法是两遍扫描算法,也称为连通分量标记算法。
算法核心思想:两遍扫描
这个算法之所以高效,是因为它只需要对图像进行两次完整的扫描。
第一遍扫描:临时标记和等价类建立
-
从左到右,从上到下扫描图像的每一个像素。
-
如果当前像素是目标像素(白色像素,值为 1),就检查它上方和左侧的邻居像素。
(图片来源网络,侵删) -
上方和左侧邻居都不是目标像素。
- 这意味着我们遇到了一个新的、独立的连通区域。
- 为这个新区域分配一个新的、唯一的标签(从 2 开始,因为 0 通常表示背景)。
- 将当前像素的标签设置为新标签。
- 将
(新标签, 新标签)添加到一个等价关系表(通常是一个数组或并查集结构)中,这表示该标签目前只等价于它自己。
-
只有一个邻居是目标像素。
将当前像素的标签设置为该邻居的标签。
-
上方和左侧邻居都是目标像素。
(图片来源网络,侵删)- 这是最关键的一步,这两个邻居可能属于同一个连通区域,也可能属于不同但相邻的区域。
- 将当前像素的标签设置为上方邻居的标签。
- 将上方邻居的标签和左侧邻居的标签记录为等价标签,如果上方是 2,左侧是 3,我们就知道标签 2 和 3 实际上代表的是同一个区域,我们将这个
(2, 3)的对记录下来。
经过第一遍扫描后,图像中的每个像素都有一个临时标签,但这些标签可能不是最终的,因为等价标签(如 2 和 3)需要被合并。
第二遍扫描:标签统一
-
处理等价类:在开始第二遍扫描之前,我们需要先处理第一遍扫描中收集到的所有等价关系,目标是找到一个最小标签作为每个等价类的“代表元”。
- 我们有等价关系对 (2,3), (3,4), (5,6),我们可以将它们合并成两个等价类:{2, 3, 4} 和 {5, 6}。
- 为每个等价类选择一个最小的标签作为最终标签,{2, 3, 4} -> 2,{5, 6} -> 5。
- 这个过程可以通过并查集 数据结构非常高效地实现。
-
从左到右,从上到下再次扫描图像。
-
对于每个像素的临时标签,查找它所属等价类的代表元。
-
将当前像素的标签替换为代表元。
经过这两遍扫描,所有属于同一个连通区域的像素都会拥有相同的、最小的标签。
C 语言实现代码
下面是一个完整的 C 语言实现,包含了注释以解释每一步。
#include <stdio.h>
#include <stdlib.h>
// 图像结构体,方便传递
typedef struct {
int width;
int height;
unsigned char *data; // 数据指针,按行存储
} Image;
// 并查集结构,用于管理等价标签
typedef struct {
int *parent;
int size;
} UnionFind;
// 初始化并查集
UnionFind* uf_init(int size) {
UnionFind* uf = (UnionFind*)malloc(sizeof(UnionFind));
uf->parent = (int*)malloc(size * sizeof(int));
uf->size = size;
for (int i = 0; i < size; i++) {
uf->parent[i] = i; // 初始时,每个元素的父节点是自己
}
return uf;
}
// 查找根节点(带路径压缩优化)
int uf_find(UnionFind* uf, int x) {
if (uf->parent[x] != x) {
uf->parent[x] = uf_find(uf, uf->parent[x]); // 路径压缩
}
return uf->parent[x];
}
// 合并两个集合
void uf_union(UnionFind* uf, int x, int y) {
int rootX = uf_find(uf, x);
int rootY = uf_find(uf, y);
if (rootX != rootY) {
uf->parent[rootY] = rootX; // 将 rootY 的父节点设为 rootX
}
}
// 释放并查集内存
void uf_free(UnionFind* uf) {
free(uf->parent);
free(uf);
}
/**
* @brief C 语言实现 bwlabel 功能
* @param binary_img 输入的二值图像 (0: 背景, 非0: 目标)
* @param labeled_img 输出的标记图像
* @return 连通区域的数量
*/
int bwlabel_c(const Image* binary_img, Image* labeled_img) {
int width = binary_img->width;
int height = binary_img->height;
// 初始化输出图像,背景为0
labeled_img->width = width;
labeled_img->height = height;
labeled_img->data = (unsigned char*)malloc(width * height * sizeof(unsigned char));
for (int i = 0; i < width * height; i++) {
labeled_img->data[i] = 0;
}
// 1. 第一遍扫描
int next_label = 1; // 下一个可用的标签
UnionFind* uf = uf_init(width * height / 4 + 10); // 并查集大小预估
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
if (binary_img->data[y * width + x] == 0) {
continue; // 跳过背景
}
int top_label = 0;
int left_label = 0;
// 检查上方邻居
if (y > 0) {
top_label = labeled_img->data[(y - 1) * width + x];
}
// 检查左侧邻居
if (x > 0) {
left_label = labeled_img->data[y * width + (x - 1)];
}
if (top_label == 0 && left_label == 0) {
// 新区域
labeled_img->data[y * width + x] = next_label;
uf->parent[next_label] = next_label; // 初始化并查集
next_label++;
} else if (top_label != 0 && left_label == 0) {
// 只有上方邻居
labeled_img->data[y * width + x] = top_label;
} else if (top_label == 0 && left_label != 0) {
// 只有左侧邻居
labeled_img->data[y * width + x] = left_label;
} else {
// 上方和左侧邻居都有,建立等价关系
labeled_img->data[y * width + x] = top_label; // 临时使用上方标签
uf_union(uf, top_label, left_label);
}
}
}
// 2. 处理等价类,确定最终标签
// 创建一个映射表,将旧标签映射到新的最小标签
int *final_label_map = (int*)malloc(next_label * sizeof(int));
for (int i = 1; i < next_label; i++) {
final_label_map[i] = uf_find(uf, i);
}
// 3. 第二遍扫描,应用最终标签
int num_labels = 0;
for (int i = 1; i < next_label; i++) {
if (final_label_map[i] == i) { // 如果一个标签是它所在等价类的代表元
num_labels++;
}
}
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
int temp_label = labeled_img->data[y * width + x];
if (temp_label > 0) {
labeled_img->data[y * width + x] = final_label_map[temp_label];
}
}
}
// 4. 释放内存
uf_free(uf);
free(final_label_map);
return num_labels;
}
// 辅助函数:打印图像
void print_image(const Image* img) {
for (int y = 0; y < img->height; y++) {
for (int x = 0; x < img->width; x++) {
printf("%3d ", img->data[y * img->width + x]);
}
printf("\n");
}
}
// 辅助函数:释放图像内存
void free_image(Image* img) {
free(img->data);
}
int main() {
// 创建一个简单的 5x5 测试图像
// 0: 背景, 1: 目标
Image binary_img;
binary_img.width = 5;
binary_img.height = 5;
binary_img.data = (unsigned char*)malloc(5 * 5 * sizeof(unsigned char));
// 手动设置一个有3个连通区域的图像
unsigned char test_data[5][5] = {
{0, 1, 0, 0, 1},
{1, 1, 0, 0, 1},
{0, 0, 0, 1, 0},
{0, 0, 1, 1, 1},
{0, 0, 0, 0, 1}
};
for (int i = 0; i < 5; i++) {
for (int j = 0; j < 5; j++) {
binary_img.data[i * 5 + j] = test_data[i][j];
}
}
printf("原始二值图像:\n");
for (int i = 0; i < 5; i++) {
for (int j = 0; j < 5; j++) {
printf("%d ", binary_img.data[i * 5 + j]);
}
printf("\n");
}
printf("\n");
Image labeled_img;
int num_components = bwlabel_c(&binary_img, &labeled_img);
printf("标记后的图像 (共 %d 个连通区域):\n", num_components);
print_image(&labeled_img);
free_image(&binary_img);
free_image(&labeled_img);
return 0;
}
代码编译与运行
- 将上述代码保存为
bwlabel_example.c。 - 使用 GCC 进行编译:
gcc bwlabel_example.c -o bwlabel_example
- 运行程序:
./bwlabel_example
预期输出
原始二值图像:
0 1 0 0 1
1 1 0 0 1
0 0 0 1 0
0 0 1 1 1
0 0 0 0 1
标记后的图像 (共 3 个连通区域):
0 1 0 0 2
1 1 0 0 2
0 0 0 3 0
0 0 3 3 3
0 0 0 0 3
输出结果符合预期,图像中的三个白色连通区域被分别标记为 1, 2, 3。
扩展与注意事项
-
连通性定义:上面的代码实现了8-连通(即一个像素的8个邻居都算作连通),如果你需要实现4-连通(只有上下左右4个邻居是连通的),只需在第一遍扫描中只检查上方和左侧的4-连通邻居即可,将代码中的
top_label和left_label检查修改为只检查这4个方向。 -
性能优化:对于非常大的图像,并查集的路径压缩已经非常高效,如果追求极致性能,可以考虑使用更底层的数组来模拟并查集,或者使用哈希表来管理等价关系,但这会增加实现的复杂度。
-
数据类型:示例中使用
unsigned char存储标签,如果图像非常大,可能导致标签数量超过255,在这种情况下,应该使用int或short类型来存储图像数据和标签。 -
库依赖:这个实现是纯 C 的,不依赖任何外部库(除了标准库),非常适合嵌入式系统或对性能要求高的环境。
通过这个详细的讲解和代码示例,你应该已经掌握了如何在 C 语言中实现 bwlabel 的核心功能,这个算法是计算机视觉和图像处理领域的基础,理解它对学习更复杂的图像分析任务非常有帮助。
