OpenCV对图像特征提取

万壑松风知客来,摇扇抚琴待留声

1. 文起

随着深度学习的兴起,借由搭建神经网络完成的图像识别技术也越来越成熟,并由此产出了许多科技产品,例如生活中常见的人脸识别。当然随着技术的成熟,在如今这个鼓励、热衷开源的时代自然会出现许多普通人就能使用的强大工具——OpenCV

2. OpenCV 简介

OpenCV 全称 Open Source Computer Vision Library(即:开源计算机视觉库),是一个跨平台的计算机视觉库,高效维护的 GitHub 项目地址在这里。OpenCV 由因特尔公司发起并参与开发,可以在商业和研究领域免费使用。

OpenCV 可用于开发实时的图像处理、计算机视觉以及模式识别程序。由于它使用 C++ 语言编写,所以主要接口也是 C++ ,但依然保留了大量的 C 语言接口。同时目前也提供了其它如:Python、Java、MATLAB、C#、Ch、Ruby 等语言的接口,所以方便了许多程序员使用 OpenCV。

OpenCV 对大多数操作系统都支持如:Windows、Android、Maemo、FreeBSD、OpenBSD、IOS、Linux、Mac OS。不过在 Windows 上使用可能会需要做一点编译上的处理,这个可以在具体使用时查询处理方法。

OpenCV 可用于解决如下领域问题:

  • 增强实现
  • 人脸识别
  • 手势识别
  • 人机交互
  • 动作识别
  • 运动跟踪
  • 物体识别
  • 图像分割
  • 机器人

OpenCV 真的是功能强大。有了 OpenCV 对于一个深度学习不是特别了解的人,不用亲自搭建神经网络也可以高效完成图像处理任务。这里对 OpenCV 做了简单介绍,下面以 OpenCV 的一个小点进行介绍。

3. 特征提取

说明:不论是深度学习,还是在传统机器学习中,提取对象的特征往往是最重要的,因为将抽象的物体转换为一个个具体的特征,这样会大大方便、加快后续的模型学习。所以这里将介绍如何通过 Python 调用 OpenCV 接口来提取图像的各种特征。

opencv-python 版本: 4.1.1.26

原始图像如下:

一:读取图像:

1
2
3
4
5
6
7
8
9
import numpy as np
import cv2

# np. set_printoptions(threshold=np.inf)

img = cv2.imread('heart.jpg', 0)
cv2.imshow('heart', img)
cv2.waitKey()
cv2.destroyAllWindows()

使用 cv2 的 imread() 函数读取图像,第一个参数传入图片路径,第二个参数告诉函数如何读取图片。默认值为 1 表示以原始图像读取(包括 alpha 通道);0 表示以灰度图像读取,后续操作需要单通道的会读取。其它读取模式可以参考其它资料。

显示图像使用 imshow() 函数,不过后续需要添加 waitKey() 函数等待操作,否则显示不会持久,最后添加 destroyAllWindows() 函数关闭所有创建窗口是一个好习惯。

返回值 img 是读取的图像信息。

二:二值化矩阵

1
2
3
4
5
6
# 二值化
ret, thresh = cv2.threshold(src=img, thresh=200, maxval=255, type=1)

cv2.imshow('heart', thresh)
cv2.waitKey()
cv2.destroyAllWindows()

threshold() 函数可以对图像进行二值化处理,二值化处理的目的在于可以方便后续对图像的处理,例如将图片中目标转换为白色,背景转换为黑色,那 OpenCV 就可以更好的在黑色背景中寻找白色目标。

src 给定图像数组矩阵(现在这个版本既可以对单通道矩阵做处理,也可以处理多通道),thresh 给定阈值,maxval 给定填充值,type 是二值化模式选择(模式决定了如何二值化矩阵)。

返回值 ret 是阈值,thresh 是二值化后的矩阵信息。

阈值类型表:

阈值 小于阈值的像素点 大于阈值的像素点
0 置0 置填充色
1 置填充色 置0
2 保持原色 置灰色
3 置0 保持原色
4 保持原色 置0

三:边缘检测

1
2
3
4
5
6
# 边缘检测
canny = cv2.Canny(image=thresh, threshold1=200, threshold2=250, apertureSize=3, L2gradient=False)

cv2.imshow('heart', canny)
cv2.waitKey()
cv2.destroyAllWindows()

