一个很实在的栗子:假设我们在图像分割为一幅2值图像,图像里不能保证每个像素点的正确分类,但是总体分割效果还算ok。分割图像里存在噪声点,例如图一,图中最大的空白区域为我们想要得到的区域,那些比较小的奇形怪状的小白区域可以理解为图像分割中的一些分割错误的噪声点,我们想把我们的最大区域单独从图像里扣出来,如图二。
图一 图二
1寻找轮廓
2选出最大面积的轮廓
3图像透视变换
4目标区域的裁剪
opencv中使用findContours()函数进行二值图像的轮廓寻找,有一点需要注意的是不同版本的opencv仿佛findContours()函数返回值不太一样,如果你使用时发现报返回值数量错误时可以将返回值个数由3改为2,或者由2改为3,这里我们主要使用返回值中的contours,它记录了图像中的每个轮廓的信息。
contours, hierarchy = cv2.findContours(img, cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE)
cv2.RETR_TREE:提取轮廓后,输出轮廓信息的组织形式,除了cv2.RETR_TREE还有以下几种选项:
cv2.RETR_EXTERNAL:输出轮廓中只有外侧轮廓信息;
cv2.RETR_LIST:以列表形式输出轮廓信息,各轮廓之间无等级关系;
cv2.RETR_CCOMP:输出两层轮廓信息,即内外两个边界(下面将会说到contours的数据结构);
cv2.RETR_TREE:以树形结构输出轮廓信息。
cv2.CHAIN_APPROX_SIMPLE:指定轮廓的近似办法,有以下选项:
cv2.CHAIN_APPROX_NONE:存储轮廓所有点的信息,相邻两个轮廓点在图象上也是相邻的;
cv2.CHAIN_APPROX_SIMPLE:压缩水平方向,垂直方向,对角线方向的元素,只保留该方向的终点坐标;
cv2.CHAIN_APPROX_TC89_L1:使用teh-Chinl chain 近似算法保存轮廓信息。
返回值:
contours:list结构,列表中每个元素代表一个边沿信息。每个元素是(x,1,2)的三维向量,x表示该条边沿里共有多少个像素点,第三维的那个“2”表示每个点的横、纵坐标;
注意:如果输入选择cv2.CHAIN_APPROX_SIMPLE,则contours中一个list元素所包含的x点之间应该用直线连接起来,这个可以用cv2.drawContours()函数观察一下效果。
hierarchy:返回类型是(x,4)的二维ndarray。x和contours里的x是一样的意思。如果输入选择cv2.RETR_TREE,则以树形结构组织输出,hierarchy的四列分别对应下一个轮廓编号、上一个轮廓编号、父轮廓编号、子轮廓编号,该值为负数表示没有对应项
对提取所得contours的一些应用。
对输出的contours可以进行一些基本操作,比如计算contours[i]中所包括的点数,contours[i]的长度和面积等,下面列出求长度和面积用的函数:
求长度:cv2.arcLength(contours[i],False)
可以看到第二个参数是选择False还是True。这个参数指定识别的contours是否闭合,True对应闭合,False对应非闭合。
求面积:cv2.contourArea(contours[i])
这个步骤很简单主要使用的是cv2.contourArea(contours[i])函数
areas = []
for c in range(len(contours)):
areas.append(cv2.contourArea(contours[c]))
max_id = areas.index(max(areas))
max_rect = cv2.minAreaRect(contours[max_id])
做透视变化之前我们需要得到的选出最大面积轮廓的外接矩形,这个很关键,有了外接矩形我们就得到了透视变换的桥梁。
opencv中使用cv2.minAreaRect()函数,
max_rect = cv2.minAreaRect(contours[max_id]) #contours[max_id]这里是我们上个步骤中的到的面积最大的轮廓
# 返回的 max_rect 就是下图中的rect,里面包括外接矩形的中心点,宽和高,以及偏移角度
max_box = cv2.boxPoints(max_rect)
max_box = np.int0(max_box)
img2 = cv2.drawContours(img2,[max_box],0,(0,255,0),2)
# 外接矩形的四个顶点坐标
pts1 = np.float32(max_box)
# 变换后的矩形四个顶点坐标
pts2 = np.float32([[max_rect[0][0]+max_rect[1][1]/2, max_rect[0][1]+max_rect[1][0]/2],
[max_rect[0][0]-max_rect[1][1]/2, max_rect[0][1]+max_rect[1][0]/2],
[max_rect[0][0]-max_rect[1][1]/2, max_rect[0][1]-max_rect[1][0]/2],
[max_rect[0][0]+max_rect[1][1]/2, max_rect[0][1]-max_rect[1][0]/2]])
# pst1 和pst2中的每个点应该一一对应
构建透视变换的矩阵,透视变换
在此不阐述透视变换的原理,网上很多
M = cv2.getPerspectiveTransform(pts1,pts2)
dst = cv2.warpPerspective(img2, M, (img2.shape[1],img2.shape[0]))
裁剪时候要注意先y方向后x方向
target = dst[int(pts2[2][1]):int(pts2[1][1]),int(pts2[2][0]):int(pts2[3][0]),:]
此处的target就是我们的想要的面积最大的轮廓裁剪结果
import cv2
import numpy as np
img1= cv2.imread('./qingxie.png')
img2= cv2.imread('./qingxie.png')
img = cv2.cvtColor(img1, cv2.COLOR_BGR2GRAY)
contours, hierarchy = cv2.findContours(img, cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE)
areas = []
for c in range(len(contours)):
areas.append(cv2.contourArea(contours[c]))
max_id = areas.index(max(areas))
max_rect = cv2.minAreaRect(contours[max_id])
max_box = cv2.boxPoints(max_rect)
max_box = np.int0(max_box)
img2 = cv2.drawContours(img2,[max_box],0,(0,255,0),2)
pts1 = np.float32(max_box)
pts2 = np.float32([[max_rect[0][0]+max_rect[1][1]/2, max_rect[0][1]+max_rect[1][0]/2],
[max_rect[0][0]-max_rect[1][1]/2, max_rect[0][1]+max_rect[1][0]/2],
[max_rect[0][0]-max_rect[1][1]/2, max_rect[0][1]-max_rect[1][0]/2],
[max_rect[0][0]+max_rect[1][1]/2, max_rect[0][1]-max_rect[1][0]/2]])
M = cv2.getPerspectiveTransform(pts1,pts2)
dst = cv2.warpPerspective(img2, M, (img2.shape[1],img2.shape[0]))
# 此处可以验证 max_box点的顺序
color = [(0, 0, 255),(0,255,0),(255,0,0),(255,255,255)]
i = 0
for point in pts2:
cv2.circle(dst, tuple(point), 2, color[i], 4)
i+=1
target = dst[int(pts2[2][1]):int(pts2[1][1]),int(pts2[2][0]):int(pts2[3][0]),:]
cv2.imshow('img2',img2)
cv2.imshow('dst',dst)
cv2.imshow('target',target)
cv2.waitKey()
cv2.destroyAllWindows()
结果: