目录
- 2.1 图像的基本操作
- 目标
- 访问和修改像素值
- 访问图像属性
- 图像ROI
- 分割和合并图像通道
- 为图像制作边框(填充)
- 2.2 图像上的算术操作
- 目标
- 图像加法
- 图像混合
- 位操作
- 练习
- 2.3 性能测量和改进技术
- 目标
- 用OpenCV测量性能
- OpenCV中的默认优化
- 更多的IPython魔法命令
- 性能优化技术
- 额外的资源
2.1 图像的基本操作
翻译及二次校对:cvtutorials.com
编辑者:廿瓶鲸(和鲸社区Siby团队成员)
目标
学会:
- 访问像素值并修改它们
- 访问图像属性
- 设置感兴趣的区域(ROI)
- 分割和合并图像
本节中几乎所有的操作都主要与Numpy而不是OpenCV有关。要想用OpenCV写出更好的优化代码,需要有良好的Numpy知识。
(例子将在Python终端中显示,因为大多数只是单行的代码)
访问和修改像素值
让我们先加载一个彩色图像。
>>> import numpy as np
>>> import cv2 as cv
>>> img = cv.imread('messi5.jpg')
你可以通过其行和列坐标来访问一个像素值。对于BGR图像,它返回一个蓝、绿、红值的数组。对于灰度图像,只返回相应的强度。
>>> px = img[100,100]
>>> print( px )
[157 166 200]
# accessing only blue pixel
>>> blue = img[100,100,0]
>>> print( blue )
157
你可以用同样的方法修改像素值。
>>> 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
访问图像属性
图像属性包括行、列和通道的数量;图像数据的类型;像素的数量等。
图像的形状是由img.shape访问的。它返回一个包含行数、列数和通道数(如果图像是彩色的)的元组。
>>> print( img.shape )
(342, 548, 3)
如果一个图像是灰度的,返回的元组只包含行和列的数量,所以这是一个很好的方法来检查加载的图像是灰度还是彩色。
总像素数由Img.size访问。
>> print( img.size )
562248
图像数据类型由img.type
获得。
>> print( img.dtype )
uint8
注意:img.dtype在调试时非常重要,因为OpenCV-Python代码中大量的错误是由无效的数据类型引起的。
图像ROI
有时,你必须对图像的某些区域进行处理。对于图像中的眼睛检测,首先在整个图像上进行人脸检测。当得到一个人脸时,我们单独选择人脸区域并在其中搜索眼睛,而不是搜索整个图像。它提高了准确性(因为眼睛总是在脸上)和性能(因为我们在一个小区域内搜索)。
使用Numpy索引再次获得ROI。这里我选择了球,并将其复制到图像的另一个区域。
>> ball = img[280:340, 330:390]
>> img[273:333, 100:160] = ball
看看下面的结果:
分割和合并图像通道
有时你需要分别处理图像的B、G、R通道。在这种情况下,你需要将BGR图像分割成单个通道。在其他情况下,你可能需要将这些单独的通道连接起来以创建一个BGR图像。你可以通过以下方式简单地做到这一点。
>>> b,g,r = cv.split(img)
>>> img = cv.merge((b,g,r))
或者:
>>> b = img[:,:,0]
假设你想把所有的红色像素设置为零–你不需要先分割通道。Numpy索引的速度更快。
>>> img[:,:,2] = 0
警告:cv.split()是一个耗时的操作。所以只有在必要时才使用它。否则,请使用Numpy索引。
为图像制作边框(填充)
如果你想在图像周围创建一个边框,类似于一个相框,你可以使用cv.copyMakeBorder()。但它在卷积操作、零填充等方面有更多应用。这个函数需要以下参数。
- src - 输入图像
- top, bottom, left, right - 相应方向的边框宽度,以像素数计
- borderType - 定义要添加的边界类型的标志。它可以是以下类型: cv.BORDER_CONSTANT - 添加一个恒定颜色的边框。该值应作为下一个参数给出; cv.BORDER_REFLECT - 边框将是边框元素的镜像反射,像这样:Fedcba|abcdefgh|hgfedcb; cv.BORDER_REFLECT_101 或 cv.BORDER_DEFAULT - 与上述相同,但有轻微变化,像这样:gfedcb|abcdefgh|gfedcba; cv.BORDER_REPLICATE - 最后一个元素被整体复制,像这样:aaaaa|abcdefgh|hhhhh; cv.BORDER_WRAP - 无法解释,看起来会像这样:cdefgh|abcdefgh|abcdefg;
- value - 如果边框类型是cv.BORDER_CONSTANT,它指的是边框的颜色。
下面是一个演示所有这些边框类型的示例代码,以便更好地理解:
import cv2 as cv
import numpy as np
from matplotlib import pyplot as plt
BLUE = [255,0,0]
img1 = cv.imread('opencv-logo.png')
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=BLUE)
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显示的。所以红色和蓝色通道将被替换)。
2.2 图像上的算术操作
翻译及二次校对:cvtutorials.com
目标
- 学习图像上的几种算术运算,如加法、减法、位运算等。
- 学习这些函数:cv.add(), cv.addWeighted(), 等等。
图像加法
你可以用OpenCV函数cv.add()将两幅图像相加,或者简单地用numpy操作res = img1 + img2。两幅图像应该是相同的深度和类型,或者第二幅图像可以只是一个标量值。
注意:OpenCV的加法和Numpy的加法是有区别的。OpenCV加法是一个饱和操作,而Numpy加法是一个模数操作。
例如,考虑下面的例子:
>>> x = np.uint8([250])
>>> y = np.uint8([10])
>>> print( cv.add(x,y) ) # 250+10 = 260 => 255
[[255]]
>>> print( x+y ) # 250+10 = 260 % 256 = 4
[4]
当你添加两张图片时,这将更加明显。请使用OpenCV函数,因为它们会提供一个更好的结果。
图像混合
这也是图像添加,但对图像给予不同的权重,以便给人以混合或透明的感觉。图像的添加是按照下面的公式进行的:
g
(
x
)
=
(
1
−
α
)
f
0
(
x
)
+
α
f
1
(
x
)
g(x) = (1 - \alpha)f_{0}(x) + \alpha f_{1}(x)
g(x)=(1−α)f0(x)+αf1(x)
通过改变α从0→1,你可以在一个图像和另一个图像之间进行很酷的过渡。
这里我取了两张图片来混合。第一张图片的权重为0.7,第二张图片的权重为0.3。cv.addWeighted()对图片应用了以下公式。
d
s
t
=
α
⋅
i
m
g
1
+
β
⋅
i
m
g
2
+
γ
dst = \alpha \cdot img1 + \beta \cdot img2 + \gamma
dst=α⋅img1+β⋅img2+γ
这里的γ取0。
img1 = cv.imread('ml.png')
img2 = cv.imread('opencv-logo.png')
dst = cv.addWeighted(img1,0.7,img2,0.3,0)
cv.imshow('dst',dst)
cv.waitKey(0)
cv.destroyAllWindows()
请看下面的结果:
位操作
这包括按位的AND、OR、NOT和XOR操作。它们在提取图像的任何部分(正如我们将在接下来的章节中看到的那样)、定义和处理非矩形的ROI等方面将非常有用。下面我们将看到一个如何改变图像中某一区域的例子。
我想把OpenCV的标志放在一张图片上面。如果我将两张图片相加,它将改变颜色。如果我把它们混合起来,我就会得到一个透明的效果。但我希望它是不透明的。如果它是一个矩形区域,我可以像我们在上一章做的那样使用ROI。但是OpenCV的标志不是一个矩形的形状。所以你可以用位操作来做,如下图所示。
# Load two images
img1 = cv.imread('messi5.jpg')
img2 = cv.imread('opencv-logo-white.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 = cv.cvtColor(img2,cv.COLOR_BGR2GRAY)
ret, mask = cv.threshold(img2gray, 10, 255, cv.THRESH_BINARY)
mask_inv = cv.bitwise_not(mask)
# Now black-out the area of logo in ROI
img1_bg = cv.bitwise_and(roi,roi,mask = mask_inv)
# Take only region of logo from logo image.
img2_fg = cv.bitwise_and(img2,img2,mask = mask)
# Put logo in ROI and modify the main image
dst = cv.add(img1_bg,img2_fg)
img1[0:rows, 0:cols ] = dst
cv.imshow('res',img1)
cv.waitKey(0)
cv.destroyAllWindows()
请看下面的结果。左图是我们创建的遮罩。右图是最终的结果。为了更好地理解,显示上述代码中的所有中间图像,特别是img1_bg和img2_fg。
练习
使用cv.addWeighted函数创建一个文件夹中图片的幻灯片,并在图片之间平滑过渡。
2.3 性能测量和改进技术
翻译及二次校对:cvtutorials.com
目标
在图像处理中,由于你要每秒处理大量操作,你的代码不仅要提供正确的解决方案,而且要以最快的方式提供,这是必须的。因此,在本章中,你将学习:
- 测试代码的性能。
- 一些提高代码性能的技巧。
- 你会看到这些函数:cv.getTickCount, cv.getTickFrequency,等等。
除了OpenCV之外,Python还提供了一个模块time,这对测量执行时间很有帮助。另一个模块profile有助于获得代码的详细报告,比如代码中每个函数花了多少时间,函数被调用了多少次,等等。但是,如果你使用的是IPython,所有这些功能都以一种用户友好的方式整合在一起。我们将看到一些重要的功能,更多的细节,请查看附加资源部分的链接。
用OpenCV测量性能
cv.getTickCount函数返回一个参考事件(比如机器被打开的那一刻)到这个函数被调用的那一刻之后的时钟周期的数量。因此,如果你在函数执行之前和之后调用它,你可以得到执行一个函数所使用的时钟周期数。
cv.getTickFrequency函数返回时钟周期的频率,或每秒的时钟周期数。所以要找到以秒为单位的执行时间,你可以做以下工作。
e1 = cv.getTickCount()
# your code execution
e2 = cv.getTickCount()
time = (e2 - e1)/ cv.getTickFrequency()
我们将用下面的例子来证明。下面的例子应用中值滤波,其内核大小从5到49不等。 不要担心结果会是什么样子–那不是我们的目标:
img1 = cv.imread('messi5.jpg')
e1 = cv.getTickCount()
for i in range(5,49,2):
img1 = cv.medianBlur(img1,i)
e2 = cv.getTickCount()
t = (e2 - e1)/cv.getTickFrequency()
print( t )
# Result I got is 0.521107655 seconds
你可以用时间模块做同样的事情。不使用cv.getTickCount,而使用time.time()函数。然后取这两个时间的差值。
OpenCV中的默认优化
OpenCV的许多函数都使用SSE2,AVX等进行了优化。它也包含未经优化的代码。因此,如果我们的系统支持这些功能,我们应该利用它们(几乎所有的现代处理器都支持它们)。在编译的时候,它是默认启用的。所以,如果OpenCV启用了优化代码,它就会运行优化的代码,否则就会运行未优化的代码。你可以使用cv.useOptimized()来检查它是否被启用/禁用,cv.setUseOptimized()来启用/禁用它。让我们看一个简单的例子。
In [10]: x = 5
In [11]: %timeit y=x**2
10000000 loops, best of 3: 73 ns per loop
In [12]: %timeit y=x*x
10000000 loops, best of 3: 58.3 ns per loop
In [15]: z = np.uint8([5])
In [17]: %timeit y=z*z
1000000 loops, best of 3: 1.25 us per loop
In [19]: %timeit y=np.square(z)
1000000 loops, best of 3: 1.16 us per loop
你可以看到,x = 5 ; y = x*x是最快的,与Numpy相比,它大约快20倍。如果你也考虑到数组的创建,它可能达到100倍的速度。(Numpy的开发者们正在解决这个问题)。
注意:Python的标量操作要比Numpy的标量操作快。所以对于包括一个或两个元素的操作,Python标量比Numpy数组更好。当数组的大小稍微大一点时,Numpy有优势。
我们将再试一个例子。这一次,我们将比较cv.countNonZero()和np.count_nonzero()对同一图像的性能:
In [35]: %timeit z = cv.countNonZero(img)
100000 loops, best of 3: 15.8 us per loop
In [36]: %timeit z = np.count_nonzero(img)
1000 loops, best of 3: 370 us per loop
看,OpenCV函数比Numpy函数快了近25倍。
注意:通常情况下,OpenCV函数比Numpy函数快。所以对于同样的操作,OpenCV函数是首选。但是,也可能有例外,特别是当Numpy使用视图而不是拷贝时。
更多的IPython魔法命令
还有其他一些神奇的命令来测量性能、剖析、行剖析、内存测量等等。它们都有很好的文档。所以这里只提供这些文档的链接。建议有兴趣的读者可以尝试一下。
性能优化技术
有几种技术和编码方法可以发挥Python和Numpy的最大性能。这里只指出了相关的技术和方法,并给出了重要来源的链接。这里需要注意的是,首先尝试以一种简单的方式实现算法。一旦它开始工作,对它进行剖析,找到瓶颈,并对其进行优化。
1.尽可能避免在Python中使用循环,特别是双倍/三倍循环等。它们本身就很慢。
2.尽可能地将算法/代码矢量化,因为Numpy和OpenCV是为矢量操作而优化的。
3.利用高速缓存的一致性。
4.除非有必要,否则不要对数组进行复制。尽量使用视图来代替。阵列的复制是一个昂贵的操作。
如果你的代码在做完所有这些操作后仍然很慢,或者不可避免地要使用大的循环,请使用额外的库,如Cython,使其更快。
额外的资源
- Python优化技术
- Scipy讲义–高级Numpy
- IPython中的计时和剖析