Core Operations
1 Basic Operatons on Images
1.1 目标
学习:
- 访问并修改像素值
- 访问图像属性
- 设置图像域(ROI)
- 分开和合并图像
在这部分几乎所有的操作主要是和Numpy而非OpenCV相关,用OpenCV写更好的优化代码需要更好的掌握Numpy。
1.2 访问并修改像素值(pixel values)
首先加载图像:
import cv2
import numpy as np
img = cv2.imread('messi5.jpg')
可以通过行和列坐标访问像素值。对BGR图像,返回Blue, Green, Red值的数列。对于灰度(grayscale)图像,返回相应的intensity。
px = img[100, 100]
print(px)
# 输出: [157 166 200]
# accessing only blue pixel
blue = img[100, 100, 0]
print(blue)
# 输出:157
# accessing only green pixel
green = img[100, 100, 1]
print(green)
# 输出:166
同样的方式修改像素值。
img[100, 100] = [255, 255, 255]
print(img[100, 100])
# 输出:[255 255 255]
警告:
Numpy是一个数组快速计算的优化库。所以简单访问和修改每个像素值是非常慢的,不被鼓励。
注意:
上面提及的方法主要用于选择数组的区域,比如前5行和后3列。对于个别像素的访问,Numpy数组方法array.item()和array.itemset()被认为更好。但是它们总是返回标量。所以,如果你想要访问所有的B,G,R值,你需要对它们分别调用array.item()。
更好的像素访问和编辑方法:
# accessing RED value
img.item(10, 10, 2)
# 输出:59
# modifying RED value
img.itemset((10, 10, 2), 100)
img.item(10, 10, 2)
# 输出:100
1.3 访问图像属性
图像属性包含行、列、通道的数量,图像数据的类型,像素数量等。
img.shape访问图像的shape,返回行、列、通道数量的元组(如果是彩色图像)。
print(img.shape)
# 输出:(342, 548, 3)
注意:
如果是灰度图像,则返回只有行和列数的元组。所以它是一个检验加载图像是灰度图像还是色彩图像的好方法。
整个像素数量通过img.size访问:
print(img.size)
# 输出:562248
图像数据类型通过img.dtype得到:
print(img.dtype)
# 输出:uint8
注意:
当调试的时候,img.dtype非常重要,因为在OpenCV-Python代码中大量的错误是由无效的数据类型引起的。
1.4 图像域(ROI)
有时,你将不得不处理一些图像域。对于图像中的眼睛检测,首先在图像中执行脸部检测,直到找到脸部,然后在脸部搜索眼睛区域。这个方法改善了精确度(因为眼睛总是在脸上)和性能(因为我们搜索一小块区域)。
ROI用Numpy索引可以得到。这里我们将选择球,并且复制它到图像的另一个区域:
ball = img[280:340, 330:390]
img[273:333, 100:160] = ball
cv2.imshow("img", img)
k = cv2.waitKey(0)
if k == 27: # wait for ESC key to exit
cv2.destroyAllWindows()
1.5 分开和合并图像通道
图像的B,G,R通道可以分裂为它们单独的平面,单独通道可以再一次合并为BGR图像。执行如下:
b, g, r = cv2.split(img)
img = cv2.merge((b, g, r))
# 或者
b = img[:,:,0]
假如你想将所有红色像素设置为0,不必像这样分裂,然后将其设置为0,你只需用更快的Numpy所以就可以。
img[:,:,2] = 0
警告:
cv2.split()是比较耗时的操作,所以只有必要的时候采用。Numpy索引更加有效,如果可能的话应该尽量使用。
1.6 为图像创建边界(填充Padding)
如果想要围绕图像创建一个像相片框那样的边界,可以用cv2.copyMakeBorder()函数。但是对于卷积操作(convolution)它有更多的应用,例如0填充(zero padding)等。此函数有如下参数:
-
src: 输入图像
-
top, bottom, left, right: 在相应方向像素数量中的边界宽度(边界填充)
-
borderType: 定义添加哪种类型边界的Flag,有以下几种类型:
- cv2.BORDER_CONSTANT: 添加常量色彩边界
- cv2.BORDER_REFLECT: 边界将会是边界元素的镜像反射,像这样:fedcba|abcdefgh|hgfedcb
- cv2.BORDER_REFLECT_101或者cv2.BORDER_DEFAULT: 和上面相同,但有稍微的改变,像这样:gfedcb|abcdefgh|gfedcba
- cv2.BORDER_REPLICATE: 最后一个元素被复制,像这样:aaaaaa|abcdefgh|hhhhhhh
- cv2.BORDER_WRAP: 没法解释,看起来像这样:cdefgh|abcdefgh|abcdefg
-
value: 如果边界类型是cv2.BORDER_CONSTANT,则表示边界颜色
为了更好的理解,下面的代码展示了所有的这些边界类型:
import cv2
import numpy as np
from matplotlib import pyplot as plt
%matplotlib inline
BLUE = [255, 0, 0]
img1 = cv2.imread('opencv_logo.png')
replicate = cv2.copyMakeBorder(img1, 20, 20, 20, 20, cv2.BORDER_REPLICATE)
reflect = cv2.copyMakeBorder(img1, 20, 20, 20, 20, cv2.BORDER_REFLECT)
reflect101 = cv2.copyMakeBorder(img1, 20, 20, 20, 20, cv2.BORDER_REFLECT_101)
wrap = cv2.copyMakeBorder(img1, 20, 20, 20, 20, cv2.BORDER_WRAP)
constant= cv2.copyMakeBorder(img1, 20, 20, 20, 20, cv2.BORDER_CONSTANT, value=BLUE)
plt.rcParams['figure.figsize'] = (12, 8)
plt.subplot(231), plt.imshow(img1,'gray'), plt.title('ORIGINAL')
plt.subplot(232), plt.imshow(replicate,'gray'), plt.title('REPLICATE')
plt.subplot(233), plt.imshow(reflect,'gray'), plt.title('REFLECT')
plt.subplot(234), plt.imshow(reflect101,'gray'), plt.title('REFLECT_101')
plt.subplot(235), plt.imshow(wrap,'gray'), plt.title('WRAP')
plt.subplot(236), plt.imshow(constant,'gray'), plt.title('CONSTANT')
plt.show()
在上图中可以看到展示结果,由于图像是由matplotlib来展示的,所以RED和BLUE平面会交换。
2 Arithmetic Operations on Images
2.1 目标
- 学习在图像上的算术运算,如加法(addition)、减法(subtraction)、按位操作(bitwise operations)等
- 学习函数:cv2.add(), cv2.addWeighted()等
2.2 图像加法
你可以用OpenCV的cv2.add()函数或者通过numpy操作res = img1 + img2将两个图像相加。两张图像应该是同样的深度(depth)和类型(type),或者第二个图像是一个标量值。
注意:
OpenCV加法和Numpy加法有些不同,OpenCV加法是饱和(saturated)操作,而Numpy加法是模(modulo)操作。
例如,考虑下面的例子
x = np.uint8([250])
y = np.uint8([10])
print(cv2.add(x, y)) # 250 + 10 = 260 => 255
# 输出:[[255]]
print(x + y) # 250 + 10 = 260 % 256 = 4
# 输出:[4]
如果是两个图像相加,可视化的效果更加明显。OpenCV函数会提供一个更好的结果,所以总是坚持利用OpenCV函数会更好。
2.3 图像混合(blending)
这也是一个图像加法,但是给予图像不同的权重,所以会给人一种混合(blending)或者透明(transparency)的感觉。图像相加的等式如下:
通过改变\(\alpha\)从0到1的值,可以实现从一个图像到另一个图像的转变。
这里,我们拿两个图像混合在一起。第一个图像给予0.7的权重,第二个图像给予0.3的权重。cv2.addWeighted()在图像上应用下面的等式:
这里\(\gamma\)取0。
img1 = cv2.imread('ml.png')
img2 = cv2.imread('opencv_logo.png')
img3 = img1[50:249, 50:250] # 这里两个图像大小不同,需要处理
dst = cv2.addWeighted(img3, 0.7, img2, 0.3, 0)
cv2.imshow('dst', dst)
cv2.waitKey(0)
cv2.destroyAllWindows()
原图像和混合图像展示:
import matplotlib.pyplot as plt
%matplotlib inline
plt.rcParams['figure.figsize'] = (9, 6)
plt.subplot(1,3,1), plt.imshow(img3, 'gray')
plt.subplot(1,3,2), plt.imshow(img2, 'gray')
plt.subplot(1,3,3), plt.imshow(dst, 'gray')
2.4 按位操作
按位操作包含bitwise AND, OR, NOT 和 XOR操作。它们在提取图像任意部分、定义和处理非矩形ROI时非常有用。下面的例子我们可以看到如何改变图像的特定区域。
我想将OpenCV logo放到一个图像上。如果我将两个图像相加,它将会改变颜色。如果混合它,将会得到透明,但是我想得到不透明的(opaque)。如果是一个矩形区域,我可以像我们上一章中一样用ROI。但是OpenCV logo不是一个矩形形状,所以你可以像下面一样运用按位运算处理。
# Load two images
img1 = cv2.imread('messi5.jpg')
img2 = cv2.imread('opencv_logo.png')
# I want to put logo on top-left corner, So I create a ROI
rows, cols, channels = img2.shape
roi = img1[0:rows, 0:cols]
# Now create a mask of logo and create its inverse mask also
img2gray = cv2.cvtColor(img2, cv2.COLOR_BGR2GRAY)
ret, mask = cv2.threshold(img2gray, 10, 255, cv2.THRESH_BINARY)
mask_inv = cv2.bitwise_not(mask)
# Now black-out the area of logo in ROI
img1_bg = cv2.bitwise_and(roi, roi, mask=mask_inv)
# Take only region of logo from logo image
img2_fg = cv2.bitwise_and(img2, img2, mask=mask)
# Put logo in ROI and modify the main image
dst = cv2.add(img1_bg, img2_fg)
img1[0:rows, 0:cols] = dst
cv2.imshow('res', img1)
cv2.waitKey(0)
cv2.destroyAllWindows()
结果展示:
plt.rcParams['figure.figsize'] = (9, 6)
plt.subplot(1,3,1), plt.imshow(mask, 'gray')
plt.subplot(1,3,2), plt.imshow(img1_bg, 'gray')
plt.subplot(1,3,3), plt.imshow(img2_fg, 'gray')
plt.show()
3 性能测量和改善技术
3.1 目标
在图像处理当中,因为每秒要处理大量的操作,需要强制你的代码不止要提供正确的解答,还要以最快的方式。所以在这一章中将会学到:
- 测量代码性能
- 一些改善代码性能的技巧
- 函数:cv2.getTickCount, cv2.getTickFrequency等
除了OpenCV,Python也提供一个time模块来测量执行时间,profile模块来得到代码的细节报告,例如,在代码中的每个函数花费多长时间,函数被调用多少次等等。但是,如果你正在使用IPython的话,所有的这些特征以user-friendly方式被集成到。我们将看到一些重要的特征,更多的细节可以检查额外资源中的链接。
3.2 用OpenCV度量性能
cv2.getTickCount函数返回从参考事件(如机器开关被打开的时刻)到这个函数被调用的时钟周期数量。所以,如果你在函数执行前后调用它,你可以得到执行这个函数的时钟数。
cv2.getTickFrequency函数返回时钟频率(即每秒的时钟数)。所以如果要知道执行了多少秒,可以如下操作:
e1 = cv2.getTickCount()
# your code execution
e2 = cv2.getTickCount()
time = (e2 - e1)/cv2.getTickFrequency()
time
# 输出:0.000238761
我们用下面的例子来论证。下面的例子应用核几率大小从5到49的中值滤波(median filtering)(不要担心结果是什么,这不是我们所关心的)。
img1 = cv2.imread('messi5.jpg')
e1 = cv2.getTickCount()
for i in range(5, 49, 2):
img1 = cv2.medianBlur(img1, i)
e2 = cv2.getTickCount()
t = (e2 - e1)/cv2.getTickFrequency()
print(t)
# 输出:0.422783205
注意:
你也可以用time模块做同样的事情。用time.time()函数来替代cv2.getTickCount,然后计算两次时间的差值。
3.3 OpenCV中的默认优化
OpenCV中很多函数用SSE2, AVX等优化,也包含未经优化的代码。所以,如果我们的系统支持这些特征,我们应该利用它们(现在几乎所有的处理器都支持它们)。编译的时候默认启动,所以,如果启动的话,OpenCV运行优化过的代码,否则运行未优化过的代码。你可以用cv2.useOptimized()检查是否被启动或关闭,用cv2.setUseOptimized()来启动或关闭。下面看一个简单的例子:
可以看到,优化过得中值滤波是未优化版本的2倍快。如果检查它的源码,你会发现中值滤波是经由SIMD优化的。所以你可以用这个在代码前面启动优化器(记住,默认启动)。
注意:
上面的图像展示结果是官网上的,下面图像是自己电脑上的运行结果,所以上面得到的结论没有完美体现。
3.4 在IPython中度量性能
有时你可能需要比较两个相似操作的性能。IPython给你一个神奇的命令%timeit去执行这个。它会运行几次代码来得到更精确的结果。强调一下,它们适合度量单行代码。
例如,你知道下面暗中加法操作更好,\(x = 5; y = x**2\), \(x = 5; y = x*x\), \(x = np.uint8([5]); y = x*x\), \(y = np.square(x)\) ?我们将在IPython Shell中用%timeit测量。
可以看到,\(x = 5; y = x*x\)最快,而且比Numpy快很多倍。如果也考虑到数组创建,可能会快100倍。
注意:Python的标量操作比Numpy的标量操作要快。所以对于包含一个或两个元素的操作,Python标量比Numpy数组更好。当数组比较大的时候,Numpy就占优了。
我们尝试另一个例子,这次,我们对同一图像比较cv2.countNonZero()和np.count_nonzero()的性能。
3.5 更多IPython魔法命令
有几个其他的魔法命令来度量性能,profiling, line profiling, memory measurement等,它们都有完整文档。
3.6 性能优化技术
有几种技术和编程方法来利用Python和Numpy的最大性能,在这里我们只记录相关的,其他资料可以查看链接。在这里记录的主要是:先用一种简单的范式实施算法,一旦它行得通,剖析(profile)它来找到瓶颈,并优化。
- 在Python中尽可能避免用循环,尤其双层/三层循环等,它们天生就慢。
- 因为Numpy和OpenCV对向量操作有优化,所以尽可能的向量化算法/代码。
- 利用内存一致性。
- 除非必须,否则不要复制数组。尝试用视图(view)来代替复制,数组复制是很耗时的操作。
做完上面所有的这些操作之后,如果你的代码仍然很慢,或者大的循环不可避免,利用像Cython这样的库使之运行更快。
3.7 额外资源
- Python Optimization Techniques
- Scipy Lecture Notes - Additional Numpy
- Timing and Profiling in IPython 链接失效