Canny 是一种边缘检测的方法,使用 Canny() 函数可以调用该方法。image 传入图像,threshold1threshold2 分别传入 minVal 和 maxVal 。当图像的灰度梯度高于 maxVal 时被认为是真的边界,那些低于 minVal 的边界会被抛弃。如果介于两者之间的话,就要看这个点是否与某个被确定为真正边界点相连,如果是,就认为它也是边界点,如果不是就抛弃。

apertureSize 用来设置计算图像梯度的 Sobel 卷积核的大小(3、5、7),默认值为3。L2gradient 设定求梯度大小的方程,默认为 False。

返回值 canny 是边缘点的图像矩阵信息。

四:轮廓检测

1
2
3
4
5
6
7
8
9
# 找轮廓
contours,hierarchy = cv2.findContours(image=canny,mode=cv2.RETR_TREE,method=cv2.CHAIN_APPROX_NONE)
# 画轮廓,需要先将轮廓信息与图像结合后绘制
cnt = contours[0]
image = cv2.drawContours(img,cnt,-1,(0,255,0),2)

cv2.imshow('heart', image)
cv2.waitKey()
cv2.destroyAllWindows()

findContours() 函数可以帮助我们找寻图像轮廓位置信息。image 传入图像信息,这里我是用的是上面边缘检测后的边缘图像 canny,当然你也可以传入二值化后的图像 thresh,canny 可能比 thresh 更适合。

mode 选择轮廓的检索模式:

检索模式 模式功能
cv2.RETR_EXTERNAL 表示只检测外轮廓
cv2.RETR_LIST 检测的轮廓不建立等级关系
cv2.RETR_CCOMP 建立两个等级的轮廓,上面的一层为外边界,里面的一层为内孔的边界信息。如果内孔内还有一个连通物体,这个物体的边界也在顶层。
cv2.RETR_TREE 建立一个等级树结构的轮廓。

method 为轮廓的近似方法:

近似方法 方法功能
cv2.CHAIN_APPROX_NONE 存储所有的轮廓点,相邻的两个点的像素位置差不超过1,即max(abs(x1-x2),abs(y2-y1))==1
cv2.CHAIN_APPROX_SIMPLE 压缩水平方向,垂直方向,对角线方向的元素,只保留该方向的终点坐标,例如一个矩形轮廓只需4个点来保存轮廓信息
cv2.CHAIN_APPROX_TC89_L1 使用 teh-Chinl chain 近似算法
cv2.CHAIN_APPROX_TC89_KCOS 使用 teh-Chinl chain 近似算法

通过轮廓列表找到了某一个轮廓后,如果绘制则需要使用 drawContours() 函数处理。传入的参数分别是:原图像、轮廓点矩阵、如何绘制(-1绘制所有点)、绘制线条颜色(原图像是灰度图则只有黑白线条)、线条粗细。返回值 image 是原图像与轮廓点结合处理后的图像矩阵。

返回值 contours 是一个列表,每一个元素表示检测到的一个轮廓点信息,轮廓的检测与传入的图像情况有很大关系,所以需要传入一个处理达标的图像。列表越长说明检测到的轮廓越多,某些轮廓可能只是一个小区域的几个点,此时如果需要选择最大的轮廓,一个方法可以循环列表找到当前轮廓最大的外界矩阵,判断矩阵大小从而找到最大轮廓。

返回值 hierarchy 是轮廓点的信息,包含了图像的拓扑信息,作为轮廓数量的表示 hierarchy 包含了很多元素,每个轮廓 contours[i] 对应 hierarchy 中 hierarchy[i][0]~hierarchy[i][3],分别表示后一个轮廓,前一个轮廓,父轮廓,内嵌轮廓的索引,如果没有对应项,则相应的 hierarchy[i] 设置为负数。

五:周长、面积

当拿到了图像的轮廓信息后,就可以通过使用对应函数获取图像的周长、面积了。

1
2
3
4
5
6
7
8
# 周长获取
perimeter = cv2.arcLength(cnt, True)

# 面积获取
area = cv2.contourArea(cnt)
print('周长:{},面积:{}'.format(perimeter, area))

# 周长:863.0264731645584,面积:40656.5

arcLength() 函数可以通过轮廓信息获取图像的周长,第一个参数传入轮廓数组矩阵,第二参数可以用来指定对象的形状是闭合的(True),还是打开的(一条曲线)。

contourArea() 函数可以用过轮廓信息获取图像面积。

六:外接最小矩阵

1
2
3
4
5
6
7
8
9
10
11
minAreaRect = cv2.minAreaRect(cnt)
boxpoint = cv2.boxPoints(minAreaRect)

