opencv基础

该过程主要通过实际操作完成

素材

选取合适的高清图片,通过截屏生成新图片降低图片质量,将新的低质量图片命名为text1.png保存在python脚本的目录中

代码环境

1
2
3
python解释器:anaconda3/python3.8
编译器:pycharm
编码:utf-8

代码

为了方便测试,只使用了一个脚本测试,学习笔记和部分运行结果也通过注释的方式简单加入

去除注释#即可运行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
# python解释器:anaconda3/python3.8
# 编译器:pycharm
# utf-8

# 一些测试过程以注释方式保留,以便以后查看

# 导入所需要的库,并给以简洁的名称
import numpy as np
import cv2 as cv

# 1
# 按指定方式读取图像
img = cv.imread('test1.png', 1) # 该步骤类似于C语言的文件指针
# 第一个参数为图片路径,不能含有中文等不兼容字符,否则报错,这里没办法只好使用了相对路径
# 第二个参数代表读取方式
# 1:加载彩色图像。任何图像的透明度都会被忽视。它是默认标志。
# 0:以灰度模式加载图像
# -1:加载图像,包括alpha通道

# 2
# show图像
# cv.imshow('test1', img) # 第一个参数是窗口名称,它是一个字符串。第二个参数是我们的对象。
# cv.waitKey(0) # 以0为参数时,无限制等待用户按下任意键
# cv.destroyAllWindows() # 销毁窗口

# 3
# 访问和修改像素
# px = img[100, 100] # 该值与图像读入方式有关
# print(px)
# img[100, 100] = 255 # 灰度
# img[100, 100] = [255, 255, 255] # BGR
# print(img[100, 100])

# 查阅资料了解到上面的方法的效率并不是很高
# 可以使用Numpy数组方法array.item()和array.itemset()
# 经测试,因为某些未知原因,导致运行错误,这里先略去,以后再debug

# 4
# 访问图像属性
# print(img.shape) # 访问图像形状
# 以灰度图像读入时,输出(674, 1200),仅返回行和列
# 以BGR读入时,输出(674, 1200, 3),多返回一位数,表示通道数

# print(img.size) # 访问图像像素总数
# 以灰度读入时,输出808800
# 以BGR读入时,输出2426400

# print(img.dtype) # 访问图像数据类型
# 输出 uint8
# img.dtype在调试时非常重要,因为OpenCV-Python代码中的大量错误是由无效的数据类型引起的。

# 5
# 拆分和合并图像通道
# B, G, R = cv.split(img) # 此时img由BGR方式读入,才可进行此操作
# print(B.shape) # 输出(674, 1200),与原图像数据相比仅缺少通道数,这表明拆分成功

# img = cv.merge((B, G, R)) # 进行这一步操作时,图像通过BGR方式读入,且上面的“B, G, R = cv.split(img)”需要先执行
# print(img.shape) # 输出(674, 1200, 3),与原图像数据完全相同,这表明合并成功

# 6
# 图像加法
# x = np.uint8([250])
# y = np.uint8([10])
# print(cv.add(x, y)) # 输出[[255]] 原理:opencv的加法采用饱和运算 250+10=260->255
# print(x + y) # 输出[4] 原理:numpy的加法采用模运算 (250+10)%256=4
# 两者相比较,使用时应该选用opencv的加法

opencv进阶

注:这部分内容是有针对性的学习,暂时用不到的就没有学

性能衡量和提升技术

该部分内容,我只简单提取了cv.useOptimized()cv.setUseOptimized()两条命令

对于部分操作的运行速度,优化会比不优化快两倍,所以我觉得有必要注意

检查是否使用优化

1
2
3
import cv2 as cv               # 导入opencv库,简化为cv
cv.useOptimized() # 检查是否使用opencv优化,该函数值为Ture或者False
print(cv.useOptimized()) # 打印该函数值,判断是否启用优化

启用/禁用优化

