CV学习:OpenCv快速入门(python版)

2023-11-16

本文代码全部可运行,笔者运行环境: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. 参考内容


1. 图像的读取、显示与写入

图像的读取、显示与写入分别对应三个函数,cv2. imread()cv2.imshow()cv2.imwrite()

1.1. 读取图像

语法:cv2.imread(filename[, flags])--->image

参数:

filename---文件路径(相对路径和绝对路径),路径中不要带有中文。

flags---可选标志,用于指定读取图像的样式,常见的有cv2.IMREAD_UNCHANGED(-1)cv2.IMREAD_GRAYSCALE(0)cv2.IMREAD_COLOR(1)。默认为1。

注意:opencv读取彩色图像的格式是BGR,而大多数视觉库使用的是RGB,因此当将 OpenCV 与其他工具包一起使用时,当从一个库切换到另一个库时,不要忘记交换蓝色和红色通道。

1.2. 显示图像

语法: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()

运行结果:

1.3. 写入图像

语法: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()

运行结果如下:

2. 图像的色彩空间

常用的图像色彩空间有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_heightpixel_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

运行结果如下(静态展示):

3. 图像编辑

图像编辑主要包括调整图像大小、裁剪图像、图像的旋转和移动、翻转,透视变换。

3.1. 调整大小

在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_AREAcv2.INTER_CUBICcv2.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()

运行结果(部分)如下:

3.2. 图像裁剪

裁剪图像是为了从图像中删除不需要的对象或区域;也就是删除其他,保留想要的图像区域。

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()

运行结果如下:

3.3. 旋转和平移