# 如果需要绘制外接矩阵,则需要处理四个顶点数组的格式
boxpoint = [np.int0(boxpoint)]
# 同样需要用drawContours函数结合原图与外接矩阵
image = cv2.drawContours(thresh,boxpoint,-1,(255,255,255),1)

cv2.imshow('heart', image)
cv2.waitKey()
cv2.destroyAllWindows()

minAreaRect() 函数可以获取轮廓 cnt 的最小外接矩阵的(中心(x,y),(宽,高),旋转角度);boxPoints() 函数获取最小外接矩阵的四个顶点(顺时针[w,h]:左下角、左上角、右上角、右下角)。

七:形态学

形态学包括几种对图像的处理方式:腐蚀、膨胀、开、闭等。

腐蚀(erode()):基于二值图像,通过定义一个结构数组(卷积核),卷积核沿着图像滑动,如果与卷积核对应的原图像的所有像素值都是 1,那么中心元素就保持原来的像素值,否则就变为零,白色面积会减少。 通过此方法可以有效去除噪声点,去除的范围由卷积核大小决定。

膨胀(dilate()):与腐蚀相反,与卷积核对应的原图像的像素值中只要有一个是 1,中心元素的像素值就是 1,白色面积会增加。通过此方法可以有效的填补原图像中的小空洞,补齐图像。

开(morphologyEx()):先进行腐蚀再膨胀叫做开运算,两种方法通常结合起来使用。因为腐蚀在去掉白噪声的同时,也会使对象变小。所以再对它进行膨胀,这时噪声已经被去除了,不会再回来了,但是对象还在并会增加。

闭(morphologyEx()):与开运算相反的操作。

函数使用:

1
2
3
4
5
6
7
8
9
10
11
# 腐蚀(二值图像;卷积核;腐蚀次数)
erode_img = cv2.erode(img,kernel,iterations = 1)

# 膨胀
dilate_img = cv2.dilate(img,kernel,iterations = 1)

# 开(二值图像;开闭选项;卷积核)
= cv2.morphologyEx(img, cv2.MORPH_OPEN, kernel)

# 闭
cv2.morphologyEx(img, cv2.MORPH_CLOSE, kernel)

在上面的基础上还可以改进,除了大小还能改变卷积核的形状,通过 Numpy 可以定义一个简单的结构数组(卷积核):

1
2
3
4
5
6
7
8
# 定义一个简单的(5,5)卷积核
kernel = np.ones((5,5), np.uint8)

# [[1 1 1 1 1]
# [1 1 1 1 1]
# [1 1 1 1 1]
# [1 1 1 1 1]
# [1 1 1 1 1]]

其实 opencv 已经提供了卷积核的定义接口,并且有多种形状(矩形、交叉形、椭圆形),无需我们再重复定义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 矩形
rect_kernel = cv2.getStructuringElement(cv2.MORPH_RECT,(5,5))
# [[1 1 1 1 1]
# [1 1 1 1 1]
# [1 1 1 1 1]
# [1 1 1 1 1]
# [1 1 1 1 1]]

# 交叉形
cross_kernel = cv2.getStructuringElement(cv2.MORPH_CROSS,(5,5))
# [[0 0 1 0 0]
# [0 0 1 0 0]
# [1 1 1 1 1]
# [0 0 1 0 0]
# [0 0 1 0 0]]

# 椭圆形
ellipse_kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE,(5,5))
# [[0 0 1 0 0]
# [1 1 1 1 1]
# [1 1 1 1 1]
# [1 1 1 1 1]
# [0 0 1 0 0]]

卷积核的大小可以调整,两个数值也不一定相等。理论上来说形状是可以多种多样的。

使用开运算对图像做处理:

1
2
3
4
5
6
7
# 定义一个椭圆形卷积核
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (160,160))
open_thresh = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, kernel)

cv2.imshow('open_thresh',open_thresh)
cv2.waitKey()
cv2.destroyAllWindows()

4. 文末

所以技术共享是提高人类发展的高速通道,开源是大势所趋。以上只是 OpenCV 的冰山一角,更多的使用接口可以查询官方资料。使用过程中需要注意的是,不同接口函数使用的阈值参数需要根据实际情况调整到较好程度才能得到一个不错的结果,并且这个结果很可能会影响下一个阶段的图像处理,所以这点尤为重要。

参考:OpenCV官方教程中文版 for Python

参考:opencv学习(四十)之寻找图像轮廓findContours()