本文代码全部可运行,笔者运行环境:python3.7+pycharm+opencv4.6。此文是学习记录,记录opencv的入门知识,对各知识点并不做深入探究。文章的目的是让阅读者在极短的时间达到入门水平。在学习过程中,我们应养成 查询opencv官方文档的好习惯。
OpenCV是一个(开源)发行的跨平台计算机视觉库,可以运行在Linux、Windows和Mac OS操作系统上。它轻量级而且高效——由一系列 C 函数和少量 C++ 类构成,同时提供了Python、Ruby、MATLAB等语言的接口,实现了图像处理和计算机视觉方面的很多通用算法。
目录
1. 图像的读取、显示与写入
1.1. 读取图像
1.2. 显示图像
1.3. 写入图像
2. 图像的色彩空间
3. 图像编辑
3.1. 调整大小
3.2. 图像裁剪
3.3. 旋转和平移
3.4. 图像翻转
4. 图像注释
4.1. 绘制直线
4.2. 绘制圆形
4.3. 绘制矩形
4.4. 绘制椭圆
4.5. 添加文本
5. 鼠标回调函数和跟踪栏回调函数
5.1. 鼠标回调函数
5.2. 跟踪栏回调函数
6. 图像过滤
6.1. 卷积核及其应用
6.2. 内置函数模糊图像
6.3. 高斯模糊
6.4. 中值模糊
6.5. 自定义内核锐化图像
6.6. 图像的双边滤波
7. 图像阈值处理
8. Blob检测
8.1. Blob检测是如何工作的?
8.2. 按颜色、大小和形状筛选Blob
8.3. 设置cv2.SimpleBlobDeterctor参数
9. 边缘检测
9.1. Sobel边缘检测
9.2. Canny边缘检测
10. 轮廓检测
10.1. 轮廓检测的步骤
10.2. 轮廓检测的实现
10.3. 轮廓的层次结构
11. 参考内容
图像的读取、显示与写入分别对应三个函数,cv2. imread()
、cv2.imshow()
、cv2.imwrite()
。
语法:cv2.imread(filename[, flags])--->image
参数:
filename---文件路径(相对路径和绝对路径),路径中不要带有中文。
flags---可选标志,用于指定读取图像的样式,常见的有cv2.IMREAD_UNCHANGED(-1)
、cv2.IMREAD_GRAYSCALE(0)
、cv2.IMREAD_COLOR(1)
。默认为1。
注意:opencv读取彩色图像的格式是BGR,而大多数视觉库使用的是RGB,因此当将 OpenCV 与其他工具包一起使用时,当从一个库切换到另一个库时,不要忘记交换蓝色和红色通道。
语法:cv2.imshow(window_name, image)--->None
参数:
window_name---显示图像的窗口的名字。
image---显示图像的变量名。
注意:该函数一般和cv2.waitKey()
、cv2.destroyAllWindows()
、cv2.destroyWindow()
一起使用。cv2.waitKey()
函数是键盘绑定函数,等待键击任意键或指定键继续程序。cv2.destroyAllWindows()
用于销毁全部窗口(从内存中清除)、cv2.destroyWindow()
销毁指定窗口(从内存中清除)。
# 读取、显示图像
import cv2
# 查看opencv版本
print(cv2.getVersionString())
# 读取图像
image_unchanged = cv2.imread("image\\cat.jpg", cv2.IMREAD_UNCHANGED)
image_grayscale = cv2.imread("image\\cat.jpg", cv2.IMREAD_GRAYSCALE)
image_color = cv2.imread("image\\cat.jpg", cv2.IMREAD_COLOR)
# 显示图像
cv2.imshow('unchanged', image_unchanged)
cv2.imshow('grayscale', image_grayscale)
cv2.imshow('color', image_color)
# 永远暂停程序直到键击任意键
cv2.waitKey(0)
# 销毁所有窗口
cv2.destroyAllWindows()
运行结果:
语法:cv2.imwrite(filename, image[, params])
参数:
filename---文件名,它必须包含文件的扩展名(如,.jpg
、.png
)
image---要保存的图像变量名。
# 写入图像
import cv2
image_grayscale = cv2.imread("image\\cat.jpg", cv2.IMREAD_GRAYSCALE)
cv2.imshow('grayscale', image_grayscale)
cv2.imwrite("image\\cat_grayscale.jpg", image_grayscale)
cv2.waitKey(0)
cv2.destroyAllWindows()
运行结果如下:
常用的图像色彩空间有RGB、Lab、YCrCb、HSV等模式,
RGB模式:通过对红(R)、绿(G)、蓝(B)三个颜色通道的变化以及它们相互之间的叠加来得到各种颜色。依赖于光线。R、G、B的取值范围:[0, 255]。容易理解,但连续变换时不直观。
详见:RGB_百度百科
Lab模式:它是一种设备无关的颜色模型,也是一种基于生理特征的颜色模型。它由亮度(L)、颜色(a:绿色到洋红色;b:蓝色到黄色)组成。
详见:Lab颜色模型_百度百科
YCrCb模式:它派生自RGB颜色模式,主要应用在优化彩色视频信号的传输,使其向后相容老式黑白电视。Y:伽马校正后从RGB获得的亮度或亮度分量;Cr:反映了RGB输入信号红色部分与RGB信号亮度值之间的差异;Cb:反映的是RGB输入信号蓝色部分与RGB信号亮度值之间的差异。
HSV模式:它是H、S、V三维颜色空间中的一个可见光子集。是为了数字化图像提出来的,不能很好的表示人眼解释图像的过程。H:色相;S:饱和度;V:明度。取值范围,H---[0, 179],S---[0, 255],V---[0, 255]。
详见:HSV颜色模型_百度百科
灰度图:图像是有不同灰度的像素组成的,每个像素都是在[0, 255]取值。根据人眼敏感度,把RGB图片转换为灰度图,不是简单把RGB每个通道取平均值而是:Y = 0.299*R + 0.587*G + 0.114*B
注意:必须要指出的是图像就是一个数组,因此可以使用image.shape
查询形状,其输出结果为 (pixel_height, pixel_width, 3)
。pixel_height
、pixel_width
代表图像的像素尺寸,3
代表每个像素的颜色是由一个三维数组确定的(opencv中是BGR模式的数组)。
如何查看某像素的RGB、Lab、YCrCb、HSV值?
由于在读取图像时直接读取的是RGB值,因此可通过cv2.imshow()
函数直接查看。
利用cv2.cvtColor(np.uint8([[[image[x, y]]]]), cv2.COLOR_BGR2Lab)[0][0]
查看位置(x, y)处的Lab值。YCrCb、HSV与Lab类似。
注意:图像中某位置的图像色彩空间转换为其他时,必须要使其BGR "空间值"(由三个通道组成)三维化。
练习:如何通过鼠标的移动实时获取指定图片的不同色彩空间值?
import cv2
import glob
import numpy as np
# 定义鼠标回调的‘动作’函数
def showPixelValue(event, x, y, flags, param):
# 定义全局变量
global img, combinedResult, placeholder
# 判断鼠标是否移动
if event == cv2.EVENT_MOUSEMOVE:
# 获取鼠标坐标的RGB值
bgr = img[y, x]
# 转换色彩空间值
ycb = cv2.cvtColor(np.uint8([[bgr]]), cv2.COLOR_BGR2YCrCb)[0][0]
lab = cv2.cvtColor(np.uint8([[bgr]]), cv2.COLOR_BGR2Lab)[0][0]
hsv = cv2.cvtColor(np.uint8([[bgr]]), cv2.COLOR_BGR2HSV)[0][0]
# 创建一个与载入图片高度相同的黑色图块,作为结果区域
placeholder = np.zeros((img.shape[0], 400, 3), dtype=np.uint8)
# 在结果区域内显示鼠标位置不同色彩空间的值
cv2.putText(placeholder, "BGR {}".format(bgr), (20, 70), cv2.FONT_HERSHEY_COMPLEX, .9, (255, 255, 255), 1,
cv2.LINE_AA)
cv2.putText(placeholder, "HSV {}".format(hsv), (20, 140), cv2.FONT_HERSHEY_COMPLEX, .9, (255, 255, 255), 1,
cv2.LINE_AA)
cv2.putText(placeholder, "YCrCb {}".format(ycb), (20, 210), cv2.FONT_HERSHEY_COMPLEX, .9, (255, 255, 255), 1,
cv2.LINE_AA)
cv2.putText(placeholder, "LAB {}".format(lab), (20, 280), cv2.FONT_HERSHEY_COMPLEX, .9, (255, 255, 255), 1,
cv2.LINE_AA)
# 合并加载的图片和结果区域
combinedResult = np.hstack([img, placeholder])
# 显示合并后的图像
cv2.imshow('PRESS P for Previous, N for Next Image', combinedResult)
if __name__ == '__main__':
# 读取指定的图片,并转换尺寸
files = glob.glob('image/rub*.jpg')
files.sort()
img = cv2.imread(files[0])
img = cv2.resize(img, (400, 400))
# 显示转换尺寸后的图像(在‘PRESS...’显示框加载)
cv2.imshow('PRESS P for Previous, N for Next Image', img)
# 创建一个无加载图像的显示框,并覆盖了原‘PRESS...’显示框
cv2.namedWindow('PRESS P for Previous, N for Next Image')
# 创建一个鼠标回调,加载回调‘动作’函数,并且该动作在‘PRESS...’显示框内实现
cv2.setMouseCallback('PRESS P for Previous, N for Next Image', showPixelValue)
i = 0
while 1:
k = cv2.waitKey(1) & 0xFF
# 键击‘n’切换到下一张图片
if k == ord('n'):
i += 1
img = cv2.imread(files[i % len(files)])
img = cv2.resize(img, (400, 400))
cv2.imshow('PRESS P for Previous, N for Next Image', img)
# 键击‘p’切换到上一张图片
elif k == ord('p'):
i -= 1
img = cv2.imread(files[i % len(files)])
img = cv2.resize(img, (400, 400))
cv2.imshow('PRESS P for Previous, N for Next Image', img)
# 键击‘Esc’退出程序
elif k == 27:
cv2.destroyAllWindows()
break
运行结果如下(静态展示):
图像编辑主要包括调整图像大小、裁剪图像、图像的旋转和移动、翻转,透视变换。
在opencv中使用image.shape
获得图像大小,image.shape
获得结果是(heigh, width, channels)
即高度、宽度、通道数。在opencv中使用resize()函数调整图像的大小。
语法:cv2.resize(src, dsize[, dst[, fx[, fy[, interpolation]]]])--->dst
参数:
src---源图像。
dsize---可为tuple参数或None。目标图像的大小,即新的图像宽高。需要注意的是,dsize=(width, height)中第 一个参数是图像宽度第二个是高度,与shape=(height, width, channels)的相对位置刚好相反。
dst---目标图像,在python中无任何意义,一般不传参或设成None。
fx---沿水平轴的比例因子。fy---沿垂直轴的比例因子。
interpolation---插值方式,提供了调整图像大小的不同方法。其本质是一个int数值,不过一般用opencv内置的参 数名称以提高可读性。默认为cv2.INTER_LINEAR
。其他还有cv2.INTER_AREA
、cv2.INTER_CUBIC
、cv2.INTER_NEAREST
。
注意:当目标图像与源图像的宽高比不一致,代表经缩放后图像失真。关于插值参数,①要缩小图片选用cv2.INTER_AREA
;②放大图片选用cv2.INTER_CUBIC
(速度慢)或cv2.INTER_LINEAR
(速度快,效果还行)。
# 调整图像的大小
import cv2
cat = cv2.imread('image\\cat.jpg')
# 指定目标图像的宽高
# 图像不失真
cat_down_0 = cv2.resize(cat, (150, 150))
cat_up_0 = cv2.resize(cat, (500, 500))
cv2.imshow('cat_down-No distortion', cat_down_0)
cv2.imshow('cat_up-No distortion', cat_up_0)
# 图像失真
cat_distortion = cv2.resize(cat, (200, 300))
cv2.imshow('cat distortion', cat_distortion)
cv2.waitKey(0)
cv2.destroyAllWindows()
# 指定图像的比例因子(需要指出的是dsize必须要传参,None)
# 图像不失真
cat_down_1 = cv2.resize(cat, None, fx=0.75, fy=0.75)
cat_up_1 = cv2.resize(cat, None, fx=1.2, fy=1.2)
cv2.imshow('cat_down-No distortion1', cat_down_1)
cv2.imshow('cat_up-No distortion1', cat_up_1)
# 图像失真
cat_distortion1 = cv2.resize(cat, None, fx=0.75, fy=1.2)
cv2.imshow('cat distortion1', cat_distortion1)
cv2.waitKey(0)
cv2.destroyAllWindows()
# 指定图像缩放方法
cat_scale0 = cv2.resize(cat, (150, 150), interpolation=cv2.INTER_AREA)
cat_scale1 = cv2.resize(cat, (450, 450), interpolation=cv2.INTER_CUBIC)
cat_scale2 = cv2.resize(cat, (450, 450), interpolation=cv2.INTER_LINEAR)
cat_scale3 = cv2.resize(cat, (150, 150), interpolation=cv2.INTER_NEAREST)
cv2.imshow('cat_scale0', cat_scale0)
cv2.imshow('cat_scale1', cat_scale1)
cv2.imshow('cat_scale2', cat_scale2)
cv2.imshow('cat_scale3', cat_scale3)
cv2.waitKey(0)
cv2.destroyAllWindows()
运行结果(部分)如下:
裁剪图像是为了从图像中删除不需要的对象或区域;也就是删除其他,保留想要的图像区域。
opencv中的图像裁剪是利用numpy数组切片的方法来实现的。首先,我们要知道图像就是一个数组,其由高、宽、通道数三个维度组成。仅对图像进行裁剪,就是意味着对宽和高两个维度进行切片操作。具体如下:
语法:image[start_row : end_row, start_col : end_col]--->dst
参数:start_row 、end_row---图像的开始与结束的行坐标。
start_col :、end_col---图像的开始与结束的列坐标。
注意:图像可视为坐标系,坐标原点为图像的左上角顶点,裁剪区域即是四条直线的围合区域。
# 图像裁剪
import cv2
cat = cv2.imread('image\\cat.jpg')
print(cat.shape)
# 裁剪图像
cropped_cat = cat[25:190, 50:300]
cv2.imshow('original', cat)
cv2.imshow('cropped', cropped_cat)
cv2.waitKey(0)
cv2.destroyAllWindows()
运行结果如下:
图像的旋转和平移是图像编辑中最基本的操作之一。旋转和平移都属于同一种图像编辑类型,即放射变换(Affine transformations)类型。
旋转和平移都是借助cv2.warpAffine()
函数实现的,其区别是转换矩阵M的不同。旋转利用cv2.getRotationMatrix2D()
函数构造旋转矩阵,平移矩阵利用二维数组构建。
首先看一下仿射变换函数,cv.warpAffine()
。需要指出的是,图像变换时其坐标系是不变的,与源图像一致。
语法:cv2.warpAffine(src, M, desize[, dst[, flags[, borderMode[, borderValue]]]])--->dst
参数:
src---源图像。M---转换矩阵。
desize---tuple类型参数。目标图像的宽高,即新的图像宽高(新的图像可能包含部分或者全部变换后的图像,其他区域一般由黑色像素填充)。坐标系遵循源图像坐标系。
dst---目标图像,在python中无任何意义,一般不传参或设成None。
flags---插值方法。cv2.INTER_NEAREST
,最近邻插值;cv2.INTER_LINEAR
,线性插值(默认值);cv2.INTER_AREA
,区域插值;cv2.INTER_CUBIC
,三次样条插值;cv2.INTER_LANCZOS4
,Lanczos插值,等。
borderMode---边界像素模式。
borderValue---边界填充值(即显示框内图像之外的区域),默认值为0。
旋转矩阵的构造,cv2.getRotationMatrix2D()
。
语法:cv2.getRotationMatrix2D(center, angle, scale)--->M
参数:
center---图像的旋转中心,是一个元组类型的参数。即在图像构建的坐标系中选取旋转中心。
angle---旋转角度(角度制),正负值遵循坐标系角度规则。
scale---图像的缩放比例因子。
平移矩阵的构造。
构造数组,2Darray = np.ndarray([[1 0 tx], [0, 1, ty]])
参数:tx---正值,图像将右移|tx|个像素;负值,将向左移|tx|个像素。
ty---正值,图像将下移|ty|个像素;负值,将向上移|ty|个像素
平移矩阵,M = 2Darray
# 图像的旋转和平移
import cv2
import numpy as np
cat = cv2.imread('image\\cat.jpg')
print(cat.shape)
height, width = cat.shape[:2]
# 旋转图像
center = (width/3, height/10)
rotate_matrix = cv2.getRotationMatrix2D(center, -15, scale=1.2)
rotated_cat = cv2.warpAffine(cat, rotate_matrix, dsize=(500, 300))
# 原图像以坐标(width/3, height/10)为旋转中心旋转-15度然后再缩放1.2倍
cv2.imshow('original image', cat)
cv2.imshow('rotated_cat', rotated_cat)
cv2.waitKey(0)
cv2.destroyAllWindows()
# 平移图像
tx, ty = width/5, -height/5
translation_matrix = np.array([[1, 0, tx], [0, 1, ty]])
translation_cat = cv2.warpAffine(cat, translation_matrix, dsize=(500, 300))
cv2.imshow('original image', cat)
cv2.imshow('translation_cat', translation_cat)
cv2.waitKey(0)
cv2.destroyAllWindows()
运行结果如下:
图像翻转,也就是通常所说的镜像。主要包括垂直翻转、水平翻转、水平垂直翻转。
语法:cv2.flip(src, flipCode[, dst])--->dst
参数:
src---源图像。
flipCode---翻转方式。flipCode == 0
,垂直翻转(沿x轴翻转);flipCode>0
,水平翻转(沿y轴翻转);flipCode< 0
,水平垂直翻转(先沿X轴翻转,再沿Y轴翻转,等价于旋转180°)。
# 图像翻转
import cv2
import numpy as np
cat = cv2.imread('image\\cat.jpg')
# 翻转图像
flip_cat1 = cv2.flip(cat, flipCode=0)
flip_cat2 = cv2.flip(cat, flipCode=1)
flip_cat3 = cv2.flip(cat, flipCode=-1)
combined = np.hstack((cat, flip_cat1, flip_cat2, flip_cat3))
cv2.imshow('combined', combined)
cv2.waitKey(0)
cv2.destroyAllWindows()
运行结果:
注释图像即是在图像添加注释性信息,通常是绘制图形标出对象或者添加文字信息。需要指出的是在进行注释图像前一定要使用copy()
方法创建副本,以确保源图像的完整性。
语法:cv2.line(image, start_point, end_point, color, thickness)--->image
参数:
image---图像,通常是源图像的副本。
start_point---直线的起点坐标,元组参数。
end_point---直线的终点坐标,元组参数。
color---直线的颜色,元组参数。
thickness---直线的厚度。
注意:start_point与end_point的值第一个值代表的是横轴值,第二个值代表的是竖轴值,可简单理解为x、y轴的值。
# 绘制直线
import cv2
girl = cv2.imread('image\\girl.jpg')
print(girl.shape)
# 绘制一条长260像素的横向白色线
girl_line = girl.copy()
cv2.line(girl_line, (0, 250), (260, 250), (255, 255, 255), thickness=2)
cv2.imshow('girl_line', girl_line)
cv2.waitKey()
cv2.destroyAllWindows()
运行结果如下:
语法:cv2.circle(image, center_coordinates, radius, color[, thickness[, lineType[, shift]]])--->image
参数:
image---图像,通常是源图像的副本。
center_coordinates---圆形的圆心坐标,元组参数。
radius---圆形的半径。
color---直线的颜色,元组参数。
thickness---正值,直线的厚度;负值表示绘制一个填充圆。
lineType---圆边线类型。
shift---圆心和半径值中的小数位数。
注意:start_point与end_point的值第一个值代表的是横轴值,第二个值代表的是竖轴值,可简单理解为x、y轴的值。
# 绘制圆形
import cv2
# 绘制一个圆形
girl = cv2.imread('image\\girl.jpg')
girl_circle = girl.copy()
cv2.circle(girl_circle, (380, 270), 175, (255, 255, 255), thickness=2)
cv2.imshow('girl_circle', cv2.resize(girl_circle, None, fx=0.6, fy=0.6))
# 绘制一个填充圆
girl_circle1 = girl.copy()
cv2.circle(girl_circle1, (380, 270), 175, (255, 255, 255), thickness=-1)
cv2.imshow('girl_circle1', cv2.resize(girl_circle1, None, fx=0.6, fy=0.6))
cv2.waitKey()
cv2.destroyAllWindows()
运行结果如下:
语法:cv2.rectangle(image, start_point, end_point, color[, thickness[, lineType[, shift]]])--->image
参数:
image---图像,通常是源图像的副本。
start_point---矩形的起始点,即其左上角,元组参数。
end_point---矩形的终点,即其右下角, 元组参数。
color---直线的颜色,元组参数。
thickness---正值,直线的厚度;负值表示绘制一个填充矩形。
lineType---矩形边线类型。
shift---坐标中的小数位数。
注意:start_point与end_point的值第一个值代表的是横轴值,第二个值代表的是竖轴值,可简单理解为x、y轴的值。
import cv2
# 绘制一个矩形
girl = cv2.imread('image\\girl.jpg')
print(girl.shape)
girl_rec = girl.copy()
cv2.rectangle(girl_rec, (200, 170), (570, 400), (255, 255, 255), thickness=2)
cv2.imshow('girl_rec', cv2.resize(girl_rec, None, fx=0.6, fy=0.6))
# 绘制一个填充矩形
girl_rec1 = girl.copy()
cv2.rectangle(girl_rec1, (200, 170), (570, 400), (255, 255, 255), thickness=-2)
cv2.imshow('girl_rec1', cv2.resize(girl_rec1, None, fx=0.6, fy=0.6))
cv2.waitKey()
cv2.destroyAllWindows()
运行结果如下:
语法:cv2.ellipse(image, center, axesLength, angle, startAngle, endAngle, color[, thickness[,lineType[, shift]]])--->image
参数:
image---图像,通常是源图像的副本。
centert---椭圆的圆心坐标,元组参数。
axesLength---椭圆长轴与短轴大小的一半,元组参数。
angle---椭圆的旋转角度(角度制)。
startAngle---椭圆弧的起始角度。
endAngle---椭圆弧的结束角度。
color---直线的颜色,元组参数。
thickness---正值,直线的厚度;负值表示绘制一个填充矩形。
lineType---椭圆边线类型。
shift---坐标中的小数位数。
注意:①椭圆的角度均遵循顺逆时针规则,x轴正半轴为起始轴,顺时针为正,逆时针为负。②先绘制正常的椭圆,然后再根据参数angle以center为旋转中心旋转椭圆。
import cv2
# 绘制一个椭圆
girl = cv2.imread('image\\girl.jpg')
print(girl.shape)
girl_ellipse = girl.copy()
cv2.ellipse(girl_ellipse, (380, 300), (200, 100), 45, 180, 360, (255, 255, 255), thickness=2)
cv2.ellipse(girl_ellipse, (380, 300), (200, 100), 45, 0, 180, (0, 0, 255), thickness=2)
cv2.imshow('girl_ellipse', girl_ellipse)
cv2.waitKey()
cv2.destroyAllWindows()
运行结果如下:
语法:cv2.putText(image, text, org, fontFace, fontScale, color[, thickness[, lineType[, bottomLeftOrigin]]])--->image
参数:
image---图像,通常是源图像的副本。
text---文本字符串。
org---text的左下角的位置。
fontFace---字体类型。
fontScale---字体的比例(用于乘以特定字体的基大小)。
color---字体颜色。
thickness---文本字体线条的粗细
lineType---字体边线类型。
bottomLeftOrigin---如果为 true,则图像数据原点位于左下角。否则,它位于左上角。
# 添加文本
import cv2
girl = cv2.imread('image\\girl.jpg')
print(girl.shape)
girl_putText = girl.copy()
text = 'Finally, let’s try annotating images with text. To do this, use the function in OpenCV'
cv2.putText(girl_putText, text, (50, 400), cv2.QT_FONT_BLACK, 0.85, (255, 255, 255))
cv2.imshow('1111', girl_putText)
cv2.waitKey()
cv2.destroyAllWindows()
运行结果如下:
鼠标回调函数和跟踪栏可以达到如下动图的效果:
鼠标指针是图形用户界面(GUI)中的关键组件,没有鼠标指针就无法真正做到图形界面的人机交互。opencv中与鼠标指针相关的有两个重要的函数,即鼠标回调函数和创建跟踪栏。
我们通过实现鼠标标注图像这一功能来了解鼠标回调函数。
首先,定义一个特殊的“回调”函数,该函数将在显示窗口中显示的图像上绘制一个矩形。仅在计算机检测到某些用户界面事件时调用的函数,被称为“回调”函数。由于这个函数与鼠标相关联,因此一般命名为MouseCallback,不过你也可以自定义。这个函数在调用时无需输入任何参数,这是因为在用户与鼠标进行交互时,所需参数会自动填充。
然后,将“回调”函数与特定的鼠标事件相关联。每当调用此函数(用户和鼠标交互)时,位于显示图像上的鼠标会记录鼠标x轴、y轴的坐标,并记录鼠标的事件类型和事件标志。对于本例,当触发动作cv2.LEFTBUTTONDOWN
和 cv2.LEFTBUTTONUP
时,我们将坐标存储在各自的变量中,并使用它们来绘制矩形。
①怎么定义鼠标回调函数?
语法:def MouseCallback(event, x, y, flags, *userdata)
参数:
event---鼠标事件。
x, y---鼠标的x轴和y轴的坐标。
flags---事件标志。
*userdata---其他的输入数据。
注意:定义回调函数时,函数名是可变的。
②怎么调用鼠标回调函数?
使用cv2.setMouseCallback()
函数来调用鼠标回调函数。
语法:cv2.setMouseCallback(winname, onMouse[, userdata])
参数:
winname---窗口名称(必须是已经存在的窗口)。
onMouse---鼠标回调函数名。
userdata:传递给回调函数的可选参数。
③怎么使显示窗口和用户发生交互?
利用while循环和cv2.waitKey()函数指定键盘字符发生交互。具体而言:
- 创建一个持续显示图像的 while 循环,直到用户按 'q' 键(ASCII 代码:113)退出应用程序脚本。
- 在循环中为用户提供清除所有先前批注的功能。用户可以将命名窗口中的图像重置为我们在原始图像中读取时所做的副本。这可以通过检查键盘条目“c”(ASCII 代码:99)来实现。
- 当用户退出循环时,我们使用
cv2.destroyAllWindows()
销毁窗口。
④代码的具体实现。
# 定义一个绘制矩形的回调函数
import cv2
# 初始化参数
top_left_corner, bottom_right_corner = [], []
# flags,*userdata虽然没有调用,但不可缺少
def drawRectangle(event, x, y, flags, *userdata):
global top_left_corner, bottom_right_corner
if event == cv2.EVENT_LBUTTONDOWN:
top_left_corner = [(x, y)]
elif event == cv2.EVENT_LBUTTONUP:
bottom_right_corner = [(x, y)]
cv2.rectangle(image, top_left_corner[0], bottom_right_corner[0], (0, 255, 0), 2, 8)
cv2.imshow('Window', image)
image = cv2.imread('image\\cat.jpg')
temp = image.copy()
cv2.namedWindow('Window')
cv2.setMouseCallback('Window', drawRectangle)
k = 0
# 键击‘q’退出程序,键击‘c’重新绘制矩形
while k != 113:
cv2.imshow("Window", image)
k = cv2.waitKey(0)
if k == 99:
image = temp.copy()
cv2.imshow("Window", image)
cv2.destroyAllWindows()
运行过程截图:
跟踪栏回调函数与鼠标回调函数类似。
①怎么定义跟踪栏回调函数?
语法:def TrackbarCallback(*args)
参数:*args---跟踪栏参数。args[0]可以检索跟踪栏条目位置,这是一个与用户发生交互的参数,其范围是(0, count]内的整数。
注意:定义回调函数时,函数名是可变的。
②怎么调用跟踪栏回调函数?
使用cv2.createTrackbar()
函数来调用跟踪栏回调函数。
语法:cv2.createTrackbar(trackbarName, winname, value, count, onChange[, userdata])
参数:
trackbarName---创建的跟踪栏名称。
winname---显示窗口的名称。
value---滑块的默认位置。
count---滑块可滑到的最大位置。
onChange---回调函数名。
userdata---传递给回调函数的用户数据。它可用于处理跟踪栏事件,而无需使用全局变量。
③代码的具体实现。
# 跟踪栏控制图像大小
import cv2
# 初始化参数
maxScaleUp = 100
scaleFactor = 1
windowName = "Resize Image"
trackbarValue = "Scale"
# 创建回调函数,图像可以在[1, 2]之间缩放
def scaleImage(*args):
scaleFactor = 1 + args[0]
scaledImage = cv2.resize(image, None, fx=scaleFactor, fy=scaleFactor, interpolation=cv2.INTER_LINEAR)
cv2.imshow(windowName, scaledImage)
image = cv2.imread("image\\jellyfish.jpg")
cv2.namedWindow(windowName, cv2.WINDOW_AUTOSIZE)
# 调用跟踪栏回调函数
cv2.createTrackbar(trackbarValue, windowName, scaleFactor, maxScaleUp, scaleImage)
cv2.imshow(windowName, image)
c = cv2.waitKey(0)
cv2.destroyAllWindows()
运行过程截图:
opencv使用卷积内核模糊(平滑)、锐化图像,也就是过滤图像。本节涉及到了卷积,建议对卷积有个基本了解,可参考:如何通俗易懂地解释卷积? - 知乎
卷积核也被称为卷积矩阵,是用于过滤图像的二维矩阵,它通常是一个N阶方阵,其中N是奇数。卷积核的作用:使用卷积核对图像的每个像素执行数学运算,可以达到模糊(平滑)或锐化图像的目的。
卷积计算
前面已经提到,图像就是一个三维数组,当去掉通道维度时它就是一个矩阵,可用于参与矩阵的运算。比如,下图的噪点较多,此时我们可以使用卷积操作将图像进行平滑处理。具体过程如下:
①用矩阵表示图像;
②设置卷积核(卷积矩阵),做归一化处理(不改变图像的亮度)。(需要注意的是,图像处理中一般用的是正态分布矩阵,这里为了简单,就用了算术平均矩阵)
③卷积计算。若要平滑a1,1点,就在矩阵中,取出a1,1 点附近的点组成矩阵 f(形状与卷积矩阵相同) ,和卷积核进行卷积计算后,再填回去:
下面用一个动图诠释计算过程,
④写成一个公式:
将卷积核应用于图像
这里的仅简单介绍cv2.filter()
函数。
语法:cv2.filter2D(src, ddepth, kernel)--->dst
参数:
src---源图像。
ddepth---目标图像的深度值。-1,表示目标图像与源图像的深度相同。
kernel---内核,也就是卷积核。矩阵。
注意:array = np.array[[0, 0, 0], [0, 1, 0], [0, 0, 0]],是一个恒等核,根据上述卷积计算可知经过恒等核过滤后的目标图像与源图像是一样的(外观上是一样的)。
# 恒等核过滤后的图像
import cv2
import numpy as np
car = cv2.imread('image\\car.jpg')
# 创建恒等核
kernel1 = np.array([[0, 0, 0],
[0, 1, 0],
[0, 0, 0]])
# 使用filter2D()函数执行线性滤波操作
smooth_car = cv2.filter2D(car, ddepth=-1, kernel=kernel1)
combine = np.hstack((car, smooth_car))
# 显示原始图像和经过恒等核过滤后的图像
cv2.imshow('original image on the left, smooth image on the right', combine)
cv2.waitKey(0)
cv2.imwrite('image\\smooth_car.jpg', smooth_car)
cv2.destroyAllWindows()
运行结果如下:
# 算术平方根核过滤后的图像
import cv2
import numpy as np
car = cv2.imread('image\\car.jpg')
# 创建算术平方根核
kernel1 = np.ones((3, 3), 'f4')/9
# 使用filter2D()函数执行线性滤波操作
smooth_car = cv2.filter2D(car, ddepth=-1, kernel=kernel1)
combine = np.hstack((car, smooth_car))
# 显示原始图像和经过恒等核过滤后的图像
cv2.imshow('original image on the left, smooth image on the right', combine)
cv2.waitKey(0)
cv2.destroyAllWindows()
运行结果如下:
语法:cv2.blur(src, ksize[, dst[, anchor[, borderType]]])--->dst
参数:
src---源图像。
ksize---模糊函数的内核尺寸,元组类型参数。
dst---目标图像,在python中无任何意义,一般不传参或设成None。
borderType---制作图形边界。
注意:该函数与使用算术平方根核模糊的图像结果完全相同。
import cv2
import numpy as np
car = cv2.imread('image\\car.jpg')
smooth_car = cv2.blur(car, ksize=(3, 3))
combine = np.hstack((car, smooth_car))
cv2.imshow('original image on the left, smooth image on the right', combine)
cv2.waitKey(0)
cv2.destroyAllWindows()
运行结果如下:
高斯模糊使用了高斯滤波器(即使用高斯(又称正太)分布作为滤波函数),该滤波器执行加权平均值,根据像素值与内核中心的距离对像素值进行加权,离中心较远的像素对加权平均值的影响较小。
语法:cv2.GaussianBlur(src, ksize, sigmaX[, dst[, sigmaY[, borderType]]])--->dst
参数:
src---源图像。
ksize---高斯核的尺寸,元组类型参数,一般设置为奇数方阵。
sigmaX---X方向上的高斯核标准差。
dst---目标图像,在python中无任何意义,一般不传参或设成None。
sigmaY---Y方向上的高斯核标准差。sigmaY=0
,表示sigmaY=sigmaX
;sigmaY=0
,sigmaX=0
,表示X和Y方向的高斯核标准差需要根据ksize.width
、ksize.height
计算;为了完全控制结果,建议指定ksize、sigmaX和sigmaY的值。
borderType---制作图形边界。
import cv2
import numpy as np
car = cv2.imread('image\\car.jpg')
gaussian_blur_car = cv2.GaussianBlur(car, (3, 3), sigmaX=1, sigmaY=0)
combine = np.hstack((car, gaussian_blur_car))
cv2.imshow('original image on the left, smooth image on the right', combine)
cv2.waitKey(0)
cv2.imwrite('image\\gaussian_blur_car.jpg', gaussian_blur_car)
cv2.destroyAllWindows()
运行结果如下:
中值模糊是在源图像中的每个像素都替换为内核区域中图像像素的中值。
语法:cv2.medianBlur(src, ksize[, dst])--->dst
参数:src---源图像。
ksize---内核的大小,其必须是大于1的奇数。
注意:对于相同的内核大小,中值模糊的效果比高斯模糊更突出。
import cv2
import numpy as np
car = cv2.imread('image\\car.jpg')
median_blur_car = cv2.medianBlur(car, 3)
combine = np.hstack((car, median_blur_car))
cv2.imshow('original image on the left, smooth image on the right', combine)
cv2.waitKey(0)
cv2.destroyAllWindows()
运行结果如下:
我们也可以利用卷积核锐化图像,其实现依赖于cv2.filter2D()
函数。
import cv2
import numpy as np
car = cv2.imread('image\\car.jpg')
# 创建锐化内核(归一化处理,保持亮度不变)
kernel1 = np.array([[0, -1, 0],
[-1, 3, 0],
[0, 0, 0]])
# 使用filter2D()函数执行线性滤波操作
sharp_car = cv2.filter2D(car, ddepth=-1, kernel=kernel1)
combine = np.hstack((car, sharp_car))
# 显示原始图像和经过自定义内核锐化后的图像
cv2.imshow('original image on the left, smooth image on the right', combine)
cv2.waitKey(0)
cv2.imwrite('image\\sharp_car.jpg', sharp_car)
cv2.destroyAllWindows()
运行结果如下:
通常,单一使用图像模糊或锐化是达不到预期效果的,此时我们可以选择使用双边滤波方法让二者共同作用在图像中。双边滤波本质上是对图像应用2D高斯模糊,且考虑了相邻像素强度的变化(用以降低边缘区域的高斯权重)。
让我们先来探讨下双边滤波的工作过程。假如我们正在过滤图像中靠近边缘的区域,普通的高斯模糊会根据实际权重模糊边缘,但是双边滤波器可以通过感知边缘(通过像素强度的差异)主动降低边缘区域的权重,从而减少高斯模糊对边缘的影响。因此,像素强度更均匀的区域更加模糊,边缘区域几乎无模糊。
图像中的过滤像素的最终值是由空间和强度权重决定,因此
- 相似且接近过滤像素的像素将产生不同程度的影响(根据高斯权重);
- 远离过滤像素的像素影响较小(由于高斯权重);
- 过滤像素附近的像素强度差异越大影响越小,即使很靠近过滤像素。
opencv中利用cv2.bilateraFilter()
函数实现双边过滤。
语法:cv2.bilateralFilter(src, d, sigmaColor, sigmaSpace[, dst[, borderType]])--->dst
参数:
src---源图像。
d---定义用于过滤的像素邻域的直径。
sigmaColor---颜色(像素)强度标准差,定义一维高斯分布,该分布指定允许的像素强度变化的程度。
sigmaSpace---x和y轴组成的空间的标准差。
borderType---制作图形边界。
# 双边滤波
import cv2
import numpy as np
car = cv2.imread('image\\car.jpg')
bilateral_filter_car = cv2.bilateralFilter(car, d=5, sigmaColor=40, sigmaSpace=50)
combine = np.hstack((car, bilateral_filter_car))
# 显示原始图像和经过自定义内核锐化后的图像
cv2.imshow('original image on the left, smooth image on the right', combine)
cv2.waitKey(0)
cv2.destroyAllWindows()
运行结果如下:
阈值又叫临界值,是指一个效应能够产生的最低值或最高值。图像的阈值是指灰度阈值,是针对灰度图像的。
阈值处理是指剔除图像内像素值高于阈值或低于阈值的像素点。利用图像阈值可以实现图像分割,即阈值分割法。根据阈值分割规则的作用区域可分为全局阈值分割和局部阈值分割两种。全局阈值分割方法包括直方图计数法、熵算法、Otsu算法;局部阈值分割是指自适应阈值法。
语法:cv2.threshold(src, thresh, maxValue, type[, dst])--->retval, dst
参数:
src---源图像。thresh---阈值。
maxValue---与cv2.THRESH_BINARY
和cv2.THRESH_BINARY_INV
阈值类型一起使用的最大值。
type---阈值类型。cv2.THRESH_BINARY
、cv2.THRESH_BINARY_INY
、cv2.THRESH_TRUNC
、cv2.THRESH_TOZERO
、cv2.THRESH_TOZERO_INV
、cv2.THRESH_MASK
、cv2.THRESH_OTSU
、cv2.THRESH_TRIANGLE
。
返回值:retval---阈值。dst---目标图像。
这里只简单的演示使用cv2.THRESH_BINARY
阈值类型的示例,仅作为入门了解。
# RESH_BINARY阈值类型示例
import cv2
import numpy as np
# 需要对图像做预处理——转化为灰度图
src = cv2.imread('image\\cat.jpg', cv2.IMREAD_GRAYSCALE)
retval, dst = cv2.threshold(src, 100, 255, cv2.THRESH_BINARY)
print(retval)
combine = np.hstack((src, dst))
cv2.imshow('original image on the left, smooth image on the right', combine)
cv2.waitKey()
cv2.destroyAllWindows()
运行结果如下:
Blob,译文是“(颜色)的一小片,斑点”。它是图像中拥有共同属性(例如灰度值)的连通域,上图中的Blob就是灰色的连通域。Blob检测就是为了识别并标记这些连通域。
就上图而言,使用Blob检测可以达到如下效果:
Blob检测是通过cv2.SimpleBlobDeterctor
算子实现的。
filterByColor=1
(或True)。其次设置颜色倾向,blobColor=0
以选择较暗的blob,blobColor=255
以选择较浅的区域。filterByArea=1
(或True)。其次设置minArea和maxArea的适当值。例如,设置minArea=50,maxArea=234,将滤除所有少于50和多于234个像素点的blob。filterByCircularity=1
(或True),然后设置适当的minCircularity
和maxCircularity
。filterByConvexity=1
(或True),然后设置适当的minConvexity
和maxConvexity
(两值在[0, 1]范围)。filterByInertia=1
(或True),然后设置适当的minInertiRatio
和maxInertiRatio
(两值在[0, 1]范围)。cv2.SimpleBlobDeterctor
参数# 导入库
import cv2
import numpy as np
# 读取图像
im = cv2.imread("image\\blob.jpg", cv2.IMREAD_GRAYSCALE)
# 设置SimpleBlobDetector参数
params = cv2.SimpleBlobDetector_Params()
# 改变阈值
params.minThreshold = 10
params.maxThreshold = 200
# 根据面积过滤
params.filterByArea = True
params.minArea = 1500
# 根据Circularity过滤
params.filterByCircularity = True
params.minCircularity = 0.1
# 根据Convexity过滤
params.filterByConvexity = True
params.minConvexity = 0.87
# 根据Inertia过滤
params.filterByInertia = True
params.minInertiaRatio = 0.01
# 创建一个带有参数的检测器
# opencv3.0前的版本使用cv2.SimpleBlobDetector()创建检测器
# 3.0之后的版本使用cv2.SimpleBlobDetector_create()创建
ver = cv2.__version__.split('.')
if int(ver[0]) < 3:
detector = cv2.SimpleBlobDetector(params)
else:
detector = cv2.SimpleBlobDetector_create(params)
# 检测blobs
keypoints = detector.detect(im)
# 用红色圆圈画出检测到的blobs
# cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS 确保圆的大小对应于blob的大小
im_with_keypoints = cv2.drawKeypoints(im, keypoints, np.array([]), (0, 0, 255),
cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS)
# 结果显示
cv2.imshow("Keypoints", im_with_keypoints)
cv2.waitKey(0)
运行结果如下:
这里需要补充一个函数,cv2.drawKeypoints()
,即绘制特征关键点函数。
语法:cv2.drawKeypoints(image, keypoints, outImage[, color[, flags]])-->outImage
参数:
image---源图像。
keypoints---特征点向量,向量内每个元素是一个keypoint对象,它包含了特征点的各种属性信息。
outImage---特征点绘制的画布对象,可以是原始图像。
color---绘制的特征点的颜色信息,默认绘制的是随机彩色。
flags---特征点的绘制模式,有以下几种模式可选。
①DRAW_MATCHES_FLAGS_DEFAULT
,只绘制特征点的坐标点,显示在图像上就是一个个小圆点,每个圆点的坐标都是特征点的坐标。②DRAW_MATCHES_FLAGS_DRAW_OVER_OUTIMG
,函数不创建输出的图像,而是直接在输出图像变量空间绘制,要求本身输出图像变量就是一个初始化好了的,size与type都是已经初始化好的变量。③DRAW_MATCHES_FLAGS_NOT_DRAW_SINGLE_POINTS
,单点的特征点不被绘制。④DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS
,绘制特征点的时候绘制的是一个个带有方向的圆,这种方法同时显示图像的坐标,size和方向,是最能显示特征的一种绘制方式。
边缘检测是一种重要的图像处理技术,用于确定图片中物体的边界(边缘)或区域。边缘检测算法广泛应用于生产工作中的计算机视觉算法处理流水线中。
在进行检测前需要进行预处理,①边缘检测主要是依靠图像的像素强度来判别是否为边缘,因此需要将图像转为灰度图。②在对图像进行边缘检测之前需要进行平滑处理,以减少图像中的噪点。这是因为在边缘检测中,需要计算像素强度的数值导数,而噪点会产生不必要的干扰。
这里我们介绍两种重要的边缘检测算法:Sobel边缘检测和Canny边缘检测。
关于Sobel检测的原理,可参看Edge Detection Using OpenCV | LearnOpenCV #
语法:cv2.Sobel(src, ddepth, dx, dy[, dst[, ksize[, scale[, delta[, borderType]]]]])--->dst
参数:
src---源图像。ddepth---输出图像的位深度。
dx---x方向的求导阶数。dy---y方向的求导阶数。
ksize---Sobel卷积核的尺寸,必须是奇数。
import cv2
image = cv2.imread('image\\tiger.png', cv2.IMREAD_GRAYSCALE)
# 平滑图像
image_blur = cv2.GaussianBlur(image, (3, 3), sigmaX=1, sigmaY=0)
# Sobel算子1,dx=1,dy=0
sobel_x = cv2.Sobel(image_blur, cv2.CV_64F, dx=1, dy=0, ksize=3)
# Sobel算子2,dx=0,dy=1
sobel_y = cv2.Sobel(image_blur, cv2.CV_64F, dx=0, dy=1, ksize=3)
# Sobel算子3,dx=1,dy=1
sobel_xy = cv2.Sobel(image_blur, cv2.CV_64F, dx=1, dy=1, ksize=3)
cv2.imshow('Sobel X', cv2.resize(sobel_x, None, fx=0.6, fy=0.6))
cv2.waitKey(0)
cv2.imshow('Sobel Y', cv2.resize(sobel_y, None, fx=0.6, fy=0.6))
cv2.waitKey(0)
cv2.imshow('Sobel XY', cv2.resize(sobel_xy, None, fx=0.6, fy=0.6))
cv2.waitKey(0)
cv2.destroyAllWindows()
运行结果如下:
关于Canny检测的原理,可参看Edge Detection Using OpenCV | LearnOpenCV #
语法:cv2.Canny(image, threshold1, threshold2[, edges[, apertureSie[, L2gradient]]]])--->dst
参数:
image---8位的图像。
threshold1---阈值1,强度小于此阈值的像素将被排除。
threshold2---阈值2,强度大于此阈值的像素将于实体边相关联。
import cv2
image = cv2.imread('image\\tiger.png', 0)
# 平滑图像
image_blur = cv2.GaussianBlur(image, (3, 3), sigmaX=1, sigmaY=0)
edges = cv2.Canny(image_blur, 75, 200)
cv2.imshow('Canny Edge Detection', edges)
cv2.waitKey(0)
cv2.destroyAllWindows()
运行结果如下:
轮廓检测可以检测物体的边界,并在图像中轻松定位它们。它可以被应用到图像前景提取、简单图像分割、检测和识别等工作中。
等高线:指具有相同颜色和强度的边界像素点连接而成的闭合线路,也就是轮廓。
在opencv中,我们可以使用cv2.findContours()
和cv2.drawContours()
在图像中查找和绘制轮廓。在查找轮廓时,常用两种检测算法:cv2.CHAIN_APPROX_NONE
、cv2.CHAIN_APPROX_SIMPLE
。
cv2.findContours()
函数检测图像轮廓。cv2.drawContours()
函数绘制图像轮廓。首先我们先来了解两个函数,cv2.findContours()
和cv2.drawContours()
。
cv2.findContours()
语法:cv2.findContours(image, mode, method[, contours[, hierarchy[, offset]]])--->contours, hierarchy
参数:
image---二值图像。
mode---轮廓检索模式。包括cv2.RETR_EXTERNAL
、cv2.RETR_LIST
、cv2.RETR_CCOMP
、cv2.RETR_TREE
。示例中,我们将使用cv2.RETR_TREE
,这意味着将从二值图像中检索所有可能的轮廓。
method---轮廓近似方法。常用cv2.CHAIN_APPROX_NONE
和cv2.CHAIN_APPROX_SIMPLE
。cv2.CHAIN_APPROX_NONE
存储所有的等高点;cv2.CHAIN_APPROX_SIMPLE
压缩水平、垂直和对角线段,仅保留其端点。例如,举行轮廓使用4个角点进行编码。
hierarchy---层次结构。
offset---偏移。
返回:contours---轮廓线。hierarchy---层次结构。
cv2.drawContours()
语法:cv2.drawContours(image, contours, contourIdx, color[, thickness[, lineType[, hierarchy[, maxLevel[, offset]]]]])--->image
参数:
image---要在其上绘制轮廓的图像。
contours---从cv2.findContours()函数获得的轮廓线。
contoursIdx---等值线点的像素坐标列在获得的等值线中。使用此参数,可以指定此列表中的索引位置,以准确指示要绘制的轮廓点。提供负值将绘制所有等值线点。
color---要绘制的轮廓线的颜色。
thickness---要绘制的轮廓线的粗细,若为负数则填充轮廓线内部。
import cv2
apple = cv2.imread('image\\apple.jpg')
image = cv2.cvtColor(apple, cv2.COLOR_BGR2GRAY)
image_copy = apple.copy()
retval, dst = cv2.threshold(image, 100, 255, cv2.THRESH_BINARY)
contours, hierarchy = cv2.findContours(dst, cv2.RETR_TREE, cv2.CHAIN_APPROX_NONE)
cv2.drawContours(image=image_copy, contours=contours, contourIdx=-1, color=(0, 255, 0), thickness=2)
cv2.imshow('111', image_copy)
cv2.waitKey(0)
cv2.destroyAllWindows()
运行结果如下:
import cv2
apple = cv2.imread('image\\apple.jpg')
image = cv2.cvtColor(apple, cv2.COLOR_BGR2GRAY)
image_copy = apple.copy()
retval, dst = cv2.threshold(image, 100, 255, cv2.THRESH_BINARY)
contours, hierarchy = cv2.findContours(dst, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
cv2.drawContours(image=image_copy, contours=contours, contourIdx=-1, color=(0, 255, 0), thickness=2)
cv2.imshow('111', image_copy)
cv2.waitKey(0)
cv2.destroyAllWindows()
运行结果如下:
可以看到cv2.CHAIN_APPROX_NONE
和cv2.CHAIN_APPROX_SIMPLE
的输出之间几乎没有差异。这是为什么呢?
这归功于 cv2.drawContours()
函数。尽管 cv2.CHAIN_APPROX_SIMPLE
方法通常生成的点较少,但 cv2.drawContours()
函数会自动连接相邻的点,即使它们不在轮廓列表中,也会连接它们。
层次结构(hierarchy)表示等高线(轮廓线)之间的父子关系,在轮廓检测中轮廓检索模式mode对生成的轮廓层次结构产生影响。为什么要引入层次结构概念呢?这是因为,我们既需要描绘图像中单个对象的轮廓,也需要描绘对象和其内部的轮廓。
通过上图中的几个形状和线条我们可以很好的了解层次结构,不同的数字显示了不同形状之间的层次结构。
cv2.findContours()函数返回的hierarchy(层次结构)是一个三维数组,该数组中的每个层次结构包含了四个值,分别是Next, Previous, First_Child, Parent。
Next---表示当前轮廓的下一个处于同一层次结构级别的轮廓索引。上图中,等高线1与2的结构级别处于同一层次结构级别上,因此1 Next---> 2;没有与等高线3处于同一层次结构级别的等高线,因此3 Next---> -1。
Previous---表示当前轮廓的上一个处于同一层次结构级别的轮廓索引。
First_Child---表示当前轮廓的第一个子轮廓索引。上图中等高线3的子轮廓是3a,因此3 First_Child---> 3a。
Parent---表示当前轮廓的父等高线的索引。
不同轮廓检索模式下的轮廓层次结构和轮廓线:
mode = cv2.RETR_LIST
RETR_LIST模式不会在提取的轮廓之间创建任何父子关系,因此,对于检测到的所有等高线的First_Child与Parent的值都是-1。
import cv2
image2 = cv2.imread('image\\shape.jpg')
image = cv2.cvtColor(image2, cv2.COLOR_BGR2GRAY)
retval, thresh2 = cv2.threshold(image, 150, 255, cv2.THRESH_BINARY)
contours5, hierarchy5 = cv2.findContours(thresh2, cv2.RETR_LIST, cv2.CHAIN_APPROX_NONE)
image_copy6 = image2.copy()
cv2.drawContours(image_copy6, contours5, -1, (0, 255, 0), 2, cv2.LINE_AA)
cv2.imshow('RETR_LIST', image_copy6)
print(f"RETR_LIST: \n{hierarchy5}")
cv2.waitKey(0)
cv2.imwrite('image\\contours_retr_list.jpg_LIST.jpg', image_copy6)
cv2.destroyAllWindows()
'''
RETR_LIST:
[[[ 1 -1 -1 -1]
[ 2 0 -1 -1]
[ 3 1 -1 -1]
[ 4 2 -1 -1]
[-1 3 -1 -1]]]
'''
生成图如下:
mode = cv2.RETR_EXTERNAL
RETR_EXTERNAL模式仅检测父轮廓,并忽略任何子轮廓,因此所有的内部轮廓(如 3a 和 4)都不会被绘制。
import cv2
image2 = cv2.imread('image\\shape.jpg')
image = cv2.cvtColor(image2, cv2.COLOR_BGR2GRAY)
retval, thresh2 = cv2.threshold(image, 150, 255, cv2.THRESH_BINARY)
contours5, hierarchy5 = cv2.findContours(thresh2, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
image_copy6 = image2.copy()
cv2.drawContours(image_copy6, contours5, -1, (0, 255, 0), 2, cv2.LINE_AA)
cv2.imshow('RETR_EXTERNAL', image_copy6)
print(f"RETR_EXTERNAL: \n{hierarchy5}")
cv2.waitKey(0)
cv2.imwrite('image\\contours_retr_external.jpg', image_copy6)
cv2.destroyAllWindows()
'''
RETR_EXTERNAL:
[[[ 1 -1 -1 -1]
[ 2 0 -1 -1]
[-1 1 -1 -1]]]
'''
生成图如下:
mode = cv2.RETR_CCOMP
RETR_CCOMP模式建立了两级层次结构,即轮廓线不是第一级层次结构就是第二级层次结构;若有嵌套的轮廓线,则2N+1(N是非负整数)是第一级层次结构。如下图所示,
import cv2
image2 = cv2.imread('image\\shape.jpg')
image = cv2.cvtColor(image2, cv2.COLOR_BGR2GRAY)
retval, thresh2 = cv2.threshold(image, 150, 255, cv2.THRESH_BINARY)
contours5, hierarchy5 = cv2.findContours(thresh2, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_NONE)
image_copy6 = image2.copy()
cv2.drawContours(image_copy6, contours5, -1, (0, 255, 0), 2, cv2.LINE_AA)
cv2.imshow('RETR_CCOMP', image_copy6)
print(f"RETR_CCOMP: \n{hierarchy5}")
cv2.waitKey(0)
cv2.imwrite('image\\contours_retr_ccomp.jpg', image_copy6)
cv2.destroyAllWindows()
'''
RETR_CCOMP:
[[[ 1 -1 -1 -1]
[ 3 0 2 -1]
[-1 -1 -1 1]
[ 4 1 -1 -1]
[-1 3 -1 -1]]]
'''
生成图如下:
mode = cv2.RETR_TREE
RETR_TREE模式创建完整的层次结构,级别不限于两级,其视情况而定。
import cv2
image2 = cv2.imread('image\\shape.jpg')
image = cv2.cvtColor(image2, cv2.COLOR_BGR2GRAY)
retval, thresh2 = cv2.threshold(image, 150, 255, cv2.THRESH_BINARY)
contours5, hierarchy5 = cv2.findContours(thresh2, cv2.RETR_TREE, cv2.CHAIN_APPROX_NONE)
image_copy6 = image2.copy()
cv2.drawContours(image_copy6, contours5, -1, (0, 255, 0), 2, cv2.LINE_AA)
cv2.imshow('RETR_TREE', image_copy6)
print(f"RETR_TREE: \n{hierarchy5}")
cv2.waitKey(0)
cv2.imwrite('image\\contours_retr_tree.jpg', image_copy6)
cv2.destroyAllWindows()
'''
RETR_TREE:
[[[ 3 -1 1 -1]
[-1 -1 2 0]
[-1 -1 -1 1]
[ 4 0 -1 -1]
[-1 3 -1 -1]]]
'''
生成图如下:
不同轮廓检索方式的运行时间比较:
轮廓检索方式 |
运行时间 (s) |
cv2.RETR_LIST |
0.000382 |
cv2.RETR_EXTERNAL |
0.000554 |
cv2.RETR_CCOMP |
0.001845 |
cv2.RETR_TREE |
0.005594 |