在本文中,你将学习图像的基本操作,如像素编辑、几何变换、代码优化、一些数学工具等。
学习读取和编辑像素值,使用图像 ROI 和其他基本操作。主要是以下四点:
访问像素值并修改它们
访问图像属性
设置感兴趣区域 (ROI)
拆分和合并图像
本节中几乎所有的操作都主要与 Numpy 相关,而不是 OpenCV。使用 OpenCV 编写更好的优化代码需要良好的 Numpy 知识。
>>> import numpy as np
>>> import cv2 as cv
>>> img = cv.imread('messi5.jpg')
接下来就可以通过其行和列坐标访问像素值。对于 BGR 图像,它返回一个包含蓝色、绿色、红色值的数组。对于灰度图像,只返回相应的强度:
>>> px = img[100,100] #访问坐标(100,100)处的像素值
>>> print( px ) #打印出来的是BGR,也就是蓝、绿、红、对应的值
[157 166 200]
# #访问B通道像素值,那么传入索引 0,相应的访问 R 通道,就是 2
>>> blue = img[100,100,0]
>>> print( blue )
157
>>> red = img[100,100,2]
>>> print( red )
200
我们可以直接修改某一坐标的像素值:
>>> img[100,100] = [255,255,255]
>>> print( img[100,100] )
[255 255 255]
Numpy 是一个用于快速数组计算的优化库。因此,简单地访问每个像素值并对其进行修改将非常慢,上述代码仅用于演示,不是推荐的做法。
更优雅的访问并修改像素的做法是这样的:
# 访问坐标10,10 出的 R 值
>>> img.item(10,10,2)
59
# 修改坐标10,10 出的 R 值
>>> img.itemset((10,10,2),100)
>>> img.item(10,10,2)
100
获取图片的形状:
>>> print( img.shape )
(342, 548, 3)
342 是高,也就是有多少行像素值,548 是宽,也就是有多少列像素值,而 3 代表 3 通道,表示这是个彩色图而不是灰度图。如果是灰度图,那么返回的结果只有高和宽。
获取总的像素数:342*548*3 = 562248
>>> print( img.size )
562248
获取图片的数据类型:
>>> print( img.dtype )
uint8
因为像素的最大值就是 255,因此,8 位够用了。
有时,我们将不得不使用某些图像区域。比如,对于图像中的眼睛检测,首先对整个图像进行人脸检测。当获得人脸时,我们只选择人脸区域并在其中搜索眼睛,而不是搜索整个图像。它提高了准确性(因为眼睛总是在脸)和性能(因为我们在一个小区域内搜索)。
使用 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))
但是,cv.split 效率没有下面使用索引的方式高:
>>> b = img[:,:,0]
修改也可以用索引,比如你想把所有的红色值设为 0:
>>> img[:,:,2] = 0
有时候,我们想为图片加上边框,比如相框,可以使用 cv.copyMakeBorder()。但它在卷积运算、零填充等方面有更多应用。此函数采用以下参数:
下面是一个示例代码,演示了所有这些边框类型,以便你可以更好地理解:
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()
效果如下所示:
对图像执行算术运算,如加法、减法、按位运算等。主要使用这些函数: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]
这也是图像添加,但为图像赋予不同的权重,以便给人一种混合或透明的感觉。根据以下等式添加图像:
α 从 0→1。。在这里我们把两张图像混合在一起。第一张图片的权重为 0.7,第二张图片的权重为 0.3。cv.addWeighted() 将以下等式应用于图像,这里 γ 是 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()
效果如下:
这包括按位与、或、非和异或运算。它们在提取图像的任何部分(我们将在接下来的章节中看到)、定义和使用非矩形 ROI 等时非常有用。下面我们将看到一个如何更改图像特定区域的示例。
比如将 OpenCV logo 放在图像上方。如果我添加两个图像,它会改变颜色。如果我混合它们,我会得到透明的效果。但我希望它是不透明的。如果它是一个矩形区域,我可以使用 ROI。但是 OpenCV 标志不是一个矩形。因此,你可以使用按位运算来完成,如下所示:
# 读取两个图片
img1 = cv.imread('messi5.jpg')
img2 = cv.imread('opencv-logo-white.png')
# 我想把 logo 放左上角,因此创建一个 ROI
rows,cols,channels = img2.shape
roi = img1[0:rows, 0:cols]
# 现在创建一个 logo 蒙版并创建其反向蒙版
img2gray = cv.cvtColor(img2,cv.COLOR_BGR2GRAY)
ret, mask = cv.threshold(img2gray, 10, 255, cv.THRESH_BINARY)
mask_inv = cv.bitwise_not(mask)
# 现在将 ROI 中的 logo 区域涂黑
img1_bg = cv.bitwise_and(roi,roi,mask = mask_inv)
# 从 logo 图像中仅获取 logo 区域
img2_fg = cv.bitwise_and(img2,img2,mask = mask)
# 放置 logo
dst = cv.add(img1_bg,img2_fg)
img1[0:rows, 0:cols ] = dst
cv.imshow('res',img1)
cv.waitKey(0)
cv.destroyAllWindows()
效果图:
获得解决方案很重要。但是以最快的方式获得它更重要。
在图像处理中,由于你每秒处理大量操作,因此你的代码不仅要提供正确的解决方案,而且还要以最快的方式提供解决方案,这是必须的。接下来,我们看一下如何衡量代码的性能和一些提高代码性能的技巧。
会用到这些函数:cv.getTickCount、cv.getTickFrequency 等。
除了 OpenCV,Python 还提供了一个模块 time,有助于测量执行时间。另一个模块 profile 有助于获得关于代码的详细报告,例如代码中每个函数花费了多少时间,函数被调用了多少次等。
cv.getTickCount 函数返回从机器开启的那一刻到调用此函数的那一刻的时钟周期数。因此,如果你在函数执行之前和之后调用它,你将获得用于执行函数的时钟周期数。
cv.getTickFrequency 函数返回时钟周期的频率,或每秒的时钟周期数。因此,要以秒为单位查找执行时间,你可以执行以下操作:
e1 = cv.getTickCount()
# 这里放置你的代码
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 )
# 0.521107655 seconds
你也可以使用 time 模块来计时
许多 OpenCV 函数都使用 SSE2、AVX 等进行了优化。它还包含未优化的代码。因此,如果我们的系统支持这些功能,我们应该利用它们(几乎所有现代处理器都支持它们)。编译时默认启用。所以 OpenCV 如果启用则运行优化的代码,否则运行未优化的代码。你可以使用 cv.useOptimized() 检查它是否启用/禁用,并使用 cv.setUseOptimized() 启用/禁用它。让我们看一个简单的例子。
# 检查优化是否启用
In [5]: cv.useOptimized()
Out[5]: True
In [6]: %timeit res = cv.medianBlur(img,49)
10 loops, best of 3: 34.9 ms per loop
# 禁用优化
In [7]: cv.setUseOptimized(False)
In [8]: cv.useOptimized()
Out[8]: False
In [9]: %timeit res = cv.medianBlur(img,49)
10 loops, best of 3: 64.1 ms per loop
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
有几种技术和编码方法可以发挥 Python 和 Numpy 的最大性能。此处仅注明相关内容,并提供重要来源的链接。这里要注意的主要是,首先尝试以简单的方式实现算法。一旦它开始工作,就对其进行分析,找到瓶颈再对其进行优化。
尽可能避免在 Python 中使用循环,尤其是双/三循环等。它们天生就很慢。
尽可能将算法/代码矢量化,因为 Numpy 和 OpenCV 针对矢量运算进行了优化。
利用缓存一致性。
除非必要,否则切勿复制数组。尝试改用视图。数组复制是一项昂贵的操作。
如果你的代码在执行完所有这些操作后仍然很慢,或者如果不可避免地使用大循环,请使用 Cython 等其他库来加快速度。
可以参考:
Python 优化技术[1]
Numpy 高级操作[2]
IPython 中的时序和分析[3]
如有帮助,在看支持。
参考资料[1]
Python 优化技术: https://wiki.python.org/moin/PythonSpeed/PerformanceTips
[2]Numpy 高级操作: https://scipy-lectures.github.io/advanced/advanced_numpy/index.html#advanced-numpy
[3]IPython 中的时序和分析: https://pynash.org/2013/03/06/timing-and-profiling/
上一篇:
OpenCV 教程 01:简介与安装,图片与视频的基本操作