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)的感觉。图像相加的等式如下:

\[g(x) = (1-\alpha)f_0(x) + \alpha f_1(x) \]

通过改变\(\alpha\)从0到1的值,可以实现从一个图像到另一个图像的转变。

这里,我们拿两个图像混合在一起。第一个图像给予0.7的权重,第二个图像给予0.3的权重。cv2.addWeighted()在图像上应用下面的等式:

\[dst = \alpha \cdot img1 + \beta \cdot img2 + \gamma \]

这里\(\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 额外资源

  1. Python Optimization Techniques
  2. Scipy Lecture Notes - Additional Numpy
  3. Timing and Profiling in IPython 链接失效

4 Mathematical Tools in OpenCV

posted @ 2018-07-14 17:24  Hiidiot  阅读(318)  评论(0编辑  收藏  举报