C语言中bwlabel函数如何实现连通区域标记?

99ANYc3cd6
预计阅读时长 27 分钟
位置: 首页 C语言 正文

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

bwlabel c语言
(图片来源网络,侵删)

在 C 语言中,没有内置的 bwlabel 函数,但我们可以通过经典的连通域标记算法来手动实现它,最常用和最经典的算法是两遍扫描算法,也称为连通分量标记算法


算法核心思想:两遍扫描

这个算法之所以高效,是因为它只需要对图像进行两次完整的扫描。

第一遍扫描:临时标记和等价类建立

  1. 从左到右,从上到下扫描图像的每一个像素。

  2. 如果当前像素是目标像素(白色像素,值为 1),就检查它上方左侧的邻居像素。

    bwlabel c语言
    (图片来源网络,侵删)
  3. 上方和左侧邻居都不是目标像素

    • 这意味着我们遇到了一个新的、独立的连通区域。
    • 为这个新区域分配一个新的、唯一的标签(从 2 开始,因为 0 通常表示背景)。
    • 将当前像素的标签设置为新标签。
    • (新标签, 新标签) 添加到一个等价关系表(通常是一个数组或并查集结构)中,这表示该标签目前只等价于它自己。
  4. 只有一个邻居是目标像素

    将当前像素的标签设置为该邻居的标签。

  5. 上方和左侧邻居都是目标像素

    bwlabel c语言
    (图片来源网络,侵删)
    • 这是最关键的一步,这两个邻居可能属于同一个连通区域,也可能属于不同但相邻的区域。
    • 将当前像素的标签设置为上方邻居的标签
    • 上方邻居的标签左侧邻居的标签记录为等价标签,如果上方是 2,左侧是 3,我们就知道标签 2 和 3 实际上代表的是同一个区域,我们将这个 (2, 3) 的对记录下来。

经过第一遍扫描后,图像中的每个像素都有一个临时标签,但这些标签可能不是最终的,因为等价标签(如 2 和 3)需要被合并。

第二遍扫描:标签统一

  1. 处理等价类:在开始第二遍扫描之前,我们需要先处理第一遍扫描中收集到的所有等价关系,目标是找到一个最小标签作为每个等价类的“代表元”。

    • 我们有等价关系对 (2,3), (3,4), (5,6),我们可以将它们合并成两个等价类:{2, 3, 4} 和 {5, 6}。
    • 为每个等价类选择一个最小的标签作为最终标签,{2, 3, 4} -> 2,{5, 6} -> 5。
    • 这个过程可以通过并查集 数据结构非常高效地实现。
  2. 从左到右,从上到下再次扫描图像。

  3. 对于每个像素的临时标签,查找它所属等价类的代表元

  4. 将当前像素的标签替换为代表元。

经过这两遍扫描,所有属于同一个连通区域的像素都会拥有相同的、最小的标签。


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;
}

代码编译与运行

  1. 将上述代码保存为 bwlabel_example.c
  2. 使用 GCC 进行编译:
    gcc bwlabel_example.c -o bwlabel_example
  3. 运行程序:
    ./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。


扩展与注意事项

  1. 连通性定义:上面的代码实现了8-连通(即一个像素的8个邻居都算作连通),如果你需要实现4-连通(只有上下左右4个邻居是连通的),只需在第一遍扫描中只检查上方和左侧的4-连通邻居即可,将代码中的 top_labelleft_label 检查修改为只检查这4个方向。

  2. 性能优化:对于非常大的图像,并查集的路径压缩已经非常高效,如果追求极致性能,可以考虑使用更底层的数组来模拟并查集,或者使用哈希表来管理等价关系,但这会增加实现的复杂度。

  3. 数据类型:示例中使用 unsigned char 存储标签,如果图像非常大,可能导致标签数量超过255,在这种情况下,应该使用 intshort 类型来存储图像数据和标签。

  4. 库依赖:这个实现是纯 C 的,不依赖任何外部库(除了标准库),非常适合嵌入式系统或对性能要求高的环境。

通过这个详细的讲解和代码示例,你应该已经掌握了如何在 C 语言中实现 bwlabel 的核心功能,这个算法是计算机视觉和图像处理领域的基础,理解它对学习更复杂的图像分析任务非常有帮助。

-- 展开阅读全文 --
头像
dede文章标题如何加粗?
« 上一篇 04-14
dede会员伪静态规则如何正确配置?
下一篇 » 04-14

相关文章

取消
微信二维码
支付宝二维码

目录[+]