Python 图像处理 OpenCV (15):图像轮廓
前文传送门:
「Python 图像处理 OpenCV (2):像素处理与 Numpy 操作以及 Matplotlib 显示图像」
「Python 图像处理 OpenCV (3):图像属性、图像感兴趣 ROI 区域及通道处理」
「Python 图像处理 OpenCV (4):图像算数运算以及修改颜色空间」
「Python 图像处理 OpenCV (5):图像的几何变换」
「Python 图像处理 OpenCV (6):图像的阈值处理」
「Python 图像处理 OpenCV (7):图像平滑(滤波)处理」
「Python 图像处理 OpenCV (8):图像腐蚀与图像膨胀」
「Python 图像处理 OpenCV (9):图像处理形态学开运算、闭运算以及梯度运算」
「Python 图像处理 OpenCV (10):图像处理形态学之顶帽运算与黑帽运算」
「Python 图像处理 OpenCV (11):Canny 算子边缘检测技术」
「Python 图像处理 OpenCV (12): Roberts 算子、 Prewitt 算子、 Sobel 算子和 Laplacian 算子边缘检测技术」
「Python 图像处理 OpenCV (13): Scharr 算子和 LOG 算子边缘检测技术」
「Python 图像处理 OpenCV (14):图像金字塔」
引言
其实蛮不好意思的,刚才翻了翻自己的博客,上次写 OpenCV 的文章已经接近半个月以前了,我用 3 秒钟的时间回想了下最近两星期时间都花在哪了。
每次思考这种问题总会下意识甩锅给工作,最近工作忙的一批,emmmmmmmmmmmm。。。。。。。。。
这么骗自己是不对的!
实际上是美剧真香,最近把「反击」从第一季到第六季看了一遍,还不错,喜欢看动作类的同学可以尝试下。
本篇文章是关于图像处理轮廓方面的,下面开始正文,希望能帮到各位。
Q:什么是轮廓?
A:轮廓是一系列相连的点组成的曲线,代表了物体的基本外形,相对于边缘,轮廓是连续的,边缘并不全部连续。
寻找轮廓
寻找轮廓 OpenCV 为我们提供了一个现成的函数 findContours()
。
在 OpenCV 中,轮廓提取函数 findContours()
实现的是 1985 年由一名叫做 Satoshi Suzuki
的人发表的一篇论文中的算法,如下:
Satoshi Suzuki and others. Topological structural analysis of digitized binary images by border following. Computer Vision, Graphics, and Image Processing, 30(1):32–46, 1985.
对原理感兴趣的同学可以去搜搜看,不是很难理解。
先看一个示例代码:
import cv2 as cv
img = cv.imread("black.png")
gray_img = cv.cvtColor(img, cv.COLOR_BGR2GRAY)
# 降噪
ret, thresh = cv.threshold(gray_img, 127, 255, 0)
# 寻找轮廓
contours, hierarchy = cv.findContours(thresh, cv.RETR_TREE, cv.CHAIN_APPROX_NONE)
print(len(contours[0]))
这段代码先用 threshold()
对图像进行降噪处理,它的原型函数如下:
retval, dst = cv.threshold(src, thresh, maxval, type[, dst] )
- dst:结果图像。
- src:原图像。
- thresh:当前阈值。
- maxVal:最大阈值,一般为255。
- type:阈值类型,可选值如下:
enum ThresholdTypes {
THRESH_BINARY = 0, # 大于阈值的部分被置为 255 ,小于部分被置为 0
THRESH_BINARY_INV = 1, # 大于阈值部分被置为 0 ,小于部分被置为 255
THRESH_TRUNC = 2, # 大于阈值部分被置为 threshold ,小于部分保持原样
THRESH_TOZERO = 3, # 小于阈值部分被置为 0 ,大于部分保持不变
THRESH_TOZERO_INV = 4, # 大于阈值部分被置为 0 ,小于部分保持不变
THRESH_OTSU = 8, # 自动处理,图像自适应二值化,常用区间 [0,255]
};
查找轮廓使用的函数为 findContours()
,它的原型函数如下:
cv2.findContours(image, mode, method[, contours[, hierarchy[, offset ]]])
- image:源图像。
- mode:表示轮廓检索模式。
cv2.RETR_EXTERNAL 表示只检测外轮廓。
cv2.RETR_LIST 检测的轮廓不建立等级关系。
cv2.RETR_CCOMP 建立两个等级的轮廓,上面的一层为外边界,里面的一层为内孔的边界信息。如果内孔内还有一个连通物体,这个物体的边界也在顶层。
cv2.RETR_TREE 建立一个等级树结构的轮廓。
- method:表示轮廓近似方法。
cv2.CHAIN_APPROX_NONE 存储所有的轮廓点。
cv2.CHAIN_APPROX_SIMPLE 压缩水平方向,垂直方向,对角线方向的元素,只保留该方向的终点坐标,例如一个矩形轮廓只需4个点来保存轮廓信息。
这里可以使用 print(len(contours[0]))
函数将包含的点的数量打印出来,比如在上面的示例中,使用参数 cv2.CHAIN_APPROX_NONE
轮廓点有 1382 个,而使用参数 cv2.CHAIN_APPROX_SIMPLE
则轮廓点只有 4 个。
绘制轮廓
绘制轮廓使用到的 OpenCV 为我们提供的 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)
drawContours()
函数中有五个参数:
- 第一个参数是源图像。
- 第二个参数是应该包含轮廓的列表。
- 第三个参数是列表索引,用来选择要绘制的轮廓,为-1时表示绘制所有轮廓。
- 第四个参数是轮廓颜色。
- 第五个参数是轮廓线的宽度,为 -1 时表示填充。
我们接着前面的示例把使用 findContours()
找出来的轮廓绘制出来:
import cv2 as cv
img = cv.imread("black.png")
gray_img = cv.cvtColor(img, cv.COLOR_BGR2GRAY)
cv.imshow("img", img)
# 降噪
ret, thresh = cv.threshold(gray_img, 127, 255, 0)
# 寻找轮廓
contours, hierarchy = cv.findContours(gray_img, cv.RETR_TREE, cv.CHAIN_APPROX_NONE)
print(len(contours[0]))
# 绘制绿色轮廓
cv.drawContours(img, contours, -1, (0,255,0), 3)
cv.imshow("draw", img)
cv.waitKey(0)
cv.destroyAllWindows()
特征矩
特征矩可以帮助我们计算一些图像的特征,例如物体的质心,物体的面积等,使用的函数为 moments()
。
moments()
函数会将计算得到的矩以字典形式返回。
import cv2 as cv
img = cv.imread("number.png")
gray_img = cv.cvtColor(img, cv.COLOR_BGR2GRAY)
# 降噪
ret, thresh = cv.threshold(gray_img, 127, 255, 0)
# 寻找轮廓
contours, hierarchy = cv.findContours(gray_img, cv.RETR_TREE, cv.CHAIN_APPROX_NONE)
cnt = contours[0]
# 获取图像矩
M = cv.moments(cnt)
print(M)
# 质心
cx = int(M['m10'] / M['m00'])
cy = int(M['m01'] / M['m00'])
print(f'质心为:[{cx}, {cy}]')
这时,我们取得了这个图像的矩,矩 M 中包含了很多轮廓的特征信息,除了示例中展示的质心的计算,还有如 M['m00'] 表示轮廓面积。
轮廓面积
area = cv.contourArea(cnt)
print(f'轮廓面积为:{area}')
这里取到的轮廓面积和上面 M['m00'] 保持一致。
轮廓周长
perimeter = cv.arcLength(cnt, True)
print(f'轮廓周长为:{perimeter}')
参数 True
表示轮廓是否封闭,我们这里的轮廓是封闭的,所以这里写 True
。
轮廓外接矩形
轮廓外接矩形分为正矩形和最小矩形。使用 cv2.boundingRect(cnt)
来获取轮廓的外接正矩形,它不考虑物体的旋转,所以该矩形的面积一般不会最小;使用 cv.minAreaRect(cnt)
可以获取轮廓的外接最小矩形。
两者的区别如上图,绿线代表的是外接正矩形,红线代表的是外接最小矩形,代码如下:
import cv2 as cv
import numpy as np
img = cv.imread("number.png")
gray_img = cv.cvtColor(img, cv.COLOR_BGR2GRAY)
# 降噪
ret, thresh = cv.threshold(gray_img, 127, 255, 0)
# 寻找轮廓
contours, hierarchy = cv.findContours(gray_img, cv.RETR_TREE, cv.CHAIN_APPROX_NONE)
cnt = contours[0]
# 外接正矩形
x, y, w, h = cv.boundingRect(cnt)
cv.rectangle(img, (x, y), (x + w, y + h), (0, 255, 0), 2)
# 外接最小矩形
min_rect = cv.minAreaRect(cnt)
print(min_rect)
box = cv.boxPoints(min_rect)
box = np.int0(box)
cv.drawContours(img, [box], 0, (0, 0, 255), 2)
cv.imshow("draw", img)
cv.waitKey(0)
cv.destroyAllWindows()
boundingRect()
函数的返回值包含四个值,矩形框左上角的坐标 (x, y) 、宽度 w 和高度 h 。
minAreaRect()
函数的返回值中还包含旋转信息,返回值信息为包括中心点坐标 (x,y) ,宽高 (w, h) 和旋转角度。
轮廓近似
根据我们指定的精度,它可以将轮廓形状近似为顶点数量较少的其他形状。它是由 Douglas-Peucker 算法实现的。
OpenCV 提供的函数是 approxPolyDP(cnt, epsilon, True)
,第二个参数 epsilon 用于轮廓近似的精度,表示原始轮廓与其近似轮廓的最大距离,值越小,近似轮廓越拟合原轮廓。第三个参数指定近似轮廓是否是闭合的。具体用法如下:
import cv2 as cv
img = cv.imread("number.png")
gray_img = cv.cvtColor(img, cv.COLOR_BGR2GRAY)
# 降噪
ret, thresh = cv.threshold(gray_img, 127, 255, 0)
# 寻找轮廓
contours, hierarchy = cv.findContours(gray_img, cv.RETR_TREE, cv.CHAIN_APPROX_NONE)
cnt = contours[0]
# 计算 epsilon ,按照周长百分比进行计算,分别取周长 1% 和 10%
epsilon_1 = 0.1 * cv.arcLength(cnt, True)
epsilon_2 = 0.01 * cv.arcLength(cnt, True)
# 进行多边形逼近
approx_1 = cv.approxPolyDP(cnt, epsilon_1, True)
approx_2 = cv.approxPolyDP(cnt, epsilon_2, True)
# 画出多边形
image_1 = cv.cvtColor(gray_img, cv.COLOR_GRAY2BGR)
image_2 = cv.cvtColor(gray_img, cv.COLOR_GRAY2BGR)
cv.polylines(image_1, [approx_1], True, (0, 0, 255), 2)
cv.polylines(image_2, [approx_2], True, (0, 0, 255), 2)
cv.imshow("image_1", image_1)
cv.imshow("image_2", image_2)
cv.waitKey(0)
cv.destroyAllWindows()
第一张图是 epsilon 为原始轮廓周长的 10% 时的近似轮廓,第二张图中绿线就是 epsilon 为原始轮廓周长的 1% 时的近似轮廓。
轮廓凸包
凸包外观看起来与轮廓逼近相似,只不过它是物体最外层的「凸」多边形。
如下图,红色的部分为手掌的凸包,双箭头部分表示凸缺陷(Convexity Defects),凸缺陷常用来进行手势识别等。
import cv2 as cv
img = cv.imread("number.png")
gray_img = cv.cvtColor(img, cv.COLOR_BGR2GRAY)
# 降噪
ret, thresh = cv.threshold(gray_img, 127, 255, 0)
# 寻找轮廓
contours, hierarchy = cv.findContours(gray_img, cv.RETR_TREE, cv.CHAIN_APPROX_NONE)
cnt = contours[0]
# 绘制轮廓
image = cv.cvtColor(gray_img, cv.COLOR_GRAY2BGR)
cv.drawContours(image, contours, -1, (0, 0 , 255), 2)
# 寻找凸包,得到凸包的角点
hull = cv.convexHull(cnt)
# 绘制凸包
cv.polylines(image, [hull], True, (0, 255, 0), 2)
cv.imshow("image", image)
cv.waitKey(0)
cv.destroyAllWindows()
还有一个函数,是可以用来判断图形是否凸形的:
print(cv.isContourConvex(hull)) # True
它的返回值是 True 或者 False 。
最小闭合圈
接下来,使用函数 cv.minEnclosingCircle()
查找对象的圆周。它是一个以最小面积完全覆盖物体的圆。
import cv2 as cv
img = cv.imread("number.png")
gray_img = cv.cvtColor(img, cv.COLOR_BGR2GRAY)
# 降噪
ret, thresh = cv.threshold(gray_img, 127, 255, 0)
# 寻找轮廓
contours, hierarchy = cv.findContours(gray_img, cv.RETR_TREE, cv.CHAIN_APPROX_NONE)
cnt = contours[0]
# 绘制最小外接圆
(x, y), radius = cv.minEnclosingCircle(cnt)
center = (int(x), int(y))
radius = int(radius)
cv.circle(img, center, radius, (0, 255, 0), 2)
cv.imshow("img", img)
cv.waitKey(0)
cv.destroyAllWindows()
下一个是把一个椭圆拟合到一个物体上。它返回内接椭圆的旋转矩形。
ellipse = cv.fitEllipse(cnt)
cv.ellipse(img, ellipse, (0, 255, 0), 2)
参考
作者:极客挖掘机
定期发表作者的思考:技术、产品、运营、自我提升等。
本文版权归作者极客挖掘机和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
如果您觉得作者的文章对您有帮助,就来作者个人小站逛逛吧:极客挖掘机
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 分享 3 个 .NET 开源的文件压缩处理库,助力快速实现文件压缩解压功能!
· Ollama——大语言模型本地部署的极速利器
· DeepSeek如何颠覆传统软件测试?测试工程师会被淘汰吗?