Android OpenCV(四十一):图像分割(漫水填充法)

图像分割

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

漫水填充法

漫水填充算法是根据像素灰度值之间的差值寻找相同区域实现分割。我们可以将图像的灰度值理解成像素点的高度,这样一张图像可以看成崎岖不平的地面或者山区,向地面上某一个低洼的地方倾倒一定量的水,水将会掩盖低于某个高度的区域。漫水填充法利用的就是这样的原理,其形式与注水相似,因此被称形象的称为“漫水”。

与向地面注水一致,漫水填充法也需要在图像选择一个注水像素,该像素被称为种子点,种子点按照一定规则不断向外扩散,从而形成具有相似特征的独立区域,进而实现图像分割。漫水填充分割法主要分为以下三个步骤:

  • 选择种子点(x,y);
  • 以种子点为中心,判断4邻域或者8邻域的像素值与种子点像素值的差值,将差值小于阈值的像素点添加进区域内。
  • 将新加入的像素点作为新的种子点,反复执行第二步,直到没有新的像素点被添加进该区域。

计算方式:

  • in case of a grayscale image and floating range(灰度图,浮动范围)
    src ( x ′ , y ′ ) − loDiff ≤ src ( x , y ) ≤ src ( x ′ , y ′ ) + upDiff \texttt{src} (x',y')- \texttt{loDiff} \leq \texttt{src} (x,y) \leq \texttt{src} (x',y')+ \texttt{upDiff} src(x,y)loDiffsrc(x,y)src(x,y)+upDiff

  • in case of a grayscale image and fixed range(灰度图,固定范围)
    src ( seedPoint . x , seedPoint . y ) − loDiff ≤ src ( x , y ) ≤ src ( seedPoint . x , seedPoint . y ) + upDiff \texttt{src} ( \texttt{seedPoint} .x, \texttt{seedPoint} .y)- \texttt{loDiff} \leq \texttt{src} (x,y) \leq \texttt{src} ( \texttt{seedPoint} .x, \texttt{seedPoint} .y)+ \texttt{upDiff} src(seedPoint.x,seedPoint.y)loDiffsrc(x,y)src(seedPoint.x,seedPoint.y)+upDiff

  • in case of a color image and floating range(彩色图,浮动范围)
    src ( x ′ , y ′ ) r − loDiff r ≤ src ( x , y ) r ≤ src ( x ′ , y ′ ) r + upDiff r , \texttt{src} (x',y')_r- \texttt{loDiff} _r \leq \texttt{src} (x,y)_r \leq \texttt{src} (x',y')_r+ \texttt{upDiff} _r, src(x,y)rloDiffrsrc(x,y)rsrc(x,y)r+upDiffr,

    src ( x ′ , y ′ ) g − loDiff g ≤ src ( x , y ) g ≤ src ( x ′ , y ′ ) g + upDiff g \texttt{src} (x',y')_g- \texttt{loDiff} _g \leq \texttt{src} (x,y)_g \leq \texttt{src} (x',y')_g+ \texttt{upDiff} _g src(x,y)gloDiffgsrc(x,y)gsrc(x,y)g+upDiffg

    and
    src ( x ′ , y ′ ) b − loDiff b ≤ src ( x , y ) b ≤ src ( x ′ , y ′ ) b + upDiff b \texttt{src} (x',y')_b- \texttt{loDiff} _b \leq \texttt{src} (x,y)_b \leq \texttt{src} (x',y')_b+ \texttt{upDiff} _b src(x,y)bloDiffbsrc(x,y)bsrc(x,y)b+upDiffb

  • in case of a color image and fixed range(彩色图,固定范围)
    src ( seedPoint . x , seedPoint . y ) r − loDiff r ≤ src ( x , y ) r ≤ src ( seedPoint . x , seedPoint . y ) r + upDiff r , \texttt{src} ( \texttt{seedPoint} .x, \texttt{seedPoint} .y)_r- \texttt{loDiff} _r \leq \texttt{src} (x,y)_r \leq \texttt{src} ( \texttt{seedPoint} .x, \texttt{seedPoint} .y)_r+ \texttt{upDiff} _r, src(seedPoint.x,seedPoint.y)rloDiffrsrc(x,y)rsrc(seedPoint.x,seedPoint.y)r+upDiffr,

    src ( seedPoint . x , seedPoint . y ) g − loDiff g ≤ src ( x , y ) g ≤ src ( seedPoint . x , seedPoint . y ) g + upDiff g \texttt{src} ( \texttt{seedPoint} .x, \texttt{seedPoint} .y)_g- \texttt{loDiff} _g \leq \texttt{src} (x,y)_g \leq \texttt{src} ( \texttt{seedPoint} .x, \texttt{seedPoint} .y)_g+ \texttt{upDiff} _g src(seedPoint.x,seedPoint.y)gloDiffgsrc(x,y)gsrc(seedPoint.x,seedPoint.y)g+upDiffg

    and
    src ( seedPoint . x , seedPoint . y ) b − loDiff b ≤ src ( x , y ) b ≤ src ( seedPoint . x , seedPoint . y ) b + upDiff b \texttt{src} ( \texttt{seedPoint} .x, \texttt{seedPoint} .y)_b- \texttt{loDiff} _b \leq \texttt{src} (x,y)_b \leq \texttt{src} ( \texttt{seedPoint} .x, \texttt{seedPoint} .y)_b+ \texttt{upDiff} _b src(seedPoint.x,seedPoint.y)bloDiffbsrc(x,y)bsrc(seedPoint.x,seedPoint.y)b+upDiffb