图像的旋转和平移是图像编辑中最基本的操作之一。旋转和平移都属于同一种图像编辑类型,即放射变换(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()

运行结果如下:

3.4. 图像翻转

图像翻转,也就是通常所说的镜像。主要包括垂直翻转、水平翻转、水平垂直翻转。

语法: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()

运行结果:

4. 图像注释

注释图像即是在图像添加注释性信息,通常是绘制图形标出对象或者添加文字信息。需要指出的是在进行注释图像前一定要使用copy()方法创建副本,以确保源图像的完整性。

4.1. 绘制直线

语法: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()

运行结果如下:

4.2. 绘制圆形

语法: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()

运行结果如下:

4.3. 绘制矩形

语法: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()

运行结果如下:

4.4. 绘制椭圆

语法: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()

运行结果如下:

4.5. 添加文本

语法: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()

运行结果如下:

5. 鼠标回调函数和跟踪栏回调函数

鼠标回调函数和跟踪栏可以达到如下动图的效果:

鼠标指针是图形用户界面(GUI)中的关键组件,没有鼠标指针就无法真正做到图形界面的人机交互。opencv中与鼠标指针相关的有两个重要的函数,即鼠标回调函数和创建跟踪栏。

5.1. 鼠标回调函数

我们通过实现鼠标标注图像这一功能来了解鼠标回调函数。

首先,定义一个特殊的“回调”函数,该函数将在显示窗口中显示的图像上绘制一个矩形。仅在计算机检测到某些用户界面事件时调用的函数,被称为“回调”函数。由于这个函数与鼠标相关联,因此一般命名为MouseCallback,不过你也可以自定义。这个函数在调用时无需输入任何参数,这是因为在用户与鼠标进行交互时,所需参数会自动填充。

然后,将“回调”函数与特定的鼠标事件相关联。每当调用此函数(用户和鼠标交互)时,位于显示图像上的鼠标会记录鼠标x轴、y轴的坐标,并记录鼠标的事件类型和事件标志。对于本例,当触发动作cv2.LEFTBUTTONDOWNcv2.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()

运行过程截图:

5.2. 跟踪栏回调函数

跟踪栏回调函数与鼠标回调函数类似。

①怎么定义跟踪栏回调函数?

语法: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()

运行过程截图:

6. 图像过滤

opencv使用卷积内核模糊(平滑)、锐化图像,也就是过滤图像。本节涉及到了卷积,建议对卷积有个基本了解,可参考:如何通俗易懂地解释卷积? - 知乎

6.1. 卷积核及其应用

卷积核也被称为卷积矩阵,是用于过滤图像的二维矩阵,它通常是一个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()

运行结果如下:

6.2. 内置函数模糊图像

语法: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()

运行结果如下:

6.3. 高斯模糊

高斯模糊使用了高斯滤波器(即使用高斯(又称正太)分布作为滤波函数),该滤波器执行加权平均值,根据像素值与内核中心的距离对像素值进行加权,离中心较远的像素对加权平均值的影响较小。

语法:cv2.GaussianBlur(src, ksize, sigmaX[, dst[, sigmaY[, borderType]]])--->dst

参数:

src---源图像。

ksize---高斯核的尺寸,元组类型参数,一般设置为奇数方阵。

sigmaX---X方向上的高斯核标准差。

dst---目标图像,在python中无任何意义,一般不传参或设成None。

sigmaY---Y方向上的高斯核标准差。sigmaY=0,表示sigmaY=sigmaXsigmaY=0sigmaX=0,表示X和Y方向的高斯核标准差需要根据ksize.widthksize.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()

运行结果如下:

6.4. 中值模糊

中值模糊是在源图像中的每个像素都替换为内核区域中图像像素的中值。

语法: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()

运行结果如下:

6.5. 自定义内核锐化图像

我们也可以利用卷积核锐化图像,其实现依赖于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()

运行结果如下:

6.6. 图像的双边滤波

通常,单一使用图像模糊或锐化是达不到预期效果的,此时我们可以选择使用双边滤波方法让二者共同作用在图像中。双边滤波本质上是对图像应用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()

运行结果如下:

7. 图像阈值处理

阈值又叫临界值,是指一个效应能够产生的最低值或最高值。图像的阈值是指灰度阈值,是针对灰度图像的。

阈值处理是指剔除图像内像素值高于阈值或低于阈值的像素点。利用图像阈值可以实现图像分割,即阈值分割法。根据阈值分割规则的作用区域可分为全局阈值分割和局部阈值分割两种。全局阈值分割方法包括直方图计数法、熵算法、Otsu算法;局部阈值分割是指自适应阈值法。

语法:cv2.threshold(src, thresh, maxValue, type[, dst])--->retval, dst

参数:

src---源图像。thresh---阈值。

maxValue---与cv2.THRESH_BINARYcv2.THRESH_BINARY_INV阈值类型一起使用的最大值。

type---阈值类型。cv2.THRESH_BINARYcv2.THRESH_BINARY_INYcv2.THRESH_TRUNCcv2.THRESH_TOZEROcv2.THRESH_TOZERO_INVcv2.THRESH_MASKcv2.THRESH_OTSUcv2.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()

运行结果如下:

8. Blob检测

Blob,译文是“(颜色)的一小片,斑点”。它是图像中拥有共同属性(例如灰度值)的连通域,上图中的Blob就是灰色的连通域。Blob检测就是为了识别并标记这些连通域。

就上图而言,使用Blob检测可以达到如下效果:

Blob检测是通过cv2.SimpleBlobDeterctor算子实现的。

8.1. Blob检测是如何工作的?

  1. 阈值化(Thresholding):通过对源图像进行阈值化,将源图像转换为若干二值图像。阈值从minThreshold开始,以thresholdStep为间隔递增,直到maxThreshold。
  2. 分组(Grouping):对每份二值图像提取连通域。
  3. 合并(Merging):计算每个连通域的中心,并合并距离小于minDistBetweenBlobs的连通域。
  4. 返回(Returning):计算并返回 新合并的连通域的中心和半径。

8.2. 按颜色、大小和形状筛选Blob

  1. 按颜色:首先,需要明确需要颜色参数筛选Blob,即设置filterByColor=1(或True)。其次设置颜色倾向,blobColor=0以选择较暗的blob,blobColor=255以选择较浅的区域。
  2. 按大小:首先,需要明确需要大小参数筛选Blob,即设置filterByArea=1(或True)。其次设置minArea和maxArea的适当值。例如,设置minArea=50,maxArea=234,将滤除所有少于50和多于234个像素点的blob。
  3. 按形状:在opencv中形状由三个参数控制,圆度(Circularity)、凸度(Convexity)、惯性比(Inertia Ratio)。
    1. 圆度:Blob与圆的接近程度,其计算公式为

      例如正六边形的圆度大于正方形的。圆的圆度为1,正方形的为0.785。首先设置filterByCircularity=1(或True),然后设置适当的minCircularitymaxCircularity
    2. 凸度:Blob面积/凸包面积。首先设置filterByConvexity=1(或True),然后设置适当的minConvexitymaxConvexity(两值在[0, 1]范围)。
    3. 惯性比:对于一个圆,该值是1,对于椭圆它是0和1之间,而对于线是0。首先设置filterByInertia=1(或True),然后设置适当的minInertiRatiomaxInertiRatio(两值在[0, 1]范围)。

8.3. 设置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和方向,是最能显示特征的一种绘制方式。

9. 边缘检测

边缘检测是一种重要的图像处理技术,用于确定图片中物体的边界(边缘)或区域。边缘检测算法广泛应用于生产工作中的计算机视觉算法处理流水线中。

在进行检测前需要进行预处理,①边缘检测主要是依靠图像的像素强度来判别是否为边缘,因此需要将图像转为灰度图。②在对图像进行边缘检测之前需要进行平滑处理,以减少图像中的噪点。这是因为在边缘检测中,需要计算像素强度的数值导数,而噪点会产生不必要的干扰。

这里我们介绍两种重要的边缘检测算法:Sobel边缘检测和Canny边缘检测。

9.1. Sobel边缘检测

关于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()

运行结果如下:

9.2. Canny边缘检测

关于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()

运行结果如下:

10. 轮廓检测

轮廓检测可以检测物体的边界,并在图像中轻松定位它们。它可以被应用到图像前景提取、简单图像分割、检测和识别等工作中。

等高线:指具有相同颜色和强度的边界像素点连接而成的闭合线路,也就是轮廓。

在opencv中,我们可以使用cv2.findContours()cv2.drawContours()在图像中查找和绘制轮廓。在查找轮廓时,常用两种检测算法:cv2.CHAIN_APPROX_NONEcv2.CHAIN_APPROX_SIMPLE

10.1. 轮廓检测的步骤

  1. 读取图像并将其转换为灰度格式。这是由于轮廓检测是以阈值检测为基础的,而阈值又是针对灰度图而言的。
  2. 应用二值图像。利用阈值检测或Canny检测将灰度图转化为二值图,并将此二值图输入到查找轮廓算法中。
  3. 查找轮廓。使用cv2.findContours()函数检测图像轮廓。
  4. 绘制轮廓。使用cv2.drawContours()函数绘制图像轮廓。

10.2. 轮廓检测的实现

首先我们先来了解两个函数,cv2.findContours()cv2.drawContours()


cv2.findContours()

语法:cv2.findContours(image, mode, method[, contours[, hierarchy[, offset]]])--->contours, hierarchy

参数:

image---二值图像。

mode---轮廓检索模式。包括cv2.RETR_EXTERNALcv2.RETR_LISTcv2.RETR_CCOMPcv2.RETR_TREE。示例中,我们将使用cv2.RETR_TREE,这意味着将从二值图像中检索所有可能的轮廓。

method---轮廓近似方法。常用cv2.CHAIN_APPROX_NONEcv2.CHAIN_APPROX_SIMPLEcv2.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_NONEcv2.CHAIN_APPROX_SIMPLE的输出之间几乎没有差异。这是为什么呢?

这归功于 cv2.drawContours() 函数。尽管 cv2.CHAIN_APPROX_SIMPLE方法通常生成的点较少,但 cv2.drawContours() 函数会自动连接相邻的点,即使它们不在轮廓列表中,也会连接它们。

10.3. 轮廓的层次结构

层次结构(hierarchy)表示等高线(轮廓线)之间的父子关系,在轮廓检测中轮廓检索模式mode对生成的轮廓层次结构产生影响。为什么要引入层次结构概念呢?这是因为,我们既需要描绘图像中单个对象的轮廓,也需要描绘对象和其内部的轮廓。

通过上图中的几个形状和线条我们可以很好的了解层次结构,不同的数字显示了不同形状之间的层次结构。

  • 根据等高线层次结构和父子关系,所有单独的数字(即 1、2、3 和 4)都是单独的对象。
  • 我们可以说3a是3的孩子。请注意,3a 表示轮廓 3 的内部部分。
  • 等高线 1、2 和 4 都是父形状,没有任何关联的子形状,因此它们的编号是任意的。换句话说,轮廓 2 可以标记为 1,反之亦然。

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

  • cv2.RETR_LIST和cv2.RETR_EXTERNAL执行所需的时间最少,因为cv2.RETR_LIST没有定义任何层次结构,cv2.RETR_EXTERNAL只检索父轮廓
  • cv2.RETR_CCOMP执行时间第次之,它检索所有轮廓并定义两级层次结构。
  • cv2.RETR_TREE执行所需的时间最长,因为它会检索所有轮廓,并为每个父子关系定义独立的层次结构级别。

11. 参考内容

  1. Getting Started with OpenCV
  2. OpenCV: opencv2 Directory Reference
  3. 如何通俗易懂地解释卷积? - 知乎
  4. OpenCV基础(10)使用OpenCV进行Blob检测_opencv blob_求则得之,舍则失之的博客-CSDN博客
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

CV学习:OpenCv快速入门(python版) 的相关文章

随机推荐

  • C++实现——LCS-最大公共子串长度

    求两个字符串的最长公共子串的长度 子串不一定是原串中的连续子串组成 LCS 使用动态规划 include
  • Python基础知识及概念

    Python基础知识及概念 1 注释 单行注释 这是一个单行注释 在程序开发时 同样可以使用 在代码的后面 旁边 增加说明性的文字 但是 需要注意的是 为了保证代码的可读性 注释和代码之间 至少要有 两个空格 示例代码如下 print he
  • Vue-Quill-Editor 设置编辑器中文字的默认字体大小

    Vue Quill Editor 默认字体看起来有些小 如下 设置默认字体大小 ql container 设置默认字号 font size 16px 设置之后
  • 利用jsqlparser解析SQL语句

    时常会遇到很多情况 我们需要对SQL语句进行替换或者拼接 以往我们可能会用StringBuild来进行拼接 StringBuilder sql new StringBuilder sql append select from sql app
  • 开发框架Furion之Winform+SqlSugar

    目录 1 开发环境 2 项目搭建 2 1 创建WinFrom主项目 2 2 创建子项目 2 3 实体类库基础类信息配置 2 3 1 Nuget包及项目引用 2 3 2 实体基类创建 2 4 仓储业务类库基础配置 2 4 1 Nuget包及项
  • pytorch 人脸识别

    import torch import os import numpy as np import torch nn as nn import matplotlib pyplot as plt import time import torch
  • nim游戏 C++

    如果堆中石头的数量 nn 不能被 44 整除 那么你总是可以赢得 Nim 游戏的胜利 class Solution public bool canWinNim int n if n lt 0 return 0 else return n 4
  • 头插法和尾插法的详细区别

    浅析线性表 链表 的头插法和尾插法的区别及优缺点 线性表作为数据结构中比较重要的一种 具有操作效率高 内存利用率高 结构简单 使用方便等特点 今天我们一起交流一下单向线性表的头插法和尾插法的区别及优缺点 线性表因为每个元素都包含一个指向下一
  • IDE0006 加载项目时遇到了错误,已禁用了某些项目功能,例如用于失败项目和依赖于失败项目的其他项目的完整解决方案分析。

    重新打开vs2017就好了 原因猜测 vs来大姨妈了 现象是catch ex 后面是e message 单纯少个x vs没检测出来 辛辛苦苦搜个半天 可能太依赖vs了 懒人专属编辑器
  • npm私有化docker方式部署及使用说明

    一 部署nexus 本文采用docker方式部署nexus 安装docker yum install y docker 拉取nexus镜像 docker pull sonatype nexus3 准备本地映射目录 以便本地化持续存储数据 目
  • python No module named numpy. distutils._msvccompiler in numpy. distutils; trying from distutils

    在cmd 中输入 python setup py install 报错 No module named numpy distutils msvccompiler in numpy distutils trying from distutil
  • Android 报错 : FATAL EXCEPTION:main 解决方法

    今天安卓开发课上碰到的新问题 前景提示 老师让我们自己试一下那个两个页面跳转的效果 于是我就开始写了 然后报错 解决方法 逐一排查 首先要看你mainfest xml里面有没有增加Activity 当然我是加了 但是他还报错 具体代码界面
  • Ubuntu18.04 windows10双系统安装解决grub引导问题

    最近给服务器的电脑升级了ubuntu18 直接用u盘安装 老是说grub引导问题 网上有很多教程真的坑人 说的含含糊糊的 不知道在卖弄什么关子 我参照这两个教程解决了安装问题 十分钟就装好了 感谢你们 https blog csdn net
  • vue Tesseract的 ocr 文字识别

    npm结果页 https www npmjs com package tesseract js tesseract官网地址 https tesseract projectnaptha com npm结果页 npm结果页 tesseract官
  • 如何优雅的统计代码耗时

    点击上方 小强的进阶之路 选择 星标 公众号 优质文章 及时送达 预计阅读时间 16分钟 作者 Jitwxs 原文链接 底部链接可直达 https jitwxs cn 5aa91d10 html 一 前言 代码耗时统计在日常开发中算是一个十
  • R语言—列表

    文章目录 列表 定义 创建列表 List 列表 List 元素的引用 列表 List 元素的修改 访问列表元素和值 去列表化 在列表上使用apply系列函数 递归型列表 列表 R语言的6种模式 向量 矩阵 数组 数据框 列表 因子 向量 矩
  • SQLite如何删除,修改、重命名列

    今天在SQLite数据库中添加了一列 后来发现列名写错了 于是使用SQL语句来修改列名 可是根本不管用 首先 请放弃alter吧 sqlite官方说明如下 SQLite supports a limited subset of ALTER
  • 【JS】JavaScript时间与时间戳相互转换

    时间与时间戳相互转换 1 2 时间 JS常用时间类型 1 2 1 GMT 格林尼治标准时 1 2 2 UTC 协调世界时 1 2 3 中国标准时间 1 2 4 ISO8601标准时间格式 1 2 5 时间戳 timestamp 1 时间戳转
  • spring boot项目自动加载引入外部bean

    前言 spring boot项目简化了对外部项目的引入 使我们能够狠方便的构建一个web项目 我们通常在开发的过程中会开发出一些公用的模块组件 这样在项目找那个引入后能够直接使用 减少了轮子的重复构造 同时服务引入的模块化操作 能够更多的节
  • CV学习:OpenCv快速入门(python版)

    本文代码全部可运行 笔者运行环境 python3 7 pycharm opencv4 6 此文是学习记录 记录opencv的入门知识 对各知识点并不做深入探究 文章的目的是让阅读者在极短的时间达到入门水平 在学习过程中 我们应养成 查询op