8方向连通域统计——two-pass算法(用于图像斑块数统计)

  • 问题描述
  • 连通域标记问题
  • Two-Pass算法
  • First Pass
  • Second Pass
  • Python实现
  • 例子


问题描述

现有一幅单通道灰度图像,图中像素共有0,1两种取值(取值代表类别代号,与算法无关)。现欲统计:每种取值的像素在图中构成的“斑块”的数目。斑块类似连通域的概念,这里我们定义像素数大于4的连通域才被算作一个斑块。

这个问题可以借助连通域标记算法解决。

连通域标记问题

连通域标记(Connected Component Labelling)问题, 主要有Two-Pass和Seed-Filling两种算法。本文介绍Two-Pass算法。

逐像素遍历图像,根据周围邻居像素的标签来给当前像素打标签。然后进行二次遍历,将同在一个斑块内的几种标签标记为一样(一个斑块内的几种标签,即等效标签,存储在并查集中)。

Two-Pass算法

只需要遍历两遍就可以完成标记。

First Pass

遍历每一个像素,如果像素p的值不为0,则考察它左上方的4个邻居的值。

  • 如果邻居的值均为0,则给他打上标签(自增的,label_counter)。
  • 如果邻居中存在不为0的值,取其中的最小值赋给p。随后利用并查集,把邻居的各种取值都建立联系(认定为同一个连通域)。

比如在下图中,像素p(红色块)的值不为0,考察其邻居的值,把2赋给p,同时把2和3标记为等价(在Union-Find set中,2和3有公共的根节点)。

opencv 连通域检测 seed filling python 连通域统计_连通域

Second Pass

遍历每一个非0的像素,利用建好的并查集的find方法,把每一个像素的取值设为对应根节点的值。

Python实现

import numpy as np


class UnionFind:
    def __init__(self, n):
        """长度为n的并查集"""
        self.uf = [-1] * (n + 1)  # 列表0位置空出
        self.sets_count = n  # 判断并查集里共有几个集合, 初始化默认互相独立

    def find(self, p):
        """查找p的根结点(祖先)"""
        r = p  # 初始p
        while self.uf[p] > 0:
            p = self.uf[p]
        while r != p:  # 路径压缩, 把搜索下来的结点祖先全指向根结点
            self.uf[r], r = p, self.uf[r]
        return p

    def union(self, p, q):
        """连通p,q 让q指向p"""
        proot = self.find(p)
        qroot = self.find(q)
        if proot == qroot:
            return
        elif self.uf[proot] > self.uf[qroot]:  # 负数比较, 左边规模更小
            self.uf[qroot] += self.uf[proot]
            self.uf[proot] = qroot
        else:
            self.uf[proot] += self.uf[qroot]
            self.uf[qroot] = proot
        self.sets_count -= 1                   # 连通后集合总数减一

    def is_connected(self, p, q):
        """判断pq是否已经连通"""
        return self.find(p) == self.find(q)  # 即判断两个结点是否是属于同一个祖先


def im_binary(data: np.ndarray, target_value: int):
    return np.where(data == target_value, 1, 0)


def im_padding(data: np.ndarray):
    return np.pad(data, ((1, 1), (1, 1)), 'constant')


def first_pass(data, uf_set):
    offsets = [[-1, -1], [0, -1], [-1, 1], [-1,  0]]
    label_counter = 2
    for y in range(1, data.shape[0]-1):
        for x in range(1, data.shape[1]-1):
            if data[y, x] == 0:
                continue
            neighbor = []
            for offset in offsets:
                if data[y + offset[0], x + offset[1]] != 0:
                    neighbor.append(data[y + offset[0], x + offset[1]])
            neighbor = np.unique(neighbor)
            if len(neighbor) == 0:
                data[y, x] = label_counter
                label_counter += 1
            elif len(neighbor) == 1:
                data[y, x] = neighbor[0]
            else:
                # 邻居内有多重label, 这种情况要把最小值赋给data[y, x], 同时建立值之间的联系.
                data[y, x] = neighbor[0]
                for n in neighbor:
                    uf_set.union(int(neighbor[0]), int(n))


def second_pass(data, uf_set):
    for y in range(data.shape[0]):
        for x in range(data.shape[1]):
            if data[y, x] != 0:
                data[y, x] = uf_set.find(int(data[y, x]))
  • im_binary() 方法把一个图像中等于target_value的值置为1,其余的置为0,实现二值化。
  • im_padding() 把图片周围补一圈0,这样才能用下面的算法。

例子

输入是一个植被指数(NDVI)单通道图像。希望把NDVI值大于0.1的部分设为1,小于的部分设为0,再计算1构成的连通域(图斑)的数量,并实现可视化。

def count_patch(data, get_img=False):
    # 统计某一种类别的图斑数. 返回各个图斑的像素数,和结果图.
    ufSet = UnionFind(1000000)
    first_pass(data, ufSet)
    second_pass(data, ufSet)

    count_dic = {}
    for y in range(1, data.shape[0] - 1):
        for x in range(1, data.shape[1] - 1):
            if data[y, x] in count_dic:
                count_dic[data[y, x]] += 1
            else:
                count_dic[data[y, x]] = 1

    count_dic.pop(0)

    if get_img:
        return list(count_dic.values()), data
    else:
        return list(count_dic.values())

结果图如下(同一连通域被标记成一种颜色):

opencv 连通域检测 seed filling python 连通域统计_python_02