Android OpenCV(三十五):轮廓发现与绘制

图像轮廓是一系列相连的点组成的曲线,代表物体的基本外形。轮廓与边缘的区别在于,轮廓是连续的,边缘并不全部连续。

轮廓发现的操作一般用于二值化图,所以通常会使用阈值分割或Canny边缘检测先得到二值图。

注意,轮廓发现是针对白色物体的,一定要保证物体是白色,而背景是黑色,不然很多人在寻找轮廓时会找到图片最外面的一个框。

轮廓层级

部分内容翻译自:https://docs.opencv.org/3.1.0/d9/d8b/tutorial_py_contours_hierarchy.html

通常我们使用findContours()函数寻找图像中的轮廓。某些情况下,某些形状在其他形状内,就像嵌套的数字一样。 在这种情况下,我们将外部形状称为父项,将内部形状称为子项。 这样,图像中的轮廓彼此之间就可以建立起关系。此关系的表示称为层次结构。如下图所示:

示例图片

图中,编号0,1,2,2a,3,3a,4,5。其中轮廓0,1,2是最外部轮廓。 它们在第0层,同时他们属于同一层级。接下来是轮廓-2a, 它是轮廓-2的子项(或换言之,轮廓-2是轮廓-2a的父项),它们属于第一层。以此类推,轮廓-3是轮廓-2的子项,它属于下一层。 最后,轮廓4,5是轮廓-3a的子项,它们位于最后一层。 从编号框方式来看,轮廓-4是轮廓-3a的第一个子项(其实也可以是轮廓-5)。

层次结构表示

每个轮廓都包含其层次结构信息,谁是其子级,谁是其父级等的信息。OpenCV用四个值的数组表示这种层次信息:[Next,Previous,First_Child,Parent]

  • Next:同层下一个轮廓索引

    图中看轮廓-0。 轮廓-1为其同一水平的下一个轮廓,所以Next = 1。类似地,对于轮廓-1,同一水平的下一个轮廓是轮廓-2, 所以Next = 2。

  • Previous:同层上一个轮廓索引

    图中看轮廓-1。轮廓-0为其同一水平的上一个轮廓,所以Previous = 0。类似地,对于轮廓-2,Previous = 1。 而对于轮廓-0,同一水平没有上一个轮廓,所以Previsou = -1。

  • First_Child:下层第一个子轮廓索引

    图中看轮廓-2。子项为轮廓-2a。因此First_Child=轮廓-2a索引值。针对如轮廓-3a这种存在多个子项的,我们只取第一个子项,也就是轮廓-4。针对没有子项的轮廓,我们取-1。

  • Parent:父轮廓

    图中看轮廓-4,其父项为轮廓-3a,则Parent = 轮廓3a索引值。针对如轮廓-0这类没有父项的轮廓,Parent = -1。

轮廓检索模式

RETR_EXTERNAL

此标记位只检索最高层级的轮廓。针对上图,应该只有3个轮廓,轮廓0,1,2。关系数组为:

>>> hierarchy
    array([[[ 1, -1, -1, -1],
            [ 2,  0, -1, -1],
            [-1,  1, -1, -1]]])

RETR_LIST

四个标志位中最简单的一个(从解释的角度来看)。它只是检索所有的轮廓,但不创建任何父子关系。所有轮廓都属于同一层次。所以,层次结构表示数组中,第三个元素和第四个元素都是-1。针对上图,如果采用此标志位寻找轮廓,得到的关系数组为:

>>> hierarchy
    array([[[ 1, -1, -1, -1],
            [ 2,  0, -1, -1],
            [ 3,  1, -1, -1],
            [ 4,  2, -1, -1],
            [ 5,  3, -1, -1],
            [ 6,  4, -1, -1],
            [ 7,  5, -1, -1],
            [-1,  6, -1, -1]]])

RETR_CCOMP

此标志位检索所有轮廓并将它们排列为2级层次结构。形状的外部轮廓(物体的边界)被置于层级-1中。对象内部的孔(如果有)的轮廓放置在层级-2中。如果其中还有任何对象,则其轮廓将再次仅放置在层级-1中,它在内部的孔放置至层级-2中,以此类推等等。下图很好的诠释了这个概念。红色字母给出轮廓编号,绿色字母给出层次结构顺序。