1
2
3
import cv2 as cv               # 导入opencv库,简化为cv
cv.setUseOptimized(Ture) # 启用优化
cv.setUseOptimized(False) # 禁用优化

其他性能优化技术

这部分内容暂时不学习,但是先做个摘记

有几种技术和编码方法可以充分利用 Python 和 Numpy 的最大性能。这里要注意的主要事情是,首先尝试以一种简单的方式实现算法。一旦它运行起来,分析它,找到瓶颈并优化它们。

1.尽量避免在Python中使用循环,尤其是双/三重循环等。它们本来就很慢。

2.由于Numpy和OpenCV已针对向量运算进行了优化,因此将算法/代码向量化到最大程度。

3.利用缓存一致性。

4.除非需要,否则切勿创建数组的副本。尝试改用视图。数组复制是一项昂贵的操作。

即使执行了所有这些操作后,如果你的代码仍然很慢,或者不可避免地需要使用大循环,请使用Cython等其他库来使其更快。

图像梯度

OpenCV提供三种类型的梯度滤波器或高通滤波器,即Sobel,Scharr和Laplacian

这是三个可以直接用的函数,暂时没搞清楚它们的原理,去了解了一下使用效果,进行实测后失败,原因未知,这里记录一下

官方使用的效果图如下:

1

可以看出Laplacian方法较为优秀

Canny边缘检测

cv.Canny()方法

这个算法,或许会对我的工作有借鉴意义,虽然思路上有很大不同,我把这种方法分类为矢量方法,而我认为我所需要做的工作属于标量方法。我个人的观点是:矢量方法更需要想象能力,标量方法更需要精密的思维;而有些东西是共通的,比如下面讲到的降噪、阈值思想

降噪

由于边缘检测容易受到图像中噪声的影响,因此第一步是使用5x5高斯滤波器消除图像中的噪声。

和上面的“图像梯度”一样,也需要降噪,由此可见,降噪在图像处理中是很重要的

查找图像的强度梯度

使用Sobel核在水平和垂直方向上对平滑的图像进行滤波,以在水平方向(Gx)和垂直方向(Gy)上获得一阶导数。渐变方向始终垂直于边缘。将其舍入为代表垂直,水平和两个对角线方向的四个角度之一。

非极大值抑制

在获得梯度大小和方向后,将对图像进行全面扫描,以去除可能不构成边缘的所有不需要的像素。为此,在每个像素处,检查像素是否是其在梯度方向上附近的局部最大值。

效果是能提取出细边

磁滞阈值

确定哪些边缘全部是真正的边缘,哪些不是。为此,我们需要两个阈值 minValmaxVal。强度梯度大于 maxVal 的任何边缘必定是边缘,而小于 minVal 的那些边缘必定是非边缘,因此将其丢弃。介于这两个阈值之间的对象根据其连通性被分类为边缘或非边缘。如果将它们连接到“边缘”像素,则将它们视为边缘的一部分。否则,它们也将被丢弃。

阈值说白了,就是人为搞个界限,挑选出较明显的边缘和非边缘,用于简化计算

另外,还有些特殊情况:边缘A在 maxVal 之上,因此被视为“确定边缘”。尽管边C低于 maxVal ,但它连接到边A,因此也被视为有效边,我们得到了完整的曲线。但是边缘B尽管在 minVal 之上并且与边缘C处于同一区域,但是它没有连接到任何“确保边缘”,因此被丢弃。因此,非常重要的一点是我们必须相应地选择 minValmaxVal 以获得正确的结果。

在边缘为长线的假设下,该阶段还消除了小像素噪声。因此,我们最终得到的是图像中的强边缘。

小总结

进阶部分就暂时学到这里(内容还有很多啊,但为了开始尝试一下自己的不一样的图像算法,还是先停下),不得不恭维一下opencv库,网上都说这是一个强大的库,但仅仅一个形容词“强大”,怎么能让我了解它,难不成仅仅是通过它的体积大,下载慢?