上述公式中,src(x′,y′)表示该区域内已知的相邻像素的值。简言之,当为浮动范围时,只有和已经属于某区域内的邻域相差足够小(满足公式范围),才能被选中进入该区域;当为固定范围时,只需要和种子像素相差足够小,就可以被选中进入该区域。

API

public static int floodFill(Mat image, Mat mask, Point seedPoint, Scalar newVal, Rect rect, Scalar loDiff, Scalar upDiff, int flags) 
  • 返回值:填充像素数目。

  • 参数一:image,输入和输出图像,图像可以为CV_8U或者CV_32F类型的单通道或者三通道图像。当最后一个参数设置为FLOODFILL_MASK_ONLY标志时,不改变原始图像。

  • 参数二:mask,操作掩码,为单通道8位图像,比输入图像宽2像素,高2像素。由于mask既是输入参数又是输出参数,必须初始化。漫水填充不会填充掩码中的非零区域。例如,边缘检测的输出可以用作操作掩码来防止漫水填充边缘。

  • 参数三:seedPoint,种子点。

  • 参数四:newVal,重新绘制的域像素的新值。

  • 参数五:rect,默认为 0,用于设置 floodFill 函数将要重绘的最小边界矩形区域,即若漫水填充区域 < rect,则不进行填充。

  • 参数六:loDiff,添加进种子点区域条件的下界差值。表示当前观察像素值与其邻域像素值或待加入的种子像素值之间的亮度或颜色的最大负差。

  • 参数七:upDiff,添加进种子点区域条件的上界差值。表示当前观察像素值与其邻域像素值或待加入的种子像素值之间的亮度或颜色的最大正差。

  • 参数八:flags,漫水填充法的操作标志位。该标志由3部分组成,第一部分表示邻域的种类,4邻域或者8邻域;第二部分表示掩码矩阵中被填充像素点的新像素值;第三部分是填充算法的规则标志。int 类型操作标识符,默认值为 4,一共 23 位。

    • 低八位(0~7):用于控制算法的连通性,可取 4(默认值)或 8。如果设为 4,表示填充算法只考虑当前像素水平或处置方向的相邻点,如果设为 8,除上述相邻点外,还会包含对角线方向的相邻点。

    • 中间八位(8~15):用于指定填充掩码图像的值的,如果中间八位的值为 0,则掩码会用 1 来填充。

    • 高八位(16~23):可以为 0,或者以下两种选择标识符的组合。

      FLOODFILL_FIXED_RANGE:如果设置为这个标识符,就会考虑当前像素与种子之间的差,否则就考虑当前像素与其邻域像素的差。

      FLOODFILL_MASK_ONLY,如果设置为这个标识符,函数不会去填充改变原始图像,而是去填充掩膜图像。也就是忽略第三个参数newVal

      // C++: enum FloodFillFlags
      public static final int
              FLOODFILL_FIXED_RANGE = 1 << 16,
              FLOODFILL_MASK_ONLY = 1 << 17;
      

      所以, flag 可以用 按位或,即‘|’ 连接起来。例如想用 4 邻域填充,并填充固定像素范围,填充掩码而不是填充原图,以及设置填充值为 250,那么输入的参数为

      4 or (250 shl 8) or Imgproc.FLOODFILL_FIXED_RANGE or Imgproc.FLOODFILL_MASK_ONLY 
      