RETP_CCOMP

RETR_TREE

此标记位检索所有轮廓,并建立完成的层次关系。下图很好的诠释了这个概念。红色字母给出轮廓编号,绿色字母给出层次结构顺序。

RETR_TREE

API

public static void findContours(Mat image, List<MatOfPoint> contours, Mat hierarchy, int mode, int method, Point offset)
  • 参数一:image,输入图像,数据类型为CV_8U的单通道灰度图像或者二值化图像。如果mode等于RETR_CCOMPRETR_FLOODFILL,则输入图像类型也可以为CV_32SC1

  • 参数二:contours,检测到的轮廓。每个轮廓中存放着像素的坐标。

  • 参数三:hierarchy,轮廓结构关系描述向量。

  • 参数四:mode,轮廓检测模式标志位。

    // C++: enum RetrievalModes
    public static final int
            RETR_EXTERNAL = 0,  // 只检测最外层轮廓,对所有轮廓设置hierarchy[i][2]=hierarchy[i][3]=-1
            RETR_LIST = 1,      // 提取所有轮廓,并且放置在list中。检测的轮廓不建立等级关系。也就是所有轮廓属于同一层级
            RETR_CCOMP = 2,     // 提取所有轮廓,并且将其组织为双层结构。顶层为连通域的外围边界,次层为孔的内层边界。也就是所有轮廓分2个层级,不是外界就是最里层
            RETR_TREE = 3,      // 提取所有轮廓,并建立完整的层次关系
            RETR_FLOODFILL = 4; // 官网竟然没有解释
    
  • 参数五:method,轮廓逼近方法标志位。

    // C++: enum ContourApproximationModes
    public static final int
            CHAIN_APPROX_NONE = 1,     // 获取每个轮廓的每个像素点,相邻的两个点的像素位置相差为1,也就是要么水平相邻,要么垂直相邻
            CHAIN_APPROX_SIMPLE = 2,   // 压缩水平,垂直和对角线段,仅保留其端点。 例如,一个直立的矩形轮廓编码有4个点。
            CHAIN_APPROX_TC89_L1 = 3,  // 使用Teh-Chin链逼近算法的一种
            CHAIN_APPROX_TC89_KCOS = 4;// 使用Teh-Chin链逼近算法的一种
    
  • 参数六:offset,每个轮廓点移动的可选偏移量。如果从图像ROI中提取轮廓,然后在整个图像上下文中对其进行分析,这个参数才会被使用到。

public static void drawContours(Mat image, List<MatOfPoint> contours, int contourIdx, Scalar color, int thickness, int lineType, Mat hierarchy, int maxLevel, Point offset)
  • 参数一:image,待绘制轮廓的图像。

  • 参数二:contours,待绘制的轮廓集合。

  • 参数三:contourIdx,要绘制的轮廓在contours中的索引,若为负数,表示绘制全部轮廓。

  • 参数四:color,绘制轮廓的颜色。

  • 参数五:thickness,绘制轮廓的线条粗细。若为负数,那么绘制轮廓的内部。

  • 参数六:lineType,线条类型。

    // C++: enum LineTypes
    public static final int
            FILLED = -1,
            LINE_4 = 4,   // 4连通
            LINE_8 = 8,   // 8连通
            LINE_AA = 16; // 抗锯齿
    
  • 参数七:hierarchy,可选的结构关系。只有在绘制部分轮廓时,此参数才会使用到。

  • 参数八:maxLevel,绘制轮廓的最大等级。

  • 参数九:offset,轮廓检测模式标志位。

操作

/**
 * 发现轮廓
 * author: yidong
 * 2020/9/19
 */
class FindContoursActivity : AppCompatActivity() {
    private lateinit var mBinding: ActivityFindContoursBinding
    private lateinit var mSource: Mat
    private var level = 1
        set(value) {
            field = value
            find()
            mBinding.level.text = level.toString()
        }

