Android OpenCV(二十七):​图像连通域

图像连通域

连通域

图像的连通域是指图像中具有相同像素值并且位置相邻的像素组成的区域,

连通域分析是指在图像中寻找出彼此互相独立的连通域并将其标记出来。

提取图像中不同的连通域是图像处理中较为常用的方法,例如在车牌识别、文字识别、目标检测等领域对感兴趣区域分割与识别。一般情况下,一个连通域内只包含一个像素值,因此为了防止像素值波动对提取不同连通域的影响,连通域分析常处理的是二值化后的图像

邻域

邻域,与指定元素相邻的像素集合。常用的有4邻域和8邻域。

如果像素点A与B邻接,我们称A与B连通,于是我们不加证明的有如下的结论:如果A与B连通,B与C连通,则A与C连通。

在视觉上看来,彼此连通的点形成了一个区域,而不连通的点形成了不同的区域。这样的一个所有的点彼此连通点构成的集合,我们称为一个连通区域。 下面这副图中,如果考虑4邻接,则有3个连通区域;如果考虑8邻接,则有2个连通区域。(注:图像是被放大的效果,图像正方形实际只有4个像素)。

示例

连通区域分析方法

两遍扫描法(Two-Pass)

两遍扫描法会遍历两次图像,第一次遍历图像时会给每一个非0像素赋予一个数字标签,当某个像素的上方和左侧邻域内的像素已经有数字标签时,取两者中的最小值作为当前像素的标签,否则赋予当前像素一个新的数字标签。第一次遍历图像的时候同一个连通域可能会被赋予一个或者多个不同的标签,因此第二次遍历需要将这些属于同一个连通域的不同标签合并,最后实现同一个邻域内的所有像素具有相同的标签。

  1. 第一次扫描:

    • 访问当前像素 B(x,y) ,如果 B(x,y) == 1:
      • 如果 B(x,y) 的领域中标签值都为0,则赋予 B(x,y) 一个新的 label :
        label += 1, B(x,y) = label;
      • 如果B(x,y)的邻域中有像素值 > 1的像素Neighbors:将Neighbors中的最小值赋予给 B(x,y) :B(x,y) = min{Neighbors}
    • 记录Neighbors中各个值(label)之间的相等关系,即这些值(label)同属同一个连通区域;
  2. 第二次扫描:

    • 访问当前像素 B(x,y) ,如果 B(x,y) > 1:找到与 label = B(x,y) 同属相等关系的一个最小 label 值,赋予给 B(x,y)
    • 完成扫描后,图像中具有相同 label 值的像素就组成了同一个连通区域。

第一遍过程:

第一遍过程

第一遍结束后,我们可以得到一个[1,2,3]的集合,来证明集合内的标签属于同一连通区域。

第二遍过程,则是将同一连通区域内的标签合并,使每个连通域只有一个标签。
在这里插入图片描述