看过一些函数之后,才开始发自内心赞叹,比如单单拿出一个函数,让我封装起来,提供大部分语言的接口,供给通用场景使用,这就不是现在的我能做到的了,或许有一天我也可以吧。抽空得多看看这些函数的漂亮的源码。同时在接下来的任务中,我也期待着opencv能给我带来的新的震撼,或许会是仰止弥高,钻之弥坚。

numpy的应用

OpenCV-Python Tutorials中也常提到numpy库,即便在根本没用到它的代码示例中,也会来一行import numpy

在实践中也发现,numpy能极大提高代码编写的效率;而查阅资料后发现,numpy对数据的索引效率远高于不使用它的情况,所以numpy也是图像处理中的一大利器。

但是由于时间原因,暂时不像学习opencv一样对numpy进行系统的学习,这里就记录一些用法

结构体数组

1
2
3
4
5
6
7
8
import numpy as np
# 建立结构体类型
Mytype = np.dtype({
'names': ['value', 'noise', 'part'], # value像素值,noise噪声值,part分区(1/2)
'formats': ['i', 'i', 'i'] # 这里都采用整型(numpy对于变量的范围和类型要求严格)
})
# 新建结构体数组,下面的代码能直接新建自定义类型的,初始化的数组
array = np.zeros((m, n), dtype=Mytype) # “(m, n)”定义数组的形式,这里为二维数组,m行n列

numpy数组排序

由于numpy建立的数组可以很复杂,所以numpy的排序函数的参数也很多

1
2
3
4
5
6
import numpy
numpy.sort(a, axis=-1, kind=None, order=None)
# a : 要排序的数组
# axis : 按什么轴进行排序,默认按最后一个轴进行排序
# kind :排序方法,默认是快速排序
# order : 当数组定义了字段属性时,可以按照某个属性进行排序

图像处理实战

测试1

思路

将素材图片(同附件)读入,然后通过两种划分方法的遍历比较,得出噪声值(次数),修改像素操作(容易实现)及其他特殊情况的优化先不考虑

test3

代码

(同附件)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
# python解释器:anaconda3/python3.8
# 编译器:pycharm
# utf-8

import numpy as np
import cv2 as cv
import heapq

# 创建管理像素噪声值及分区的结构体
Pixel_type = np.dtype({
'names': ['value', 'noise', 'part'], # value像素值,noise噪声值,part分区(1/2)
'formats': ['i', 'i', 'i']
})


# 判断区分度
def judge_division_degree(array):
p1 = []
p2 = []
for i in range(0, len(array)):
if array[i]['part'] == 1:
p1.append(array[i]['value'])
elif array[i]['part'] == 0:
p2.append(array[i]['value'])
# 防止列表为空
if len(p1) == 0 or len(p2) == 0:
return False
expected_degree = 0
if min(p2) - max(p1) <= expected_degree:
return False
else:
return True


# 划分方法一(不考虑位置)
def division_method1(array):
# 使p数组为有序集(p数组为一维数组,无需考虑轴)
p = np.sort(array, order='value')
# p1为p中相邻两数之差的数组
p1 = []
for i in range(0, len(p) - 1):
p1.append(p[i + 1]['value'] - p[i]['value'])
# 找出最大差值的下标
max_index = p1.index(max(p1))
# 建立较小数下标不重复数组
small_indexs = list(set(heapq.nsmallest(max_index + 1, p['value'])))
# 将较小数划分到区域一,标记为“1”,反之,标记仍为“0”的在区域二
for m in range(0, len(small_indexs)):
for n in range(0, len(array)):
if small_indexs[m] == array[n]['value']:
array[n]['part'] = 1
# 返回被标记好的数组
return array


