Android OpenCV(四十二):图像分割(分水岭法)

849 阅读4分钟

这是我参与更文挑战的第 13 天,活动详情查看: 更文挑战

图像分割

图像分割就是把图像分成若干个特定的、具有独特性质的区域并提出感兴趣目标的技术和过程。它是由图像处理到图像分析的关键步骤。现有的图像分割方法主要分以下几类:基于阈值的分割方法、基于区域的分割方法、基于边缘的分割方法以及基于特定理论的分割方法等。从数学角度来看,图像分割是将数字图像划分成互不相交的区域的过程。图像分割的过程也是一个标记过程,即把属于同一区域的像素赋予相同的编号。

分水岭法

分水岭算法介绍,下面这位知乎博主已经讲得非常详细了,详情请自行查阅。

zhuanlan.zhihu.com/p/67741538

分水岭算法的整个过程:

  1. 把梯度图像中的所有像素按照灰度值进行分类,并设定一个测地距离阈值。
  2. 找到灰度值最小的像素点(默认标记为灰度值最低点),让threshold从最小值开始增长,这些点为起始点。
  3. 水平面在增长的过程中,会碰到周围的邻域像素,测量这些像素到起始点(灰度值最低点)的测地距离,如果小于设定阈值,则将这些像素淹没,否则在这些像素上设置大坝,这样就对这些邻域像素进行了分类。
  4. 随着水平面越来越高,会设置更多更高的大坝,直到灰度值的最大值,所有区域都在分水岭线上相遇,这些大坝就对整个图像像素的进行了分区。

过程动画

用上面的算法对图像进行分水岭运算,由于噪声点或其它因素的干扰,可能会得到密密麻麻的小区域,即图像被分得太细(over-segmented,过度分割),这因为图像中有非常多的局部极小值点,每个点都会自成一个小区域。

其中的解决方法:

  1. 对图像进行高斯平滑操作,抹除很多小的最小值,这些小分区就会合并。
  2. 不从最小值开始增长,可以将相对较高的灰度值像素作为起始点(需要用户手动标记),从标记处开始进行淹没,则很多小区域都会被合并为一个区域,这被称为基于图像标记(mark)的分水岭算法

API

public static void watershed(Mat image, Mat markers) 
  • 参数一:image,输入图像,必须是CV_8U三通道图像。
  • 参数二:markers,输入/输出32位单通道图像的标记结果。必须与image大小相同。 在将图像传递给第二个参数之前,必须使用大于0的整数索引在图像中粗略标记出所需分割的区域。 因此,每个标记区域表示一个或多个连接分量,像素值分别用1、2、3等来表示。 我们可以通过使用findContoursdrawContours从二值掩码中检索此类标记。 标记是图像处理过程中的“种子”。标记图像中所有没有被标记的像素值为0。在输出图像中,两个区域之间的分割线用-1表示。

操作

/**
 * 图像分割--分水岭法
 * author: yidong
 * 2020/11/9
 */
class WaterShedActivity : AppCompatActivity() {

    private val mBinding: ActivityWaterShedBinding by lazy {
        ActivityWaterShedBinding.inflate(
            layoutInflater
        )
    }
    private lateinit var mRgb: Mat
    private lateinit var mGray: Mat

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(mBinding.root)

        mRgb = Mat()
        mGray = Mat()
        val bgr = Utils.loadResource(this, R.drawable.contourpoly)
        Imgproc.cvtColor(bgr, mRgb, Imgproc.COLOR_BGR2RGB)
        Imgproc.cvtColor(bgr, mGray, Imgproc.COLOR_BGR2GRAY)

        mBinding.ivLena.showMat(mGray)

        GlobalScope.launch(Dispatchers.IO) {
            doWaterShed()
        }
    }

    private fun doWaterShed() {
        mBinding.progressBar.setVisible()
        val markers = Mat(
            mRgb.size(),
            CvType.CV_32S,
            Scalar.all(0.0)
        )
//        Imgproc.GaussianBlur(mGray, mGray, Size(13.0, 13.0), 4.0, 4.0
        val binary = Mat()
        Imgproc.threshold(
            mGray,
            binary,
            20.0,
            255.0,
            Imgproc.THRESH_BINARY and Imgproc.THRESH_OTSU
        )
        mBinding.ivResult.showMat(binary)
        val contours = mutableListOf<MatOfPoint>()
        val hierarchy = Mat()
        Imgproc.findContours(
            binary,
            contours,
            hierarchy,
            Imgproc.RETR_TREE,
            Imgproc.CHAIN_APPROX_SIMPLE
        )

        for (i in 0 until contours.size) {
            Imgproc.drawContours(
                markers,
                contours,
                i,
                Scalar.all(i + 1.toDouble()),
                -1,
                Imgproc.LINE_8,
                hierarchy,
                Int.MAX_VALUE
            )
        }

        Imgproc.watershed(mRgb, markers)

        val colors = mutableListOf<DoubleArray>()
        for (k in 0 until contours.size) {
            val r = Random.nextInt(0, 255)
            val g = Random.nextInt(0, 255)
            val b = Random.nextInt(0, 255)
            val scalar = doubleArrayOf(r.toDouble(), g.toDouble(), b.toDouble())
            colors.add(scalar)
        }

        val resultImg = Mat(mGray.size(), CvType.CV_8UC3)
        for (i in 0 until markers.rows()) {
            for (j in 0 until markers.cols()) {
                val index = markers.get(i, j)[0].toInt()

                if (index == -1) {                                    // -1:区域之间的分割线用-1表示
                    resultImg.put(i, j, 255.0, 255.0, 255.0)
                    Log.d(App.TAG, " i= $i, j=$j")
                } else if (index <= 0 || (index > contours.size)) {   //  <0 or >size:未标记区域
                    resultImg.put(i, j, 0.0, 0.0, 0.0)
                } else {                                              // 0,1,2,3... size-1: 标记区域
                    resultImg.put(
                        i,
                        j,
                        colors[index - 1][0],
                        colors[index - 1][1],
                        colors[index - 1][2]
                    )
                }
            }
        }
        mBinding.progressBar.setInvisible()
        mBinding.ivResult.showMat(resultImg)

        markers.release()
        resultImg.release()
    }

    override fun onDestroy() {
        mGray.release()
        mRgb.release()
        super.onDestroy()
    }
}

效果

彩色标记区域

分割线

源码

github.com/onlyloveyd/…