OpenCV学习笔记10-图像轮廓的相关知识及代码实现

文章目录

  • 1 什么是图像轮廓
  • 2 查找轮廓
  • 3 绘制轮廓
  • 4 轮廓的面积和周长
  • 5 多边形逼近与凸包
  • 6 外接矩形

1 什么是图像轮廓

图像轮廓是具有相同颜色或灰度的连续点的曲线. 轮廓在形状分析和物体的检测和识别中很有用。

轮廓的作用:

  • 用于图形分析
  • 物体的识别和检测

注意点:

  • 为了检测的准确性,需要先对图像进行二值化Canny操作
  • 在OpenCV中,找到轮廓就像从黑色背景中找到白色物体。因此请记住,要找到的对象应该是白色,背景应该是黑色。
  • 画轮廓时会修改输入的图像, 如果之后想继续使用原始图像,应该将原始图像储存到其他变量中。

2 查找轮廓

  • cv2.findContours(image, mode, method[, contours[, hierarchy[, offset]]])

    • image 寻找轮廓的图像(单通道图像矩阵,可以是灰度图,但更常用的是二值图像,一般是经过Canny、拉普拉斯等边缘检测算子处理过的二值图像

    • mode 查找轮廓的模式

      • RETR_EXTERNAL , 表示只检测最外围轮廓

        OpenCV学习笔记10-图像轮廓的相关知识及代码实现_第1张图片
      • RETR_LIST , 检测的轮廓不建立等级关系, 即检测所有轮廓

        图像里面的数字是索引,从右到左,从里到外

        OpenCV学习笔记10-图像轮廓的相关知识及代码实现_第2张图片
      • RETR_CCOMP , 每层最多两级, 从小到大, 从里到外,很少用.

        OpenCV学习笔记10-图像轮廓的相关知识及代码实现_第3张图片
      • RETR_TREE, 按照树型存储轮廓, 从外到里,从大到小, 从右到左最常用!.

        OpenCV学习笔记10-图像轮廓的相关知识及代码实现_第4张图片
    • method 轮廓近似方法也叫ApproximationMode

      • CHAIN_APPROX_NONE 保存所有轮廓上的点
      • CHAIN_APPROX_SIMPLE, 只保存角点, 比如四边形, 只保留四边形的4个角, 存储信息少, 比较常用
    • 返回值 :contours和hierarchy 即轮廓(旧版本是list形式,新版本是tuple形式)层级

    • contours:轮廓点。元组格式(不是ndarray),每一个元素为一个3维数组(其形状为(n,1,2),其中n表示轮廓点个数,2表示像素点坐标),表示一个轮廓。

    • hierarchy:轮廓间的层次关系,为三维数组,形状为(1,n,4),其中n表示轮廓总个数,4指的是用4个数表示各轮廓间的相互关系。第一个数表示同级轮廓的下一个轮廓编号,第二个数表示同级轮廓的上一个轮廓的编号,第三个数表示该轮廓下一级轮廓的编号,第四个数表示该轮廓的上一级轮廓的编号。

    • offset 轮廓点的偏移量,格式为tuple,如(-10,10)表示轮廓点沿X负方向偏移10个像素点,沿Y正方向偏移10个像素点。

import cv2
import numpy as np

# 该图像显示效果是黑白的, 但是实际上却是3个通道的彩色图像.
img = cv2.imread('./contours1.jpeg')

# 变成单通道的黑白图片
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

# 二值化, 注意有2个返回值, 阈值和结果
ret, binary = cv2.threshold(gray, 150, 255, cv2.THRESH_BINARY)

# 轮廓查找, 新版本返回两个结果, 轮廓和层级, 老版本返回3个参数, 图像, 轮廓和层级
contours, hierarchy = cv2.findContours(binary, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)

# 打印轮廓
print(type(contours)) # 查看轮廓的类型
print(contours)

cv2.waitKey(0)
cv2.destroyAllWindows()

3 绘制轮廓

  • cv2.drawContours(image, contours, contourIdx, color[, thickness[, lineType[, hierarchy[, maxLevel[, offset]]]]])
  • 直接绘制轮廓时会对原图进行绘制,建议拷贝一份新的图像
    • image 指明在哪幅图像上绘制轮廓(只有三通道的图像才显示轮廓)
    • contours 轮廓点,传入的是list类型
    • contourIdx 要绘制的轮廓的编号. -1 表示绘制所有轮廓 0代表绘制最外面的轮廓,1代表绘制第二个轮廓(从外到内数),2代表绘制第三个轮廓(从外到内数),以此类推
    • color 轮廓的颜色(BGR), 如 (0, 0, 255)表示红色
    • thickness线宽, -1 表示全部填充 1代表线的宽度,1、2、3…线宽依次变粗
    • lineType (可选参数)轮廓线型,包括cv2.LINE_4,cv2.LINE_8(默认),cv2.LINE_AA,分别表示4邻域线,8领域线,抗锯齿线(可以更好地显示曲线)
    • hierarchy (可选参数)层级结构,上述函数cv2.findContours()的第二个返回值,配合maxLevel参数使用
    • maxLevel (可选参数)等于0表示只绘制指定的轮廓,等于1表示绘制指定轮廓及其下一级子轮廓,等于2表示绘制指定轮廓及其所有子轮廓
    • offset (可选参数)轮廓点的偏移量
import cv2 
import numpy as np

# 该图形显示是黑白的,但是实际上是3个通道的彩色图像
img = cv2.imread('./contours1.jpeg')

# 先变成单通道的黑白图片
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

# 二值化, 返回两个东西, 一个阈值, 一个二值化之后的图.
thresh, binary = cv2.threshold(gray, 150, 255, cv2.THRESH_BINARY) 

# 查找轮廓, 新版本返回两个结果, 分别是轮廓和层级
contours, hierarchy = cv2.findContours(binary, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)

# 绘制轮廓会直接修改原图.
# 如果想保持原图不变, 建议copy一份
img_copy = img.copy()
cv2.drawContours(img_copy, contours, -1, (0, 0, 255), 1) # 绘制全部轮廓
# cv2.drawContours(img_copy, contours, 0, (0, 0, 255), 2) #  绘制最外面的轮廓

# cv2.imshow('img', img) # 原图
cv2.imshow('img_copy', img_copy) # 绘制了轮廓的图

cv2.waitKey(0)
cv2.destroyAllWindows()
OpenCV学习笔记10-图像轮廓的相关知识及代码实现_第5张图片

4 轮廓的面积和周长

轮廓面积是指每个轮廓中所有的像素点围成区域的面积,单位为像素。

轮廓面积是轮廓重要的统计特性之一,通过轮廓面积的大小可以进一步分析每个轮廓隐含的信息,例如通过轮廓面积区分物体大小识别不同的物体。

在查找到轮廓后, 可能会有很多细小的轮廓, 我们可以通过轮廓的面积进行过滤.

计算轮廓面积(不适用于具有自交点的轮廓,即重合),通常搭配findContours()函数使用:

  • 计算面积:cv2.contourArea(contour[, oriented])

    • contour 输入的图像轮廓点
    • oriented 有方向的区域标志,表示某一方向上的轮廓的面积值
      • 默认为False,表示返回不带方向的绝对值
      • True 依赖轮廓的方向(顺时针或逆时针),返回一个已标记区域的值
  • 计算周长:cv2.arcLength(curve, closed)

    • curve 即轮廓

    • closed 是否是闭合的轮廓

      • True 表示计算闭合的轮廓的周长

      • False表示计算不是闭合的轮廓的周长,此时的周长比闭合时少最后一条

        (例如,计算正方形的周长时,用True时计算四条边的和;用False时计算三条边的和)

计算面积代码:

import cv2 
import numpy as np

# 该图形显示是黑白的,但是实际上是3个通道的彩色图像
img = cv2.imread('./contours1.jpeg')

# 先变成单通道的黑白图片
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

# 二值化, 返回两个东西, 一个阈值, 一个二值化之后的图.
thresh, binary = cv2.threshold(gray, 150, 255, cv2.THRESH_BINARY) 

# 查找轮廓, 新版本返回两个结果, 分别是轮廓和层级
contours, hierarchy = cv2.findContours(binary, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)

# 绘制轮廓会直接修改原图.
# 如果想保持原图不变, 建议copy一份
img_copy = img.copy()
cv2.drawContours(img_copy, contours, 1, (255, 0, 0), 2)

# 计算轮廓面积
area = cv2.contourArea(contours[1])
print('area:', area)

cv2.waitKey(0)
cv2.destroyAllWindows()

计算周长代码:

import cv2 
import numpy as np

# 该图形显示是黑白的,但是实际上是3个通道的彩色图像
img = cv2.imread('./contours1.jpeg')

# 先变成单通道的黑白图片
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

# 二值化, 返回两个东西, 一个阈值, 一个二值化之后的图.
thresh, binary = cv2.threshold(gray, 150, 255, cv2.THRESH_BINARY) 

# 查找轮廓, 新版本返回两个结果, 分别是轮廓和层级
contours, hierarchy = cv2.findContours(binary, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)

# 绘制轮廓会直接修改原图.
# 如果想保持原图不变, 建议copy一份
img_copy = img.copy()
cv2.drawContours(img_copy, contours, 1, (255, 0, 0), 2)

# 计算轮廓周长
perimeter = cv2.arcLength(contours[1], closed=True)
print('perimeter:', perimeter)

cv2.waitKey(0)
cv2.destroyAllWindows()

完整代码:

import cv2
import numpy as np

# 该图像显示效果是黑白的, 但是实际上却是3个通道的彩色图像.
img = cv2.imread('./contours1.jpeg')

# 变成单通道的黑白图片
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

# 二值化, 注意有2个返回值, 阈值和结果
ret, binary = cv2.threshold(gray, 150, 255, cv2.THRESH_BINARY)


# 轮廓查找, 新版本返回两个结果, 轮廓和层级, 老版本返回3个参数, 图像, 轮廓和层级
contours, hierarchy = cv2.findContours(binary, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)

# 绘制轮廓, 注意, 绘制轮廓会改变原图
cv2.drawContours(img, contours, 1, (0, 0, 255), 2)

# 计算面积
area = cv2.contourArea(contours[1])
print('area: ', area)
cv2.imshow('img', img)

# 计算周长
perimeter = cv2.arcLength(contours[1], True)
print('perimeter:', perimeter)

cv2.waitKey(0)
cv2.destroyAllWindows()

5 多边形逼近与凸包

findContours后的轮廓信息contours可能过于复杂不平滑,可以用approxPolyDP函数对该多边形曲线做适当近似,这就是轮廓的多边形逼近.
原图:
OpenCV学习笔记10-图像轮廓的相关知识及代码实现_第6张图片
多边形逼近动态平滑:
OpenCV学习笔记10-图像轮廓的相关知识及代码实现_第7张图片

理解Douglas-Peucker算法:

1、该算法从轮廓中挑出两个最远的点,进行相连;

2、然后再原轮廓上寻找一个离线段距离最远的点,将该点加入逼近后的新轮廓,即连接着三个点形成的三角型作为轮廓;

3、选择三角形的任意一条边出发,进行步骤2,将距离最远点加入新轮廓,直至满足输出的精度要求。

approxPolyDP就是以多边形去逼近轮廓,把一个连续光滑曲线折线化,采用的是Douglas-Peucker算法(方法名中的DP)

DP算法原理比较简单,核心就是不断找多边形最远的点加入形成新的多边形,直到最大距离小于指定的精度。(以最少的储存信息量反映最接近原来轮廓的样子)

OpenCV学习笔记10-图像轮廓的相关知识及代码实现_第8张图片
我们可以看看动态形成过程:
OpenCV学习笔记10-图像轮廓的相关知识及代码实现_第9张图片

如图,假设阈值为T,先处理AB段,在整个轮廓上找到与AB边最远的距离d1,若d1T,则分别处理AC段和BC段,再分别判断d2、d3和T的大小关系,再做判断,以此类推指导找到合适的。

  • 多边形逼近:cv2.approxPolyDP(curve, epsilon, closed[, approxCurve])
    • curve 要近似逼近的轮廓,类型是mat(即ndarray)
    • epsilon 判断点到相对应的line segment的距离,即DP算法使用的阈值(距离大于此阈值则舍弃,小于此阈值则保留,epsilon越小,折线的形状越“接近”曲线。)经过测试,该阈值设为0.02*perimeter比较合适。
    • closed轮廓是否闭合
      • True 近似曲线是闭合的(它的第一个和最后一个顶点是连接的)
      • False 表示不闭合
    • approxCurve 输出轮廓,类型应该与输入轮廓的类型相匹配(ndarray)。与cv2.drawContours搭配使用时,要把输出的approx转换成list类型传入到contours,详情看25行代码
import cv2 
import numpy as np

# 该图形显示是黑白的,但是实际上是3个通道的彩色图像
img = cv2.imread('./hand.png')

# 先变成单通道的黑白图片
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

# 二值化, 返回两个东西, 一个阈值, 一个二值化之后的图.
thresh, binary = cv2.threshold(gray, 150, 255, cv2.THRESH_BINARY) 

# 查找轮廓, 新版本返回两个结果, 分别是轮廓和层级
contours, hierarchy = cv2.findContours(binary, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)

# 使用多边形逼近, 近似模拟手的轮廓
approx = cv2.approxPolyDP(contours[0], 20, closed=True)
# approx本质上就是一个轮廓数据,是ndarray
print(type(approx))
# print(approx)
# print('----------------------------------------------------------------')
# print(contours[0])
# 画出多边形逼近的轮廓

cv2.drawContours(img, [approx], 0, (0, 255, 0), 2)

cv2.imshow('img', img)

cv2.waitKey(0)
cv2.destroyAllWindows()
OpenCV学习笔记10-图像轮廓的相关知识及代码实现_第10张图片

逼近多边形是轮廓的高度近似,但是有时候,我们希望使用一个多边形的凸包来简化它。凸包跟逼近多边形很像,只不过它是物体最外层的凸多边形。凸包指的是完全包含原有轮廓,并且仅由轮廓上的点所构成的多边形。凸包的每一处都是凸的,即在凸包内连接任意两点的直线都在凸包的内部。在凸包内,任意连续三个点的内角小于180°。寻找图像的凸包,能够让我们做一些有意思的事情,比如手势识别等。(简单来说外部凸起来的点连起来就是凸包)

  • cv2.convexHull(points[, hull[, clockwise[, returnPoints]]])
    • points 即原始轮廓,不要传入逼近的轮廓
    • hull 输出凸包结果(ndarray)与cv2.drawContours搭配使用时,要把输出的hull转换成list类型传入到contours,详情看19行代码
    • colckwise 转动方向,默认为True,为顺时针绘制,反之为逆时针绘制
    • returnPoints 默认为True,返回凸包上点的坐标,如果设置为False,会返回与凸包点对应的轮廓上的点。
import cv2
import numpy as np

img =cv2.imread('./hand.png')

gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

# 二值化
thersh, binary = cv2.threshold(gray, 150, 255, cv2.THRESH_BINARY)

# 查找轮廓
contours, hierarchy = cv2.findContours(binary, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
print('contours:',type(contours))

# 计算凸包
hull = cv2.convexHull(contours[0])
print('hull:',type(hull))
# 画出凸包
cv2.drawContours(img, [hull], 0, (255, 0, 0), 2)

cv2.imshow('img', img)

cv2.waitKey(0)
cv2.destroyAllWindows()
OpenCV学习笔记10-图像轮廓的相关知识及代码实现_第11张图片

绘制轮廓+多边形逼近+凸包的结合:

import cv2
import numpy as np

img =cv2.imread('./hand.png')

gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

# 二值化
thersh, binary = cv2.threshold(gray, 150, 255, cv2.THRESH_BINARY)

# 查找轮廓
contours, hierarchy = cv2.findContours(binary, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
print(type(contours))
# 绘制轮廓
cv2.drawContours(img, contours, 0, (0, 0, 255), 2)

# 多边形逼近
approx = cv2.approxPolyDP(contours[0], 20, True)

# 画出多边形逼近的轮廓
cv2.drawContours(img, [approx], 0, (0, 255, 0), 2)

# 计算凸包
hull = cv2.convexHull(contours[0])
# print(type(hull))
# 画出凸包
cv2.drawContours(img, [hull], 0, (255, 0, 0), 2)

cv2.imshow('img', img)

cv2.waitKey(0)
cv2.destroyAllWindows()

OpenCV学习笔记10-图像轮廓的相关知识及代码实现_第12张图片

6 外接矩形

外接矩形分为最小外接矩形最大外接矩形.

下图中红色矩形是最小外接矩形, 绿色矩形为最大外接矩形.

OpenCV学习笔记10-图像轮廓的相关知识及代码实现_第13张图片

  • cv2.minAreaRect(points) 最小外接矩阵(存在旋转)
    • points 即为轮廓(contours)
    • 返回元组,我们用rect接收, 内容是一个旋转矩形(RotatedRect)的参数: 矩形的起始坐标(x,y), 矩形的宽度和高度, 矩形的旋转角度.

返回的rect,我们可以通过cv2.boxPoints(box[, points])来自动获得矩形的四个顶点坐标

  • box 是rect返回的参数
  • points 返回矩形的四个坐标(二维的ndarray)

我们获得坐标后(用box接收),可以通过cv2.drawCounter( )将最小外接矩形画出来,但是通过cv2.drawCounters( )绘制外接矩形时,要把box转化成list传入到counters参数里才能使用

坑:像素是通过整数的ndarray储存的,但是上面获得的box有可能是小数,我们要把小数位通过四舍五入重新算出来,再传入到counters,具体方法是box = np.round(box).astype(‘int64’)→看26行代码

  • cv2.boundingRect(points) 最大外接矩阵(方方正正的矩形,不存在旋转)
    • points 即为轮廓(contours)
    • 返回四个参数,起始坐标(x,y),宽高(w,h),用x,y,w,h接收,起始坐标在左下角

获得x,y,w,h后,我们可以通过cv2.rectangle( )将最大外接矩形画出来,具体看33行代码

import cv2
import numpy as np


img = cv2.imread('./hello.jpeg')

gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

ret, binary = cv2.threshold(gray, 150, 255, cv2.THRESH_BINARY)

contours, hierarchy = cv2.findContours(binary, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)

# 最外面的轮廓是整个图像, contours[1]表示图像里面的图形轮廓
# 注意返回的内容是一个旋转的矩形, 包含矩形的起始坐标, 宽高和选择角度
(x, y), (w, h), angle = cv2.minAreaRect(contours[1])

print(x, y)
print(w, h)
print(angle)
r = cv2.minAreaRect(contours[1])

# 快速把rotatedrect转化为轮廓数据
box = cv2.boxPoints(r)
print(box)
# 轮廓必须是整数, 不能是小数, 所以转化为整数
box = np.round(box).astype('int64')
print(box)
# 绘制最小外接矩形
cv2.drawContours(img, [box], 0, (255, 0, 0), 2)

# 返回矩形的x,y和w,h
x,y, w, h = cv2.boundingRect(contours[1])
cv2.rectangle(img, (x, y), (x + w, y + h), (255, 0, 0), 2)

cv2.imshow('img', img)
cv2.waitKey(0)
cv2.destroyAllWindows()
OpenCV学习笔记10-图像轮廓的相关知识及代码实现_第14张图片

附OpenCV目录:OpenCV总目录学习笔记

智科专业小白,写博文不容易,如果喜欢的话可以点个赞哦!请添加图片描述

你可能感兴趣的:(机器视觉,计算机视觉,python,opencv,人工智能,jupyter)