操作

/**
 * 图像分割--漫水填充法
 * author: yidong
 * 2020/11/7
 */
class FloodFillActivity : AppCompatActivity() {
    private val mBinding by lazy { ActivityFloodFillBinding.inflate(layoutInflater) }
    private lateinit var mMenuDialog: BottomSheetDialog
    private lateinit var mMenuDialogBinding: LayoutFloodFillMenuBinding

    private var mConnectionType = 4
    private var mFloodFillFlag = 0
    private var mScalarNumber = 250 shl 8

    private lateinit var mRgb: Mat
    private var loDiff = 0.0
        set(value) {
            field = value
            doFloodFill()
        }
    private var upDiff = 0.0
        set(value) {
            field = value
            doFloodFill()
        }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(mBinding.root)
        val bgr = Utils.loadResource(this, R.drawable.wedding)
        mRgb = Mat()
        Imgproc.cvtColor(bgr, mRgb, Imgproc.COLOR_BGR2RGB)
        mBinding.ivLena.showMat(mRgb)
        mBinding.sbLow.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
            override fun onProgressChanged(p0: SeekBar?, p1: Int, p2: Boolean) {
                mBinding.tvLoDiff.text = p1.toString()
                loDiff = p1.toDouble()
            }

            override fun onStartTrackingTouch(p0: SeekBar?) {
            }

            override fun onStopTrackingTouch(p0: SeekBar?) {
            }

        })
        mBinding.sbUp.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
            override fun onProgressChanged(p0: SeekBar?, p1: Int, p2: Boolean) {
                mBinding.tvUpDiff.text = p1.toString()
                upDiff = p1.toDouble()
            }

            override fun onStartTrackingTouch(p0: SeekBar?) {
            }

            override fun onStopTrackingTouch(p0: SeekBar?) {
            }

        })
        mBinding.btFlag.setOnClickListener {
            showMenuDialog()
        }
        doFloodFill()
    }

    private fun doFloodFill() {
        val tmp = mRgb.clone()
        val maskers = Mat(mRgb.rows() + 2, mRgb.cols() + 2, CV_8UC1, Scalar.all(0.0))
        Imgproc.floodFill(
            tmp,
            maskers,
            Point(7.0, 7.0),
            Scalar(65.0, 105.0, 225.0),
            Rect(),
            Scalar.all(loDiff),
            Scalar.all(upDiff),
            mConnectionType or mFloodFillFlag or mScalarNumber
        )
        if (mFloodFillFlag and Imgproc.FLOODFILL_MASK_ONLY == Imgproc.FLOODFILL_MASK_ONLY) {
            mBinding.ivResult.showMat(maskers)
        } else {
            mBinding.ivResult.showMat(tmp)
        }

        tmp.release()
        maskers.release()
    }

    private fun showMenuDialog() {
        if (!this::mMenuDialog.isInitialized) {
            mMenuDialog = BottomSheetDialog(this)
            mMenuDialogBinding = LayoutFloodFillMenuBinding.inflate(layoutInflater)
            mMenuDialog.setContentView(mMenuDialogBinding.root)
            mMenuDialog.setOnDismissListener {
                mConnectionType =
                    if (mMenuDialogBinding.rgFirst.checkedRadioButtonId == R.id.rb_8) {
                        8
                    } else {
                        4
                    }
                mFloodFillFlag = if (mMenuDialogBinding.cbFixed.isChecked) {
                    mFloodFillFlag or Imgproc.FLOODFILL_FIXED_RANGE
                } else {
                    mFloodFillFlag and Imgproc.FLOODFILL_FIXED_RANGE.inv()
                }
                mFloodFillFlag = if (mMenuDialogBinding.cbMaskOnly.isChecked) {
                    mFloodFillFlag or Imgproc.FLOODFILL_MASK_ONLY
                } else {
                    mFloodFillFlag and Imgproc.FLOODFILL_MASK_ONLY.inv()
                }
                try {
                    mScalarNumber = mMenuDialogBinding.etScalar.text.toString().toInt(10) shl 8
                } catch (e: NumberFormatException) {
                    e.printStackTrace()
                }
                doFloodFill()
            }
        }
        mMenuDialog.show()
    }

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

结果

下图中,当FLAG设置为FLOODFILL_MASK_ONLY时,自动切换显示MASK掩码图像。

效果

源码

https://github.com/onlyloveyd/LearningAndroidOpenCV

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