# 划分方法二(考虑位置)
def division_method2(array):
# 建立数组p,存放顺时针方向像素的差值(前 - 后)
p = []
for i in range(0, len(array) - 1):
p.append(array[i]['value'] - array[i + 1]['value'])
p.append(array[-1]['value'] - array[0]['value'])
min_index = p.index(min(p))
max_index = p.index(max(p))
# 始终将较小部分的数标记为1
if min_index > max_index:
for i in range(max_index, min_index + 1):
array[i]['part'] = 1
elif min_index < max_index:
for i in range(0, len(p)):
array[i]['part'] = 1
for i in range(min_index, max_index + 1):
array[i]['part'] = 0
# 返回被标记好的数组
return array


# 检查噪声点,传入的参数为单色图像通道
def noise_check(image_channel):
# 锁定图像边界
(x, y) = image_channel.shape
# 建立二维通道数组
b = np.zeros((x, y), dtype=Pixel_type)
# 先建立数组
for i in range(0, x):
for j in range(0, y):
# 存入像素值,并初始化噪声值和分区
b[i][j] = (image_channel[i][j], 0, 0)
# 因为接下来的操作需要提取一个个完整的九宫格
# 所以遍历像素点时,图像最边缘的像素点永不成为中心像素点
# 即:从1开始到最大值-1结束
for i in range(1, x - 1):
for j in range(1, y - 1):
# 建立八邻域的数组,围绕中心像素点,按顺时针标记八个像素点
eight_neighbor1 = np.zeros(8, dtype=Pixel_type)
eight_neighbor1[0] = b[i - 1][j - 1]
eight_neighbor1[1] = b[i - 1][j]
eight_neighbor1[2] = b[i - 1][j + 1]
eight_neighbor1[3] = b[i][j + 1]
eight_neighbor1[4] = b[i + 1][j + 1]
eight_neighbor1[5] = b[i + 1][j]
eight_neighbor1[6] = b[i + 1][j - 1]
eight_neighbor1[7] = b[i][j - 1]
# 划分方法一
eight_neighbor_division1 = division_method1(eight_neighbor1)
# 建立八邻域的数组,围绕中心像素点,按顺时针标记八个像素点
eight_neighbor2 = np.zeros(8, dtype=Pixel_type)
eight_neighbor2[0] = b[i - 1][j - 1]
eight_neighbor2[1] = b[i - 1][j]
eight_neighbor2[2] = b[i - 1][j + 1]
eight_neighbor2[3] = b[i][j + 1]
eight_neighbor2[4] = b[i + 1][j + 1]
eight_neighbor2[5] = b[i + 1][j]
eight_neighbor2[6] = b[i + 1][j - 1]
eight_neighbor2[7] = b[i][j - 1]
# 划分方法二
eight_neighbor_division2 = division_method2(eight_neighbor2)
# 判断区分度
if judge_division_degree(eight_neighbor_division1) and \
judge_division_degree(eight_neighbor_division2):
# 找出划分区域不一样的像素点
for k in range(0, 8):
if eight_neighbor_division1[k]['part'] != eight_neighbor_division2[k]['part']:
if k == 0:
b[i - 1][j - 1]['noise'] += 1
elif k == 1:
b[i - 1][j]['noise'] += 1
elif k == 2:
b[i - 1][j + 1]['noise'] += 1
elif k == 3:
b[i][j + 1]['noise'] += 1
elif k == 4:
b[i + 1][j + 1]['noise'] += 1
elif k == 5:
b[i + 1][j]['noise'] += 1
elif k == 6:
b[i + 1][j - 1]['noise'] += 1
elif k == 7:
b[i][j - 1]['noise'] += 1
return b


# 以BGR方式读入图片
img = cv.imread('test3.png', 1)
# 检查读入是否成功
# print(img.shape)
# 输出(302, 302, 3),代表成功

# 拆分图像通道
B, G, R = cv.split(img)

# 调用 noise_check() 函数,检查可能的噪声点
b_px = noise_check(B) # 以B通道举例

# 在output。txt文件下输出噪声值的矩阵排布
pf = open('output.txt', 'w')
for i1 in range(0, 302):
for j1 in range(0, 302):
print(b_px[i1][j1]['noise'], end='', file=pf)
print('', file=pf)
print("导出成功")