部分 III
核心操作
9 图像的基础操作
目标
• 获取像素值并修改
• 获取图像的属性(信息)
• 图像的 ROI()
• 图像通道的拆分及合并
几乎所有这些操作与 Numpy 的关系都比与 OpenCV 的关系更加紧密,因此熟练 Numpy 可以帮助我们写出性能更好的代码。
(示例将会在 Python 终端中展示,因为他们大部分都只有一行代码)
9.1 获取并修改像素值
首先我们需要读入一幅图像:
import cv2
import numpy as np
img=cv2.imread('messi5.jpg')
你可以根据像素的行和列的坐标获取他的像素值。对 BGR 图像而言,返回值为 B,G,R 的值。对灰度图像而言,会返回他的灰度值(亮度?intensity)
import cv2
import numpy as np
img=cv2.imread('messi5.jpg')
px=img[100,100]
print(px)
blue=img[100,100,0]
print(blue)
你可以以类似的方式修改像素值。
import cv2
import numpy as np
img=cv2.imread('messi5.jpg')
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() 分割他们。
获取像素值及修改的更好方法。
import cv2
import numpy as np
img=cv2.imread('messi5.jpg')
print(img.item(10,10,2))
img.itemset((10,10,2),100)
print(img.item(10,10,2))
# 59
# 100
9.2 获取图像属性
图像的属性包括:行,列,通道,图像数据类型,像素数目等img.shape 可以获取图像的形状。他的返回值是一个包含行数,列数,通道数的元组。
import cv2
import numpy as np
img=cv2.imread('messi5.jpg')
print(img.shape)
##(342, 548, 3)
注意:如果图像是灰度图,返回值仅有行数和列数。所以通过检查这个返回值就可以知道加载的是灰度图还是彩色图。
img.size 可以返回图像的像素数目:
注意:在debug时 img.dtype 非常重要。因为在 OpenCV Python 代码中经常出现数据类型的不一致。
9.3 图像 ROI
有时你需要对一幅图像的特定区域进行操作。例如我们要检测一副图像中眼睛的位置,我们首先应该在图像中找到脸,再在脸的区域中找眼睛,而不是直接在一幅图像中搜索。这样会提高程序的准确性和性能。
ROI 也是使用 Numpy 索引来获得的。现在我们选择球的部分并把他拷贝到图像的其他区域。
import cv2
import numpy as np
img=cv2.imread('messi5.jpg')
ball=img[280:340,330:390]
img[273:333,100:160]=ball
img=cv2.imshow('test', img)
cv2.waitKey(0)
看看结果吧:
9.4 拆分及合并图像通道
有时我们需要对 BGR 三个通道分别进行操作。这是你就需要把 BGR 拆分成单个通道。有时你需要把独立通道的图片合并成一个 BGR 图像。你可以这样做:
import cv2 import numpy as np img=cv2.imread('/home/duan/workspace/opencv/images/roi.jpg') b,g,r=cv2.split(img) img=cv2.merge(b,g,r)
或者:
import cv2 import numpy as np img=cv2.imread('/home/duan/workspace/opencv/images/roi.jpg') b=img[:,:,0]
假如你想使所有像素的红色通道值都为 0,你不必先拆分再赋值。你可以直接使用 Numpy 索引,这会更快。
import cv2 import numpy as np img=cv2.imread('/home/duan/workspace/opencv/images/roi.jpg') img[:,:,2]=0
警告:cv2.split() 是一个比较耗时的操作。只有真正需要时才用它,能用Numpy 索引就尽量用。
9.5 为图像扩边(填充)
如果你想在图像周围创建一个边,就像相框一样,你可以使用 cv2.copyMakeBorder()函数。这经常在卷积运算或 0 填充时被用到。这个函数包括如下参数:
• src 输入图像
• top, bottom, left, right 对应边界的像素数目。
• borderType 要添加那种类型的边界,类型如下:
– cv2.BORDER_CONSTANT 添加有颜色的常数值边界,还需要下一个参数(value)。
– cv2.BORDER_REFLECT 边界元素的镜像。比如: fedcba|abcde-fgh|hgfedcb
– cv2.BORDER_REFLECT_101 or 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 BLUE=[255,0,0] img1=cv2.imread('opencv_logo.png') replicate = cv2.copyMakeBorder(img1,10,10,10,10,cv2.BORDER_REPLICATE) reflect = cv2.copyMakeBorder(img1,10,10,10,10,cv2.BORDER_REFLECT) reflect101 = cv2.copyMakeBorder(img1,10,10,10,10,cv2.BORDER_REFLECT_101) wrap = cv2.copyMakeBorder(img1,10,10,10,10,cv2.BORDER_WRAP) constant= cv2.copyMakeBorder(img1,10,10,10,10,cv2.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 绘制,所以交换 R 和 B 的位置,OpenCV 中是按 BGR,matplotlib 中是按 RGB 排列):
10 图像上的算术运算
目标
• 学习图像上的算术运算,加法,减法,位运算等。
• 我们将要学习的函数与有:cv2.add(),cv2.addWeighted() 等。
10.1 图像加法
你可以使用函数 cv2.add() 将两幅图像进行加法运算,当然也可以直接使用 numpy,res=img1+img。两幅图像的大小,类型必须一致,或者第二个图像可以使一个简单的标量值。
注意:OpenCV 中的加法与 Numpy 的加法是有所不同的。OpenCV 的加法是一种饱和操作,而 Numpy 的加法是一种模操作。
例如下面的两个例子:
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 中的函数。
10.2 图像混合
这其实也是加法,但是不同的是两幅图像的权重不同,这就会给人一种混合或者透明的感觉。图像混合的计算公式如下:
g (x) = (1 − α)f 0 (x) + αf 1 (x)
通过修改 α 的值(0 → 1),可以实现非常酷的混合。
现在我们把两幅图混合在一起。第一幅图的权重是 0.7,第二幅图的权重是 0.3。函数 cv2.addWeighted() 可以按下面的公式对图片进行混合操作。
dst = α · img1 + β · img2 + γ
这里 γ 的取值为 0。
img1 = cv2.imread('ml.png') img2 = cv2.imread('opencv_logo.jpg') dst = cv2.addWeighted(img1,0.7,img2,0.3,0) cv2.imshow('dst',dst) cv2.waitKey(0) cv2.destroyAllWindows()
dst = cv2.addWeighted(img1,0.7,img2,0.3,0) error: C:\projects\opencv-python\opencv\modules\core\src\arithm.cpp:659:
error: (-209) The operation is neither 'array op array' (where arrays have
the same size and the same number of channels), nor 'array op scalar',
nor 'scalar op array' in function cv::arithm_op
下面就是结果:# 这个运行有问题
10.3 按位运算
这里包括的按位操作有:AND,OR,NOT,XOR 等。当我们提取图像的一部分,选择非矩形 ROI 时这些操作会很有用(下一章你就会明白)。下面的例子就是教给我们如何改变一幅图的特定区域。我想把 OpenCV 的标志放到另一幅图像上。如果我使用加法,颜色会改变,如果使用混合,会得到透明效果,但是我不想要透明。如果他是矩形我可以象上一章那样使用 ROI。但是他不是矩形。但是我们可以通过下面的按位运算实现:
import cv2
import numpy as np
# Load two images
img1 = cv2.imread('messi5.jpg')
img2 = cv2.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 = 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()
结果如下。左面的图像是我们创建的掩码。右边的是最终结果。为了帮助大家理解我把上面程序的中间结果也显示了出来,特别是 img1_bg 和 img2_fg。
练习
1. 创建一个幻灯片用来演示一幅图如何平滑的转换成另一幅图(使用函数cv2.addWeighted)
11 程序性能检测及优化
目标
在图像处理中你每秒钟都要做大量的运算,所以你的程序不仅要能给出正确的结果,同时还必须要快。所以这节我们将要学习:
• 检测程序的效率
• 一些能够提高程序效率的技巧
• 你要学到的函数有:cv2.getTickCount,cv2.getTickFrequency等
除了 OpenCV,Python 也提供了一个叫 time 的的模块,你可以用它来测量程序的运行时间。另外一个叫做 profile 的模块会帮你得到一份关于你的程序的详细报告,其中包含了代码中每个函数运行需要的时间,以及每个函数被调用的次数。如果你正在使用 IPython 的话,所有这些特点都被以一种用户友好的方式整合在一起了。我们会学习几个重要的,要想学到更加详细的知识就打开更多资源中的链接吧。
11.1 使用 OpenCV 检测程序效率
cv2.getTickCount 函数返回从参考点到这个函数被执行的时钟数。所以当你在一个函数执行前后都调用它的话,你就会得到这个函数的执行时间(时钟数)。
cv2.getTickFrequency 返回时钟频率,或者说每秒钟的时钟数。所以你可以按照下面的方式得到一个函数运行了多少秒:
e1 = cv2.getTickCount() # your code execution e2 = cv2.getTickCount() time = (e2 - e1)/ cv2.getTickFrequency()
我们将会用下面的例子演示。下面的例子是用窗口大小不同(5,7,9)的核函数来做中值滤波:
img1 = cv2.imread('messi5.jpg') e1 = cv2.getTickCount() for i in xrange(5,49,2): img1 = cv2.medianBlur(img1,i) e2 = cv2.getTickCount() t = (e2 - e1)/cv2.getTickFrequency() print t # Result I got is 0.521107655 seconds
注 意: 你 也 可 以 中 time 模 块 实 现 上 面 的 功 能。 但 是 要 用 的 函 数 是time.time() 而不是 cv2.getTickCount。比较一下这两个结果的差别吧。
11.2 OpenCV 中的默认优化
OpenCV 中的很多函数都被优化过(使用 SSE2,AVX 等)。也包含一些没有被优化的代码。如果我们的系统支持优化的话要尽量利用只一点。在编译时优化是被默认开启的。因此 OpenCV 运行的就是优化后的代码,如果你把优化关闭的话就只能执行低效的代码了。你可以使用函数 cv2.useOptimized()来查看优化是否被开启了,使用函数 cv2.setUseOptimized() 来开启优化。
让我们来看一个简单的例子吧。
# check if optimization is enabled In [5]: cv2.useOptimized() Out[5]: True In [6]: %timeit res = cv2.medianBlur(img,49) 10 loops, best of 3: 34.9 ms per loop # Disable it In [7]: cv2.setUseOptimized(False) In [8]: cv2.useOptimized() Out[8]: False In [9]: %timeit res = cv2.medianBlur(img,49) 10 loops, best of 3: 64.1 ms per loop
看见了吗,优化后中值滤波的速度是原来的两倍。如果你查看源代码的话,你会发现中值滤波是被 SIMD 优化的。所以你可以在代码的开始处开启优化(你要记住优化是默认开启的)。
11.3 在 IPython 中检测程序效率
有时你需要比较两个相似操作的效率,这时你可以使用 IPython 为你提供的魔法命令%time。他会让代码运行好几次从而得到一个准确的(运行)时间。它也可以被用来测试单行代码的。
例如,你知道下面这同一个数学运算用哪种行式的代码会执行的更快吗?
x = 5; y = x ∗ ∗2
x = 5; y = x ∗ x
x = np.uint([5]); y = x ∗ x
y = np.squre(x)
我们可以在 IPython 的 Shell 中使用魔法命令找到答案。
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
竟然是第一种写法,它居然比 Nump 快了 20 倍。如果考虑到数组构建的话,能达到 100 倍的差。
注意:Python 的标量计算比 Nump 的标量计算要快。对于仅包含一两个元素的操作 Python 标量比 Numpy 的数组要快。但是当数组稍微大一点时Numpy 就会胜出了。
我们来再看几个例子。我们来比较一下 cv2.countNonZero() 和
np.count_nonzero()。
In [35]: %timeit z = cv2.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 对视图(而非复制)进行操作时。
11.4 更多 IPython 的魔法命令
还有几个魔法命令可以用来检测程序的效率,profiling,line profiling,内存使用等。他们都有完善的文档。所以这里只提供了超链接。感兴趣的可以自己学习一下。
11.5 效率优化技术
有些技术和编程方法可以让我们最大的发挥 Python 和 Numpy 的威力。
我们这里仅仅提一下相关的,你可以通过超链接查找更多详细信息。我们要说的最重要的一点是:首先用简单的方式实现你的算法(结果正确最重要),当结果正确后,再使用上面的提到的方法找到程序的瓶颈来优化它。
1. 尽量避免使用循环,尤其双层三层循环,它们天生就是非常慢的。
2. 算法中尽量使用向量操作,因为 Numpy 和 OpenCV 都对向量操作进行了优化。
3. 利用高速缓存一致性。
4. 没有必要的话就不要复制数组。使用视图来代替复制。数组复制是非常浪费资源的。
就算进行了上述优化,如果你的程序还是很慢,或者说大的训话不可避免的话,你你应该尝试使用其他的包,比如说 Cython,来加速你的程序。
更多资源
1. Python Optimization Techniques
2. Scipy Lecture Notes - Advanced Numpy
3. Timing and Profiling in IPython