目录:
轮廓就是连接所有连续点(沿着边界)的曲线,具有相同的颜色或灰度值。轮廓是形状分析、物体检测和识别的有用工具。为了提高提取轮廓的精确度,需要先通过阈值处理或canny边缘检测将图像转换为二值图像。
在 OpenCV 中,寻找轮廓就像从黑色背景中寻找白色物体,所以要找到的物体应该是白色的,背景应该是黑色的。
只罗列和轮廓相关的几个函数没啥意思,通过两个例子可以对其用法有更深入的理解。
1、查找轮廓
在二值图像中获取轮廓:
import cv2
im = cv2.imread('test.jpg')
imgray = cv2.cvtColor(im, cv2.COLOR_BGR2GRAY)
ret, thresh = cv2.threshold(imgray, 127, 255, 0)
contours, hierarchy = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
cv2.findContours() 函数中有三个参数:
thresh
:源图像cv2.RETR_TREE
:表示轮廓检索模式cv2.CHAIN_APPROX_SIMPLE
:表示轮廓近似方法返回值为获取到的轮廓 contours
和hierarchy
。contours
为包含图像中所有轮廓的python列表(三维数组),每个轮廓是包含边界所有坐标点(x, y)的Numpy数组。hierarchy
是一个三维数组,它储存了所有等高线(轮廓)的层级结构,详情可以查看 1 和 2。
轮廓是具有相同灰度值的形状的边界,即一个轮廓可以看做是一个等高线。它存储形状边界的(x, y)坐标。我们可以使用第三个参数来指定是否存储形状边界的所有坐标点。
第二个参数决定 hierarchy 采取什么样的格式输出。第三个参数可以指定两个值,如果是 cv2.CHAIN_APPROX_NONE,则存储形状边界的所有坐标点。但有时我们不需要所有的点,比如一个矩形的轮廓,我们只需要矩形的四个端点就可以了。这时我们就可以传入 cv2.CHAIN_APPROX_SIMPLE,它会移除所有冗余的点并压缩轮廓,从而节省内存。
比如下面这个例子,我们标记出矩形所有轮廓点,第一张图片是使用 cv2.CHAIN_APPROX_NONE 得到的结果,一共有734个点;第二张图片是使用 cv2.CHAIN_APPROX_SIMPLE 得到的结果,只有4个点。
2、绘制轮廓
可以使用 cv2.drawContours() 函数来绘制轮廓,只要有轮廓的边界点,就可以用来绘制任何形状的轮廓。
下面是绘制轮廓的三个例子:
# To draw all the contours in an image:
cv2.drawContours(img, contours, -1, (0,255,0), 3)
# To draw an individual contour, say 4th contour:
cv2.drawContours(img, contours, 3, (0,255,0), 3)
# But most of the time, below method will be useful:
cnt = contours[4]
cv2.drawContours(img, [cnt], 0, (0,255,0), 3)
cv2.drawContours() 函数中有三个参数,第一个参数是源图像;第二个参数是应该包含轮廓的Python列表;第三个参数是列表索引,用来选择要绘制的轮廓,为-1时表示绘制所有轮廓;第四个参数是轮廓颜色、第五个参数是轮廓线的宽度,为-1时表示填充。
注意:指定轮廓颜色的值要和图像通道数一致。
3、轮廓外接矩形
轮廓外接矩形分为正矩形和最小矩形。使用 cv2.boundingRect(cnt) 来获取轮廓的外接正矩形,它不考虑物体的旋转,所以该矩形的面积一般不会最小;使用 cv.minAreaRect(cnt) 可以获取轮廓的外接最小矩形。
两者区别如下图所示,绿线表示外接正矩形,红线表示外接最小矩形:
cv2.boundingRect(cnt) 的返回值包含四个值,矩形框左上角的坐标(x, y)、宽度w和高度h。
x,y,w,h = cv2.boundingRect(cnt)
cv.minAreaRect(cnt) 的返回值中还包含旋转信息,返回值信息为包括中心点坐标(x,y),宽高(w, h)和旋转角度。
然而我们绘制矩形需要矩形的四个顶点坐标,可以通过 cv.boxPoints()
来获取,如下代码所示:
rect = cv2.minAreaRect(cnt)
print(rect) # center(x, y), (width, height), angle of rotation
box = cv2.boxPoints(rect) # box.shape=(4, 2)
box = np.int0(box)
cv2.drawContours(img,[box],0,(0,0,255),2)
angle的范围为 (-90,-0]
,如上图中的角
4、轮廓面积
我们可以通过 cv2.contourArea(cnt) 来获取轮廓的面积,这里的面积表示该形状内包含的像素点数量。
5、轮廓周长
通过 cv2.arcLength(cnt,True) 来绘制轮廓周长或者曲线长度,第二个参数指定形状是为闭合轮廓(True)还是普通曲线。这里的周长/长度表示该形状边界上的像素点数量。
6、轮廓近似
我们可以将一个轮廓/曲线近似为另一个顶点数量较少的轮廓/曲线,使得它们之间的距离小于或等于指定的精度,通过 cv2.approxPolyDP(cnt, epsilon, True) 来实现。第二个参数用于轮廓近似的精度,表示原始轮廓与其近似轮廓的最大距离,值越小,近似轮廓越拟合原轮廓。第三个参数指定近似轮廓是否是闭合的。
比如下面这张图,其中物体的原始轮廓(红线所示)如第一张图所,第二张图中绿线就是epsilon为原始轮廓周长的10%时的近似轮廓,第三张图中绿线就是epsilon为原始轮廓周长的1%时的近似轮廓。
原图为:
现在我们只想获取图中圆形的内圆,不包含黑色边缘部分,就使用我们上面介绍过的函数实现。
先读取图像并将其转换为灰度图:
import cv2
import numpy as np
img = cv2.imread("shapes.jpg")
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
使用阈值处理将其转换为二值图:
ret, threshed = cv2.threshold(gray, 200, 255, cv2.THRESH_BINARY_INV|cv2.THRESH_OTSU)
然后就可以查找二值图中的轮廓:
contours, _ = cv2.findContours(threshed, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
二值图和查找到的轮廓如下所示:
我们得到了很多轮廓,但是只想要最中间的那个内圆轮廓,所以我们需要对这些轮廓进行筛选:
# 按轮廓面积将轮廓进行升序排列
cnts = sorted(contours, key=cv2.contourArea)
H, W = img.shape[:2]
for cnt in cnts:
# 获取轮廓外接矩形的坐标和长宽
x,y,w,h = cv2.boundingRect(cnt)
# 得到第一个满足条件的轮廓,就退出循环
if cv2.contourArea(cnt) > 100 and (0.8 < w/h < 1.2) and (W/4 < x + w//2 < W*3/4) and (H/4 < y + h//2 < H*3/4):
circle = cnt
break
仔细观察原图,可以发现我们想获取的那个圆形轮廓面积足够大,长宽比接近1,所以满足条件:
cv2.contourArea(cnt) > 100 and (0.8 < w/h < 1.2)
而且该圆形轮廓的中心点在图像的正中间部分,所以有:
(W/4 < x + w//2 < W*3/4) and (H/4 < y + h//2 < H*3/4)
当通过满足上述条件时,我们就可以获取到我们想要的内圆轮廓。
创建轮廓掩码并与原图进行逐位与运算:
mask = np.zeros(img.shape[:2], np.uint8)
cv2.drawContours(mask, [circle], -1, 255, -1)
dst = cv2.bitwise_and(img, img, mask=mask)
得到最终结果:
在这个例子中我们使用轮廓相关知识把这个人物给抠出来,类似于抠图。
还是先读取图像,并转换为灰度图,还要对其进行高斯模糊,事先过滤掉不同轮廓之间的细线连接。
第一个应用我们通过阈值处理得到二值图,这里使用 Canny 边缘检测,得到二值图,然后对其进行膨胀和腐蚀操作,方便稍后提取轮廓。关于形态学操作的内容可以参考这篇文章。
import cv2
import numpy as np
img = cv2.imread('Levi.jpg')
gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
blur = cv2.GaussianBlur(gray, (3, 3), 0)
edges = cv2.Canny(blur, 10, 200) # Edge detection
edges = cv2.dilate(edges, None) # 默认(3x3)
edges = cv2.erode(edges, None)
边缘图,膨胀图和腐蚀图如下所示:
接下来我们就可以获取处理后的图像中的轮廓。
先使用 cv2.findContours
获取所有轮廓,再根据轮廓面积进行降序排列,并获取到面积最大的轮廓,即图中人物的轮廓。
接下来通过 cv2.arcLength
得到轮廓周长,并将周长的0.1%作为近似轮廓的精度。然后再使用 cv2.approxPolyDP
得到近似轮廓。
# Find contours in edges(binary image)
contours, _ = cv2.findContours(edges, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
contours = sorted(contours, key=cv2.contourArea, reverse=True)
max_contour = contours[0]
epsilon = 0.001*cv2.arcLength(max_contour, True)
approx = cv2.approxPolyDP(max_contour, epsilon, True)
然后根据近似轮廓建立一个轮廓的掩码mask,在其上绘制出最大轮廓对应的填充多边形,用于下一步抠图。
对掩码进行高斯模糊用于平滑边缘,可以消除锯齿。
mask = np.zeros(img.shape[:2], np.uint8)
cv2.drawContours(mask, [approx], -1, 255, -1)
mask = cv2.GaussianBlur(mask, (5, 5), 0)
cv2.imshow('mask', mask)
dst = cv2.bitwise_and(img, img, mask=mask)
cv2.imshow('dst', dst)
最后一步就是将掩码和原图像进行求与运算,即得到最终结果。
掩码图和结果如下所示:
这里说个题外话,这里我们通过 edges = cv2.Canny(blur, 10, 200)
得到二值图,但是要手动设置两个阈值参数,不想手动设置的话可以使用如下函数来手动设置:
def auto_canny(image, sigma=0.33):
# compute the median of the single channel pixel intensities
v = np.median(image)
# construct two thresholds using the median
lower = int(max(0, (1.0 - sigma) * v))
upper = int(min(255, (1.0 + sigma) * v))
edged = cv2.Canny(image, lower, upper)
return edged
所以将 edges = cv2.Canny(blur, 10, 200)
替换为 edges = auto_canny(blur, sigma=0.33)
即可。
当需要框出目标的轮廓时,推荐使用 cv2.Canny()
边缘检测而非阈值处理生成二值图。理由:
边缘检测,框出物体的轮廓
几何形状识别与测量
Contour Features
remove the background of image:1,2
如果觉得有用,点个赞吧(ง •̀_•́)ง。