虽然Canny.之类的边缘检测算法可以根据像素间的差异检测出轮廓边界的像素,但是它并没有将轮廓作为一个整体进行处理。下一步是要将这些边缘像素合成轮廓。本文记录 OpenCV 中的轮廓查找的相关操作。
轮廓查找概述
- 一个轮廓对应一系列点,这些点以某种方式表示图像中的一条曲线。在不同情况下,这种表示方式也有所不同。有多种方式可以表示一条曲线。
- OpenCV 中用一系列二维顶点表示一个轮廓
- 函数
cv2.findContours()
从二维图像中计算轮廓。它处理的图像可以是从cv2.Canny()
函数得到的有边缘像素的图像,或是从cv2.threshold()
及cv2.adaptiveThreshold()
函数得到的图像,这时边缘是正负区域之间的边界。
轮廓层次
- 在了解到底如何提取轮廓之前,有必要花一些时间来理解轮廓到底是什么以及一组轮廓之间如何互相关联。特别要注意轮廓树的概念,这对于理解其中一种最有效的方法
cv2.findContours()
非常重要。
- 上图为一张输入cv2.findContours()函数的测试图像(左图)。图中有五块颜色区域(分别标记为A,B,C,D,E), 每块区域的外部边界和内部边界都各自组成轮廓。因此共有9条轮廓。每条轮廓都由一组输出列表表示(右上角图一轮廓参数)。也可以选择生成一组层次表达(右下角图一层次参数)。在右下角的图中(对应构筑的轮廓树),每一个节点就是一条轮廓。根据每个节点在层次队列中的四元数组索引,图中的链接都做了相应标记。
cv2.findContours
找出二值图中的轮廓。 官方文档
- 函数使用:
cv2.findContours(
image, # uint8 单通道图像,非零值即为前景,0为背景
mode, # 轮廓检索模式
method[, # 轮廓近似法
contours[, # 检测到的轮廓。每个轮廓都存储为点向量
hierarchy[, # 可选输出向量, 包含有关图像拓扑的信息。它具有与轮廓数一样多的元素
offset]]] # 每个轮廓点移动的可选偏移量。
如果从图像 ROI 中提取轮廓,然后应该在整个图像上下文中对其进行分析,可以使用该参数。
) ->
contours,
hierarchy
method:ContourApproximationModes
取值 含义 cv2.CHAIN_APPROX_NONE 存储了所有的轮廓点。也就是说,等高线的任意2个后续点(x1,y1)和(x2,y2)将是水平、垂直或对角线邻居,即 max (abs (x1-x2),abs (y2-y1)) = 1。 cv2.CHAIN_APPROX_SIMPLE 压缩水平、垂直和对角线段,只留下它们的端点。例如,一个直立的矩形轮廓用 4 个点进行编码。 cv2.CHAIN_APPROX_TC89_L1 运用了 Teh-Chin 连锁近似演算法的一种 cv2.CHAIN_APPROX_TC89_KCOS 运用了 Teh-Chin 连锁近似演算法的一种
mode:RetrievalModes
- 示例代码
img = 255 - mt.cv_rgb_imread('conc.png', gray=True)
contours, hierarchy = cv2.findContours(img, cv2.RETR_TREE, cv2.CHAIN_APPROX_NONE)
contours = mt.get_list_from_list(contours, lambda x: np.squeeze(x))
points = np.vstack(contours)
res = np.zeros_like(img)
res[points[:, 1], points[:, 0]] = 255
mt.PIS(img, res)
绘制轮廓
获得一列轮廓后,一个最常用的功能是在屏幕上绘制检测到的轮廓。绘制轮廓可以用cv2.drawContours()
函数完成。
cv2.drawContours
绘制等高线轮廓或填充等高线。 官方文档
- 函数使用
cv2.drawContours(
image, # 目标画布图像
contours, # 轮廓
contourIdx, # 参数表示要绘制的轮廓。如果为负值,则绘制所有轮廓。
color[, # 颜色
thickness[, # 宽度
lineType[, # 线型
hierarchy[, # 有关层次结构的可选信息。仅当您只想绘制一些轮廓时才需要它(请参阅 maxLevel )。
maxLevel[, # 绘制轮廓的最大级别。
如果为 0,则仅绘制指定的轮廓。如果为 1,则函数绘制轮廓和所有嵌套轮廓。
如果为 2,则函数绘制轮廓、所有嵌套轮廓、所有嵌套到嵌套的轮廓,依此类推。仅当存在可用层次结构时才考虑此参数。
offset]]]]]
) -> image
- 示例代码
img = 255 - mt.cv_rgb_imread('conc.png', gray=True)
contours, hierarchy = cv2.findContours(img, cv2.RETR_TREE, cv2.CHAIN_APPROX_NONE)
res = np.zeros_like(img)
cv2.drawContours(res, contours, 2, [200], -1)
PIS(img, res)
快速连通区域分析
- 与轮廓分析紧密相关的另一种方法是连通区域分析。采用阈值化等方法分割一张图像后,我们可以采用连通区域分析来有效地对返回图像逐张分离和处理。 OpenCV中的连通区域分析算法,输入要求是一张二值(黑白)图像,输出是一张像素标记图,其中属于同一连通区域的非零像素都是同一定值。
- 可以调用 findcontours 函数之后再使用 drawcontours 函数填充,但是这样比较慢,原因如下:
-
cv2.findContours()
函数首先为每条轮廓分配一个独立的标准模板库向量, 而图像中可能存在上百、甚至上千条轮廓。之后当你想填充一块由一条或多条轮廓包围的非凸区域时,cv2.drawContours()
也很慢,而且需要收集所有包围该区域的小线段并排序。最后,收集一块连通区域的基本信息(例如一块区域或一个包围框)需要更多、更耗时的函数调用。
cv2.connectedComponents / cv2.connectedComponentsWithAlgorithm
计算布尔图像的连接组件标记图像 官方文档
- 函数使用
cv2.connectedComponents(
image[, # 要标记的 8 位单通道图像
labels[, # 输出的目标图像
connectivity[, # 8 或 4 分别用于 8 路或 4 路连接
ltype]]] # 输出类型
) ->
retval, # 连通域个数(包含背景 0)
labels
# 选择指定算法计算连通域
cv2.connectedComponentsWithAlgorithm(
image, # 要标记的 8 位单通道图像
connectivity, # 8 或 4 分别用于 8 路或 4 路连接
ltype, # 输出图像标签类型。目前支持 CV_32S 和 CV_16U
ccltype[, # 连通算法类型
labels] # 输出的目标图像
) ->
retval, # 连通域个数(包含背景 0)
labels
- ccltype:包含
cv2.CCL_DEFAULT
,cv2.CCL_WU
,cv2.CCL_GRANA
,cv2.CCL_BOLELLI
,cv2.CCL_SAUF
,cv2.CCL_BBDT
,cv2.CCL_SPAGHETTI
- 示例代码
img = 255 - mt.cv_rgb_imread('conc.png', gray=True)
retval, labels = cv2.connectedComponents(img)
PIS(labels)
img = 255 - mt.cv_rgb_imread('conc.png', gray=True)
retval, labels = cv2.connectedComponentsWithAlgorithm(img, 8, cv2.CV_32S, cv2.CCL_WU)
PIS(labels)
参考资料
- 《学习 OpenCV3》 第十四章