    private var mFlag = Imgproc.RETR_TREE
        set(value) {
            field = value
            find()
        }

    private var ignoreLevel = true
        set(value) {
            field = value
            find()
        }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        mBinding = DataBindingUtil.setContentView(this, R.layout.activity_find_contours)
        mBinding.ignoreLevel.setOnCheckedChangeListener { _, isChecked ->
            ignoreLevel = isChecked
        }
        val bgr = Utils.loadResource(this, R.drawable.hierarchy)
        mSource = Mat()
        Imgproc.cvtColor(bgr, mSource, Imgproc.COLOR_BGR2RGB)
        mBinding.ivLena.showMat(mSource)
        title = "RETR_TREE"
    }

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

    override fun onOptionsItemSelected(item: MenuItem): Boolean {
        title = item.title
        mFlag = when (item.itemId) {
            R.id.retr_external -> Imgproc.RETR_EXTERNAL
            R.id.retr_list -> Imgproc.RETR_LIST
            R.id.retr_ccomp -> Imgproc.RETR_CCOMP
            else -> Imgproc.RETR_TREE
        }
        return true
    }

    private fun find() {
        val tmp = mSource.clone()
        val gray = Mat()
        Imgproc.cvtColor(mSource, gray, Imgproc.COLOR_BGR2GRAY)
        Imgproc.GaussianBlur(gray, gray, Size(13.0, 13.0), 4.0, 4.0)
        val binary = Mat()
        Imgproc.threshold(gray, binary, 170.0, 255.0, Imgproc.THRESH_BINARY and Imgproc.THRESH_OTSU)

        val contours = mutableListOf<MatOfPoint>()
        val hierarchy = Mat()
        Imgproc.findContours(
            binary,
            contours,
            hierarchy,
            mFlag,
            Imgproc.CHAIN_APPROX_SIMPLE
        )

        if (ignoreLevel) {
            Imgproc.drawContours(
                tmp,
                contours,
                -1,
                Scalar(255.0, 0.0, 0.0),
                2,
                Imgproc.LINE_AA
            )
        } else {
            Imgproc.drawContours(
                tmp,
                contours,
                -1,
                Scalar(255.0, 0.0, 0.0),
                2,
                Imgproc.LINE_AA,
                hierarchy,
                level
            )
        }
        mBinding.ivResult.showMat(tmp)
        Log.d(App.TAG, "hierarchy: ${hierarchy.dump()}")
        gray.release()
        binary.release()
        hierarchy.release()
        tmp.release()
    }


    fun increase(v: View) {
        level += 1
    }

    fun decrease(v: View) {
        level -= 1
    }

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

效果

轮廓发现与绘制

层次关系

示例中各种标记位对应输出的层次关系

RETR_EXTERNAL

 [1, -1, -1, -1, 
  2,  0, -1, -1, 
 -1,  1, -1, -1]

RETR_EXTERNAL

RETR_LIST

[1, -1, -1, -1, 
 2,  0, -1, -1, 
 3,  1, -1, -1, 
 4,  2, -1, -1, 
 5,  3, -1, -1, 
 6,  4, -1, -1, 
 7,  5, -1, -1, 
-1,  6, -1, -1]

RETR_LIST

RETR_CCOMP

[1, -1, -1, -1, 
 2,  0, -1, -1, 
 4,  1,  3, -1, 
-1, -1, -1,  2, 
 6,  2,  5, -1, 
-1, -1, -1,  4, 
 7,  4, -1, -1, 
-1,  6, -1, -1]

RETR_CCOMP

RETR_TREE

[6, -1,  1, -1, 
-1, -1,  2,  0, 
-1, -1,  3,  1, 
-1, -1,  4,  2, 
 5, -1, -1,  3, 
-1,  4, -1,  3, 
 7,  0, -1, -1, 
-1,  6, -1, -1]

RETR_TREE

源码

https://github.com/onlyloveyd/LearningAndroidOpenCV

扫码关注,持续更新

扫码关注并回复下列关键字,获取对应学习资料。

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