核心操作
一、图像基本操作
主要包括以下操作:
- 获取图像像素值并修改
- 获取图像属性
- 设置感兴趣区域(ROI)
- 图像切割与合并
- 图像边框填充
1. 获取图像像素值并修改
import numpy as np
import cv2 as cv
def cv_show(name, img):
cv.imshow(name, img)
cv.waitKey(0)
cv.destroyAllWindows()
def compare(imgs):
# for i in range(len(imgs)):
# imgs[i][:,-3:-1,:] = [255,255,255]
res = np.hstack(imgs)
cv_show('Compare', res)
# 先读入一幅图像
img = cv.imread('lena.png')
# 通过行列组合读取像素值
px = img[100,100]
px
array([ 78, 68, 178], dtype=uint8)
因为读入的img是一副彩色图像,所以img阵列位于坐标(100,100)处的值有三个,对应三个通道BGR
# 读取对应通道的值
r = img[100, 100, 2]
r
178
当然我们也可以修改对应坐标点的值,或者整行、整列的值
# 修改(100, 100)处的img像素值
img[100,100] = [255, 255, 255]
img[100,100]
array([255, 255, 255], dtype=uint8)
# 利用切片修改整行整列的值:
img[254:256,...] = [255,255,255]
cv_show('Test',img)
# 提取出图像的BGR层的灰度值
r = img[:,:,2]
g = img[:,:,1]
b = img[:,:,0]
compare([b,g,r])
r.shape
(512, 512)
img.shape
(512, 512, 3)
- 注意: Numpy库主要是加快阵列运算速度。所以单纯的获取、修改每个像素值不推荐使用Numpy。
上面的方法通常用于选择数组的区域,比如前5行和后3列。对于单个像素访问,Numpy 数组方法: array.item ()和 array.itemset() 更好。但是,它们总是返回一个标量,因此如果要访问所有 b、 g 和 r 值,就需要为每个值分别调用 arry.item ()。或者像上例一样。
img.item((50,50,1)) # 获取(50,50)处的green值
140
img.itemset((50,50,1), 255) # 设置(50,50)处的green值为255
img.item((50,50,1))
255
2. 获取图像属性
图像属性一般包括以下部分:
- 行数
- 列数
- 通道数
- 图像数据的类型
- 像素值的类型 and so forth
shape属性能以tuple类型返回图像的(行数、列数、通道数)
img.shape
(512, 512, 3)
dtype属性给出了图像像素值的存储类型
img.dtype
dtype('uint8')
- 注意: 当我们在Debug时,img.dtype尤为重要,因为我们使用OpenCv-python造成的大量错误都是由错误的数据类型导致的
3. 提取ROI
很多时候我们并非是直接操作整幅图像,例如在图像中的眼睛检测,首先对整个图像进行人脸检测。在获取人脸图像时,我们只选择人脸区域,搜索其中的眼睛,而不是搜索整个图像。它提高了准确性(因为眼睛总是在面部上)和性能(因为我们搜索的区域很小)。
再次使用 Numpy 索引获得 ROI。这里我选择了lena的面部,并将其复制到图片中的另一个区域:
roi = img[220:380,246:346]
cv_show('ROI',roi)
img[60:220, 146:246] = roi
cv_show('ROI_transfer', img)
4. 分割、合并图像通道
有时候你需要分别处理图像的 b、 g、 r 通道。在这种情况下,您需要将 BGR 映像拆分为单个通道。在其他情况下,您可能需要加入这些单独的通道来创建 BGR 映像。你可以通过以下简单的方法来实现:
- split(img) : 返回三个img通道的灰度值(BGR),都是灰度图
- merge([ b, g ,r]):split的逆操作
img = cv.imread('lena.png')
b,g,r = cv.split(img)
compare([b,g,r])
# 当然我们也可使用Numpy的数组访问
b = img[:,:,0]
- 注意:split的执行效率比较慢,若不是必须使用split,则我们应该使用Numpy的下标适用法
5. 图像边框填充
如果您想要在图像周围创建边框,比如相框,可以使用 cv.copyMakeBorder ()。但它在卷积运算、零填充等方面有更多的应用。这个函数接受以下参数:
- src - 输入图像
- top, bottom, left, right - 对应方位填充的宽度
- borderType - 填充边框的方式
- cv.BORDER_CONSTANT - 需要再传入一个参数(填充边框的像素值),常数填充
- cv.BORDER_REFLECT - 镜像填充:123 | 321
- cv.BORDER_REFLECT_101 or cv.BORDER_DEFAULT - 和镜像填充有一点区别,不会复制边界值:gfedcb|abcdefgh|gfedcba
- cv.BORDER_REPLICATE - 边界最终值适应: aaaaaa|abcdefgh|hhhhhhh
- cv.BORDER_WRAP - 赋值偏移 : cdefgh|abcdefgh|abcdefg
- value - 只有当borderType 为 cv.BORDER_CONSTANT 才需要选择传入一个参数,根据图像的类型选择,BGR则是tuple或list,而灰度图则是像素值
img1 = cv.imread('OpenCV.jpg')
replicate = cv.copyMakeBorder(img1,10,10,10,10,cv.BORDER_REPLICATE)
reflect = cv.copyMakeBorder(img1,10,10,10,10,cv.BORDER_REFLECT)
reflect101 = cv.copyMakeBorder(img1,10,10,10,10,cv.BORDER_REFLECT_101)
wrap = cv.copyMakeBorder(img1,10,10,10,10,cv.BORDER_WRAP)
constant= cv.copyMakeBorder(img1,10,10,10,10,cv.BORDER_CONSTANT,value=(0, 0, 255))
compare([constant, reflect, reflect101])
compare([replicate, wrap])
二、图像算术操作
主要包括以下部分:
- 加减乘除、位运算等算术操作
- cv.add(), cv.addWeighted()等函数
1. 图像加法
您可以使用 OpenCV 函数 cv.add ()让两个图像相加,或者只需使用 numpy 操作 res = img1 + img2即可。两个图像应该具有相同的通道数和像素值类型,或者被加图像可以只是一个标量值。
- 注意:OpenCV中的加法和Numpy里面的阵列加法不同, OpenCV 是一个取限操作,而 Numpy 加入是一个取模操作。
# 查看下面例子
x = np.uint8([254])
y = np.uint8([2])
# 溢出取上限或者下限
res_cv = cv.add(x,y)
# 溢出对整个数据域取模
res_np = x + y
(res_cv, res_np)
(array([[255]], dtype=uint8), array([0], dtype=uint8))
我们将两幅图像进行相加时,使用OpenCV的add方法更好。
2. 图像融合
同样是图像加法,但不同的权重给予图像以给人一种混合或透明的感觉。图片按照下面的公式融合:
通过指定上面的值([0, 1])来决定两幅图像的权重问题
cv.addWeighted() 方法提供了如下运算方式:,一般将值取为0
img1 = cv.imread('lena.png')
img2 = cv.imread('xy.png')
img2 = cv.resize(img2, img1.shape[:2])
compare([img1,img2])
dst1 = cv.addWeighted(img1, 0.5, img2, 0.5, 0)
dst2 = cv.addWeighted(img1, 0.7, img2, 0.3, 0)
dst3 = cv.addWeighted(img1, 0.3, img2, 0.7, 0)
compare([dst1, dst2, dst3])
3. 位运算
- 位运算包括按位 AND、 OR、 NOT 和 XOR 操作。它们在提取图像的任何部分(正如我们将在后面的章节中看到的)、定义和处理非矩形 ROI 等方面非常有用。
下面我们将学习如何改变一个图像的特定区域。我想把 OpenCV 的标志放在一个图片上面。如果我添加两个图像,它会改变颜色。如果我混合它们,我得到一个透明的效果。但我希望它是不透明的。如果是一个矩形区域,我可以使用 ROI,就像我们在上一章中所做的那样。但是 OpenCV 的 logo 不是长方形的。所以你可以使用如下的按位操作来实现:
img = cv.imread('lena.png')
logo = cv.imread('OpenCV.jpg')
# 转化为灰度图便于后面的阈值处理
logo_gray = cv.cvtColor(logo, cv.COLOR_BGR2GRAY)
# 从img中提取出ROI,也就是logo要插入的位置
row, col = logo.shape[:2]
roi = img[0:row, 0:col, :]
# 注意threshold的返回值,第二个参数才是对应的二值图像,白底图像需要将阈值提高才能滤除白底
_, mask = cv.threshold(logo_gray, 200, 255, cv.THRESH_BINARY)
# 取反用于对logo处理,留出对应的图像
mask_inv = cv.bitwise_not(mask)
compare([mask, mask_inv])
一定要记住,纯黑像素值为0,纯白像素值为255
所以,左边图像mask用于滤除背景图像,留出logo三环,右边图像则相反
# 取出logo中的ROI , foreground 前景
img2_fg = cv.bitwise_and(logo, logo, mask = mask_inv)
# 挖空img中的ROI, 作为background 背景
img1_bg = cv.bitwise_and(roi, roi, mask = mask)
# 组成logo方框
dst = cv.add(img1_bg,img2_fg)
# 填回原图
img[0:row, 0:col] = dst
cv_show('Res', img)
三、性能衡量与改进方法
在图像处理中,由于每秒要处理大量的操作,因此代码不仅必须提供正确的解决方案,还必须以最快的方式提供。所以在这一章中,你会学到:
- 衡量代码的性能
- 提高代码性能的一些技巧
- cv.getTickCount, cv.getTickFrequency等函数
除了 OpenCV 之外,Python 还提供了一个模块time,这有助于度量执行时间。另一个模块配置文件profile有助于获得关于代码的详细报告,比如代码中的每个函数花费了多少时间,函数被调用了多少次,等等。但是,如果使用 IPython,所有这些特性集成起来。我们将看到一些重要的信息,有关详细信息,请查看“附加资源”部分中的链接。
1. 衡量代码性能
cv.Gettickcount 函数返回引用事件(比如机器打开的那一刻)之后到调用该函数的那一刻的时钟周期数。因此,如果在函数执行之前和之后调用它,就会得到用于执行函数的时钟周期数。
cv.Gettickfrequency 函数返回时钟周期的频率,或者每秒钟的时钟周期数。因此,要查找以秒为单位的执行时间,可以执行以下操作:
# 开始时间
e1 = cv.getTickCount()
# 代码部分
for i in range(100000):
pass
# 结束时间
e2 = cv.getTickCount()
time = (e2 - e1)/ cv.getTickFrequency()
time
0.0029109
我们将用下面的例子来演示。下面的示例应用中值滤波,中值滤波的核的奇数大小从5到49不等
img1 = cv.imread('Lena.png')
e1 = cv.getTickCount()
for i in range(5,49,2):
img1 = cv.medianBlur(img1, i)
e2 = cv.getTickCount()
t = (e2 - e1)/cv.getTickFrequency()
t
0.5362088
img1 = cv.imread('Lena.png')
dict = {}
s = cv.getTickCount()
for i in range(5,49,2):
e1 = cv.getTickCount()
img1 = cv.medianBlur(img1, i)
e2 = cv.getTickCount()
t = (e2 - e1)/cv.getTickFrequency()
dict[i] = t
e = cv.getTickCount()
a = (e - s)/cv.getTickFrequency()
a
# 总时长
0.4873385
# 不同尺寸的滤波器的处理时长
for i in dict.items():
print(i[0],i[1])
5 0.00215
7 0.0271131
9 0.0275508
11 0.0278884
13 0.0302166
15 0.0316813
17 0.0229601
19 0.0220354
21 0.0222706
23 0.0214787
25 0.0214873
27 0.0214537
29 0.0209982
31 0.0216555
33 0.0215539
35 0.0211512
37 0.0209659
39 0.0210184
41 0.0210978
43 0.0207519
45 0.0205236
47 0.020461
- 注意:可以用 time 模块做同样的事情。使用 time.time()函数代替 cv.getTickCount。然后取这两次的差值。
import time
# 开始时间
e1 = time.time()
# 代码部分
for i in range(100000):
pass
# 结束时间
e2 = time.time()
t = (e2 - e1)/ cv.getTickFrequency()
t
2.991914749145508e-10
2. OpenCV默认优化方式
许多 OpenCV 函数都是使用 SSE2、 AVX 等进行优化的。它还包含未优化的代码。因此,如果我们的系统支持这些特性,我们就应该利用它们(几乎所有现代的处理器都支持它们)。在编译时默认启用它。因此,如果启用 OpenCV,它将运行优化的代码,否则它将运行未优化的代码。您可以使用 cv.useOptimized ()来检查是否启用/禁用它,使用 cv.setUseOptimized ()来启用/禁用它。让我们看一个简单的例子。
cv.setUseOptimized(True)
cv.useOptimized()
True
%timeit res = cv.medianBlur(img,49)
25.3 ms ± 259 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
cv.setUseOptimized(False)
cv.useOptimized()
False
%timeit res = cv.medianBlur(img,49)
27.2 ms ± 252 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
优化的中值滤波比未优化的版本快一点。如果你检查它的来源,你可以看到中值滤波是 SIMD 优化。因此,您可以使用它在代码顶部启用优化(请记住,它是默认启用的)。
3. 性能优化技术
有几种技术和编码方法可以充分利用 Python 和 Numpy 的最大性能。这里只注明相关信息,并提供重要信息来源的链接。这里要注意的主要事情是,首先尝试以一种简单的方式实现算法。一旦它运行起来,分析它,找到瓶颈,并优化它们。
- 尽可能避免在python中使用循环,尤其是二重甚至是三重循环,这些代码通常都很费时
- 最大限度地向量化算法/代码,因为 Numpy 和 OpenCV 针对向量操作进行了优化。
- 利用缓存一致性。
- 除非必要,否则不要使用copy复制数组,最好使用view
4. 一些优化介绍
1. Python Optimization Techniques
2. Scipy Lecture Notes - Advanced Numpy
scipy-lectures.github.io/advanced/ad…
3. Timing and Profiling in IPython