种子填充法(Seed Filling

种子填充法源于计算机图像学,常用于对某些图形进行填充,它基于区域生长算法。该方法首先将所有非0像素放到一个集合中,之后在集合中随机选出一个像素作为种子像素,根据邻域关系不断扩充种子像素所在的连通域,并在集合中删除掉扩充出的像素,直到种子像素所在的连通域无法扩充,之后再从集合中随机选取一个像素作为新的种子像素,重复上述过程直到集合中没有像素。

  1. 扫描图像,直到当前像素点B(x,y)==1:

    • 将B(x,y)作为种子(像素位置),并赋予其一个label,然后将该种子相邻的所有前景像素都压入栈中;

    • 弹出栈顶像素,赋予其相同的label,然后再将与该栈顶像素相邻的所有前景像素都压入栈中;

    • 重复上一步操作,直到栈为空;

      (此时,便找到了图像B中的一个连通区域,该区域内的像素值被标记为label)

  2. 重复第(1)步,直到扫描结束;

基本操作流程:

种子填充法

API

connectedComponents

public static int connectedComponents(Mat image, Mat labels, int connectivity, int ltype)

参数一:image,待标记的单通道图像,数据类型必须为CV_8U。

参数二:labels,标记连通域后的输出图像,与输入图像具有相同的尺寸。

参数三:connectivity,标记连通域时使用的邻域种类,4表示4-邻域,8表示8-邻域。

参数四:ltype,输出图像的数据类型,目前支持CV_32S和CV_16U两种数据类型。

public static int connectedComponents(Mat image, Mat labels)
public static int connectedComponents(Mat image, Mat labels, int connectivity)

以上为两个简易函数,省略的参数 :connectivity = 8ltype = CV_32S

connectedComponentsWithAlgorithm

public static int connectedComponentsWithAlgorithm(Mat image, Mat labels, int connectivity, int ltype, int ccltype)

参数一:image,待标记的单通道图像,数据类型必须为CV_8U。

参数二:labels,标记连通域后的输出图像,与输入图像具有相同的尺寸。

参数三:connectivity,标记连通域时使用的邻域种类,4表示4-邻域,8表示8-邻域。

参数四:ltype,输出图像的数据类型,目前支持CV_32S和CV_16U两种数据类型。

参数五:ccltype,标记连通域时使用的算法类型标志。

//! connected components algorithm
enum ConnectedComponentsAlgorithmsTypes {
    CCL_WU      = 0,  //!< SAUF algorithm for 8-way connectivity, SAUF algorithm for 4-way connectivity
    CCL_DEFAULT = -1, //!< BBDT algorithm for 8-way connectivity, SAUF algorithm for 4-way connectivity
    CCL_GRANA   = 1   //!< BBDT algorithm for 8-way connectivity, SAUF algorithm for 4-way connectivity
};

connectedComponentsWithStats

该函数能够在图像中不同连通域标记标签的同时统计每个连通域的中心位置、矩形区域大小。

public static int connectedComponentsWithStats(Mat image, Mat labels, Mat stats, Mat centroids, int connectivity, int ltype)

参数一:image,待标记的单通道图像,数据类型必须为CV_8U。

参数二:labels,标记连通域后的输出图像,与输入图像具有相同的尺寸。

参数三:stats,每个标签(包括背景标签)的统计信息输出。通过stats(label, COLUMN)来访问对应的信息。数据类型为 CV_32S。COLUMN的类型如下:

// C++: enum ConnectedComponentsTypes
public static final int
        CC_STAT_LEFT = 0,
        CC_STAT_TOP = 1,
        CC_STAT_WIDTH = 2,
        CC_STAT_HEIGHT = 3,
        CC_STAT_AREA = 4,
        CC_STAT_MAX = 5;

参数四:centroids,每个标签(包括背景标签)的中心点。X轴坐标与Y轴左边分别用centroids(label,0)和centroids(label,1)访问。数据类型为CV_64F

参数五:connectivity,标记连通域时使用的邻域种类,4表示4-邻域,8表示8-邻域。

参数六:ltype,输出图像的数据类型,目前支持CV_32S和CV_16U两种数据类型。

参数七:ccltype,标记连通域时使用的算法类型标志。

connectedComponentsWithStatsWithAlgorithm

public static int connectedComponentsWithStatsWithAlgorithm(Mat image, Mat labels, Mat stats, Mat centroids, int connectivity, int ltype, int ccltype)

参数基本同上,只是多出了connectedComponentsWithAlgorithm方法中最后一个算法类型的参数。

操作

/**
 * 连通域分析
 * author: yidong
 * 2020/6/7
 */
class ConnectedComponentsActivity : AppCompatActivity() {
    private lateinit var mBinding: ActivityConnectedComponentsBinding
    private lateinit var mBinary: Mat

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        mBinding = DataBindingUtil.setContentView(this, R.layout.activity_connected_components)

        val bgr = Utils.loadResource(this, R.drawable.number)
        val gray = Mat()
        Imgproc.cvtColor(bgr, gray, Imgproc.COLOR_BGR2GRAY)
        mBinary = Mat()
        Imgproc.threshold(gray, mBinary, 50.0, 255.0, Imgproc.THRESH_BINARY)
        showMat(mBinding.ivLena, mBinary)

        bgr.release()
        gray.release()
    }

    private fun showLoading() {
        mBinding.progressBar.show()
    }

    private fun dismissLoading() {
        mBinding.progressBar.hide()
    }

    override fun onCreateOptionsMenu(menu: Menu?): Boolean {
        menuInflater.inflate(R.menu.menu_connected_componenets, menu)
        return true
    }

    override fun onOptionsItemSelected(item: MenuItem): Boolean {
        when (item.itemId) {
            R.id.connected_components -> {
                connectedComponent()
            }
            R.id.connected_components_with_algorithm_4_wu -> {
                connectedComponentsWithAlgorithm(4, Imgproc.CCL_WU)
            }
            R.id.connected_components_with_algorithm_8_wu -> {
                connectedComponentsWithAlgorithm(8, Imgproc.CCL_WU)
            }
            R.id.connected_components_with_algorithm_4_grana -> {
                connectedComponentsWithAlgorithm(4, Imgproc.CCL_GRANA)
            }
            R.id.connected_components_with_algorithm_8_grana -> {
                connectedComponentsWithAlgorithm(8, Imgproc.CCL_GRANA)
            }
            R.id.connected_components_with_stats_with_algorithm -> {
                connectedComponentsWithStatsWithAlgorithm()
            }
        }
        return true
    }

    private fun showMat(view: ImageView, source: Mat) {
        val bitmap = Bitmap.createBitmap(source.width(), source.height(), Bitmap.Config.ARGB_8888)
        Utils.matToBitmap(source, bitmap)
        view.setImageBitmap(bitmap)
    }

    private fun connectedComponent() {
        val labels = Mat()
        val count = Imgproc.connectedComponents(mBinary, labels)
        labels.convertTo(labels, CvType.CV_8U)
        showLoading()
        GlobalScope.launch(Dispatchers.IO) {
            drawConnectedComponent(count, labels)
        }
    }

    private fun connectedComponentsWithAlgorithm(connectivity: Int, algorithm: Int) {
        val labels = Mat()
        val count = Imgproc.connectedComponentsWithAlgorithm(
            mBinary,
            labels,
            connectivity,
            CvType.CV_32S,
            algorithm
        )
        labels.convertTo(labels, CvType.CV_8U)
        showLoading()
        GlobalScope.launch(Dispatchers.IO) {
            drawConnectedComponent(count, labels)
        }
    }

    private fun connectedComponentsWithStatsWithAlgorithm() {
        val labels = Mat()
        val stats = Mat()
        val centroids = Mat()
        val labelCount = Imgproc.connectedComponentsWithStatsWithAlgorithm(
            mBinary,
            labels,
            stats,
            centroids,
            8,
            CvType.CV_32S,
            Imgproc.CCL_GRANA
        )
        labels.convertTo(labels, CvType.CV_8U)
        val statList = mutableListOf<Stat>()
        for (count in 0 until labelCount) {
            val stat = Stat(
                centroids.get(count, 0)?.get(0) ?: 0.0,
                centroids.get(count, 1)?.get(0) ?: 0.0,
                stats.get(count, 0)?.get(0)?.toInt() ?: 0,
                stats.get(count, 1)?.get(0)?.toInt() ?: 0,
                stats.get(count, 2)?.get(0)?.toInt() ?: 0,
                stats.get(count, 3)?.get(0)?.toInt() ?: 0,
                count
            )
            statList.add(stat)
        }
        showLoading()
        GlobalScope.launch(Dispatchers.IO) {
            drawConnectedComponentWithStats(labelCount, labels, statList)
        }
    }

    private fun drawConnectedComponent(count: Int, labels: Mat) {
        val result = Mat.zeros(labels.rows(), labels.cols(), CvType.CV_8UC3)
        val color = arrayListOf<Scalar>()
        for (index in 0..count) {
            val scalar = Scalar(
                (Math.random() * 255) + 1,
                (Math.random() * 255) + 1,
                (Math.random() * 255) + 1
            )
            color.add(scalar)
        }
        for (row in 0..labels.rows()) {
            for (col in 0..labels.cols()) {
                val label = labels.get(row, col)?.get(0)?.toInt() ?: 0
                if (label == 0) {
                    continue
                } else {
                    result.put(
                        row,
                        col,
                        color[label].`val`[0],
                        color[label].`val`[1],
                        color[label].`val`[2]
                    )
                }
            }
        }
        GlobalScope.launch(Dispatchers.Main) {
            dismissLoading()
            showMat(mBinding.ivResult, result)
            result.release()
        }
        labels.release()
    }

    private fun drawConnectedComponentWithStats(
        count: Int,
        labels: Mat,
        statList: MutableList<Stat>
    ) {
        val result = Mat.zeros(labels.rows(), labels.cols(), CvType.CV_8UC3)
        val color = arrayListOf<Scalar>()
        for (index in 0..count) {
            val scalar = Scalar(
                (Math.random() * 255) + 1,
                (Math.random() * 255) + 1,
                (Math.random() * 255) + 1
            )
            color.add(scalar)
        }
        for (row in 0..labels.rows()) {
            for (col in 0..labels.cols()) {
                val label = labels.get(row, col)?.get(0)?.toInt() ?: 0
                if (label == 0) {
                    continue
                } else {
                    result.put(
                        row,
                        col,
                        color[label].`val`[0],
                        color[label].`val`[1],
                        color[label].`val`[2]
                    )
                }
            }
        }

        for (index in 0 until statList.size) {
            val stat = statList[index]
            val rect = Rect(stat.left, stat.top, stat.width, stat.height)
            Imgproc.rectangle(result, rect, color[stat.label], 10)
        }
        GlobalScope.launch(Dispatchers.Main) {
            dismissLoading()
            showMat(mBinding.ivResult, result)
            result.release()
        }
        labels.release()
    }
}

效果

简单标记出连通域

根据连通域数据信息,标记出连通域以及矩形边框

源码

https://github.com/onlyloveyd/LearningAndroidOpenCV

扫码关注,持续更新

回复【计算机视觉】获取计算机视觉相关必备学习资料
回复【Android】获取Android,Kotlin必备学习资料

onlyloveyd CSDN认证博客专家 Android Kotlin OpenCV
个人公众号【OpenCV or Android】,热爱Android、Kotlin、Flutter和OpenCV。毕业于华中科技大学计算机专业,曾就职于华为武汉研究所。目前在三线小城市生活,专注技术与研发。
©️2020 CSDN 皮肤主题: 数字20 设计师:CSDN官方博客 返回首页