图像轮廓是具有相同颜色或灰度的连续点的曲线. 轮廓在形状分析和物体的检测和识别中很有用。
轮廓的作用:
注意点:
cv2.findContours(image, mode, method[, contours[, hierarchy[, offset]]])
image 寻找轮廓的图像(单通道图像矩阵,可以是灰度图,但更常用的是二值图像,一般是经过Canny、拉普拉斯等边缘检测算子处理过的二值图像)
mode 查找轮廓的模式
method 轮廓近似方法也叫ApproximationMode
返回值 :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()
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()
轮廓面积是指每个轮廓中所有的像素点围成区域的面积,单位为像素。
轮廓面积是轮廓重要的统计特性之一,通过轮廓面积的大小可以进一步分析每个轮廓隐含的信息,例如通过轮廓面积区分物体大小识别不同的物体。
在查找到轮廓后, 可能会有很多细小的轮廓, 我们可以通过轮廓的面积进行过滤.
计算轮廓面积(不适用于具有自交点的轮廓,即重合),通常搭配findContours()函数使用:
计算面积:cv2.contourArea(contour[, oriented])
计算周长: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()
findContours后的轮廓信息contours可能过于复杂不平滑,可以用approxPolyDP函数对该多边形曲线做适当近似,这就是轮廓的多边形逼近.
原图:
多边形逼近动态平滑:
理解Douglas-Peucker算法:
1、该算法从轮廓中挑出两个最远的点,进行相连;
2、然后再原轮廓上寻找一个离线段距离最远的点,将该点加入逼近后的新轮廓,即连接着三个点形成的三角型作为轮廓;
3、选择三角形的任意一条边出发,进行步骤2,将距离最远点加入新轮廓,直至满足输出的精度要求。
approxPolyDP就是以多边形去逼近轮廓,把一个连续光滑曲线折线化,采用的是Douglas-Peucker算法(方法名中的DP)
DP算法原理比较简单,核心就是不断找多边形最远的点加入形成新的多边形,直到最大距离小于指定的精度。(以最少的储存信息量反映最接近原来轮廓的样子)
如图,假设阈值为T,先处理AB段,在整个轮廓上找到与AB边最远的距离d1,若d1
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()
逼近多边形是轮廓的高度近似,但是有时候,我们希望使用一个多边形的凸包来简化它。凸包跟逼近多边形很像,只不过它是物体最外层的凸多边形。凸包指的是完全包含原有轮廓,并且仅由轮廓上的点所构成的多边形。凸包的每一处都是凸的,即在凸包内连接任意两点的直线都在凸包的内部。在凸包内,任意连续三个点的内角小于180°。寻找图像的凸包,能够让我们做一些有意思的事情,比如手势识别等。(简单来说外部凸起来的点连起来就是凸包)
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()
绘制轮廓+多边形逼近+凸包的结合:
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()
外接矩形分为最小外接矩形和最大外接矩形.
下图中红色矩形是最小外接矩形, 绿色矩形为最大外接矩形.
返回的rect,我们可以通过cv2.boxPoints(box[, points])来自动获得矩形的四个顶点坐标
我们获得坐标后(用box接收),可以通过cv2.drawCounter( )将最小外接矩形画出来,但是通过cv2.drawCounters( )绘制外接矩形时,要把box转化成list传入到counters参数里才能使用
坑:像素是通过整数的ndarray储存的,但是上面获得的box有可能是小数,我们要把小数位通过四舍五入重新算出来,再传入到counters,具体方法是box = np.round(box).astype(‘int64’)→看26行代码
获得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目录:OpenCV总目录学习笔记
智科专业小白,写博文不容易,如果喜欢的话可以点个赞哦!