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必備學習資料

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章