第 3 章
学习 OpenCV 需要的 Numpy 知识
本章共 6 节,介绍 ndarray、资料型态、阵列建立、切片、axis、vstack 与 hstack。
Python 的列表和元组可以表示一维或多维资料,但是在大量数值运算时,执行速度与系统资源会成为问题。
Numpy 是 Numerical Python 的缩写,常用于科学运算、影像处理与人工智能。OpenCV 读取的灰阶图、彩色图和视频画面,都可以看成 Numpy 的多维阵列;每一个数值就是影像中的像素资料。因此,在进入 OpenCV 影像处理前,需要先熟悉 Numpy 阵列的基本操作。
3-1
阵列 ndarray
Numpy 所建立的阵列资料型态称为 ndarray,也就是 n-dimension array,表示 n 维阵列。
例如一维阵列、二维阵列、三维阵列,都属于 ndarray 的范畴。OpenCV 的影像资料在 Python 中也以 ndarray 表示。
ndarray 与 Python 一般列表不同,主要特性如下:
因为阵列大小固定且元素型态一致,Numpy 可以用更有效率的方式储存与运算资料。在处理影像时,这一点非常重要。
3-2
Numpy 的资料型态
Numpy 支援多种资料型态。OpenCV 影像最常见的是 uint8,也就是 8 位元无符号整数,可表示 0 到 255 的像素值。
| 资料型态 | 说明 |
bool | 布尔型态,值为 True 或 False。 |
int | 整数型态,依平台而定。 |
intc | 对应 C 语言的 int。 |
intp | 用于索引的整数型态。 |
int8 | 8 位元有符号整数。 |
int16 | 16 位元有符号整数。 |
int32 | 32 位元有符号整数。 |
int64 | 64 位元有符号整数。 |
uint8 | 8 位元无符号整数,范围 0 到 255。 |
uint16 | 16 位元无符号整数。 |
uint32 | 32 位元无符号整数。 |
uint64 | 64 位元无符号整数。 |
float | 浮点数型态。 |
float16 | 半精度浮点数。 |
float32 | 单精度浮点数。 |
float64 | 双精度浮点数。 |
complex | 复数型态。 |
complex64 | 由两个 32 位元浮点数表示的复数。 |
complex128 | 由两个 64 位元浮点数表示的复数。 |
3-3
建立一维或多维阵列
3-3-1 认识 ndarray 的属性
建立 Numpy 阵列后,常用下列属性查看阵列的资料型态、维度、形状与元素数量。
| 属性 | 说明 |
dtype | 阵列元素的资料型态。 |
itemsize | 阵列中每个元素占用的位元组数。 |
ndim | 阵列的维度。 |
shape | 阵列的形状。 |
size | 阵列的元素总数。 |
3-3-2 使用 array() 建立一维阵列
array() 可以将列表或元组转换成 Numpy 阵列。
语法
numpy.array(object, dtype=None, copy=True, order='K', subok=False, ndmin=0)
| 参数 | 说明 |
object | 要转换为阵列的对象。 |
dtype | 指定阵列元素的资料型态,默认由资料自动判断。 |
copy | 是否复制对象,默认值为 True。 |
order | 储存顺序,可为 K、A、C、F。 |
subok | 是否允许传回子类别,默认值为 False。 |
ndmin | 指定阵列至少要有的维度。 |
实例 1:建立一维阵列
import numpy as np
x = np.array([1, 2, 3])
print(type(x))
print(x)
执行结果
<class 'numpy.ndarray'>
[1 2 3]
建立阵列后,可以使用索引读取阵列元素。索引从 0 开始。
实例 2:读取一维阵列元素
import numpy as np
x = np.array([1, 2, 3])
print(x[0])
print(x[1])
print(x[2])
实例 3:修改一维阵列元素
import numpy as np
x = np.array([1, 2, 3])
x[1] = 10
print(x)
实例 4:查看 ndarray 属性
import numpy as np
x = np.array([1, 2, 3])
print(x.dtype)
print(x.itemsize)
print(x.ndim)
print(x.shape)
print(x.size)
上例中,dtype 表示元素资料型态为 int32,每个元素占用 4 个位元组;ndim 为 1,表示这是一维阵列;shape 为 (3,),表示阵列有 3 个元素;size 也是 3。
实例 5:指定 int8 资料型态
import numpy as np
x = np.array([2, 4, 6], dtype=np.int8)
print(x.dtype)
print(x.itemsize)
实例 6:建立浮点数阵列
import numpy as np
x = np.array([1.1, 2.3, 3.6])
print(x.dtype)
print(x)
print(repr(x))
执行结果
float64
[1.1 2.3 3.6]
array([1.1, 2.3, 3.6])
3-3-3 使用 array() 函数建立多维阵列
如果传入巢状列表,或指定 ndmin,可以建立二维或多维阵列。
ch3_1.py:建立二维与三维阵列
import numpy as np
x1 = np.array([1, 2, 3], ndmin=2)
x2 = np.array([1, 2, 3], ndmin=3)
print(x1)
print(x2)
ch3_1.py 执行结果
ch3_2.py:使用巢状列表建立二维阵列
import numpy as np
x = np.array([[1, 2, 3], [4, 5, 6]])
print(x)
二维阵列可以使用 x[列][行] 或 x[列, 行] 访问元素。例如 x[0][2] 与 x[0, 2] 都表示第 0 列第 2 行的元素。
ch3_3.py:认识二维阵列索引
import numpy as np
x = np.array([[1, 2, 3], [4, 5, 6]])
print(x[0][2])
print(x[1][2])
print(x[0, 2])
print(x[1, 2])
3-3-4 使用 zeros() 建立内容是 0 的多维阵列
语法
np.zeros(shape, dtype=float)
shape 指定阵列形状,dtype 指定资料型态。若未指定 dtype,默认使用浮点数。
ch3_4.py:建立全 0 阵列
import numpy as np
x1 = np.zeros(3)
x2 = np.zeros((2, 3), dtype=np.uint8)
print(x1)
print(x2)
执行结果
[0. 0. 0.]
[[0 0 0]
[0 0 0]]
在 OpenCV 中,像素值为 0 的影像会呈现黑色,因此 zeros() 常用于建立黑色影像或初始化空白影像资料。
3-3-5 使用 ones() 建立内容是 1 的多维阵列
语法
np.ones(shape, dtype=None)
ch3_5.py:建立全 1 阵列
import numpy as np
x1 = np.ones(3)
x2 = np.ones((2, 3), dtype=np.uint8)
print(x1)
print(x2)
执行结果
[1. 1. 1.]
[[1 1 1]
[1 1 1]]
若要建立白色影像,可将 ones() 建立的阵列乘以 255。
ch3_5_1.py:建立全白资料
import numpy as np
x = np.ones((2, 3), dtype=np.uint8) * 255
print(x)
执行结果
[[255 255 255]
[255 255 255]]
3-3-6 使用 empty() 建立未初始化阵列
语法
np.empty(shape, dtype=float)
empty() 会建立指定形状的阵列,但不会将元素初始化为固定值,因此内容可能是内存中原有的任意数值。
ch3_6.py:建立未初始化阵列
import numpy as np
x1 = np.empty(3)
x2 = np.empty((2, 3), dtype=np.uint8)
print(x1)
print(x2)
执行结果
[6.23042070e-307 4.67296746e-307 1.69121096e-306]
[[ 0 0 0]
[ 0 0 0]]
上述结果会因电脑和执行环境不同而变化,重点是 empty() 不会先填入 0 或 1。
3-3-7 使用 random.randint() 建立随机整数阵列
语法
np.random.randint(low, high=None, size=None, dtype=int)
| 参数 | 说明 |
low | 随机数下限。 |
high | 随机数上限,产生的值不包含此数。 |
size | 输出阵列形状。 |
dtype | 输出资料型态。 |
ch3_7.py:建立随机整数资料
import numpy as np
x1 = np.random.randint(5)
x2 = np.random.randint(1, 5, size=10)
x3 = np.random.randint(0, 10, size=(3, 5))
print(x1)
print(x2)
print(x3)
执行结果范例
3
[1 4 1 2 4 2 1 3 3 4]
[[8 9 7 8 1]
[5 0 6 9 4]
[2 7 1 5 6]]
3-3-8 使用 arange() 建立连续整数阵列
语法
np.arange(start, stop, step)
start 是起始值,stop 是停止值,step 是间隔。若只给一个参数,则代表从 0 开始到该值前一项为止。
ch3_7_1.py:使用 arange()
import numpy as np
x = np.arange(16)
print(x)
执行结果
[ 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15]
3-3-9 使用 reshape() 改变阵列形状
语法
np.reshape(a, newshape)
reshape() 可以重新安排阵列形状,但是元素总数必须相同。如果新形状中的某一维写成 -1,Numpy 会自动计算该维度。
ch3_7_2.py:将一维阵列改为 2 x 8
import numpy as np
x = np.arange(16)
x = np.reshape(x, (2, 8))
print(x)
执行结果
[[ 0 1 2 3 4 5 6 7]
[ 8 9 10 11 12 13 14 15]]
ch3_7_3.py:使用 -1 自动计算第二维
import numpy as np
x = np.arange(16)
x = np.reshape(x, (4, -1))
print(x)
执行结果
[[ 0 1 2 3]
[ 4 5 6 7]
[ 8 9 10 11]
[12 13 14 15]]
ch3_7_4.py:使用 -1 自动计算第一维
import numpy as np
x = np.arange(16)
x = np.reshape(x, (-1, 8))
print(x)
执行结果
[[ 0 1 2 3 4 5 6 7]
[ 8 9 10 11 12 13 14 15]]
3-4
一维阵列的运算与切片
3-4-1 一维阵列四则运算
Numpy 阵列可以直接进行加、减、乘、除等运算。若与单一数值运算,阵列中的每个元素都会参与运算;若两个阵列形状相同,则对应位置的元素逐一运算。
| 运算子 | 说明 |
+ | 加法 |
- | 减法 |
* | 乘法 |
/ | 除法 |
// | 整数除法 |
% | 余数 |
** | 次方 |
例 1:阵列与数值相加
import numpy as np
x = np.array([1, 2, 3])
print(x + 5)
例 2:两个阵列相加
import numpy as np
x = np.array([1, 2, 3])
y = np.array([10, 20, 30])
print(x + y)
例 3:两个阵列相乘
import numpy as np
x = np.array([1, 2, 3])
y = np.array([10, 20, 30])
print(x * y)
例 4:除法与整数除法
import numpy as np
x = np.array([1, 2, 3])
y = np.array([10, 20, 30])
print(x / y)
print(x // y)
执行结果
[0.1 0.1 0.1]
[0 0 0]
3-4-2 一维阵列的关系运算子运算
阵列也可以使用关系运算子,执行结果会是布尔阵列。
| 运算子 | 说明 |
> | 大于 |
>= | 大于或等于 |
< | 小于 |
<= | 小于或等于 |
== | 等于 |
!= | 不等于 |
关系运算范例
import numpy as np
x = np.array([1, 2, 3])
y = np.array([10, 20, 30])
z = x > y
print(z)
z = x < y
print(z)
执行结果
[False False False]
[ True True True]
3-4-3 阵列切片
切片可以读取阵列中的部分资料,语法如下。
| 参数 | 说明 |
start | 起始索引。 |
end | 结束索引,不包含此位置的元素。 |
step | 间隔,若省略则为 1。 |
常见写法包括 arr[start:end]、arr[:n]、arr[:-n]、arr[n:]、arr[-n:]、arr[:]。
ch3_8.py:一维阵列切片
import numpy as np
x = np.arange(10)
print("x[2:] =", x[2:])
print("x[:2] =", x[:2])
print("x[0:3] =", x[0:3])
print("x[1:4] =", x[1:4])
print("x[0:9:2] =", x[0:9:2])
print("x[-1] =", x[-1])
print("x[::2] =", x[::2])
print("x[2::3] =", x[2::3])
print("x[:] =", x[:])
print("x[::-1] =", x[::-1])
print("x[-3:-7:-1] =", x[-3:-7:-1])
执行结果
x[2:] = [2 3 4 5 6 7 8 9]
x[:2] = [0 1]
x[0:3] = [0 1 2]
x[1:4] = [1 2 3]
x[0:9:2] = [0 2 4 6 8]
x[-1] = 9
x[::2] = [0 2 4 6 8]
x[2::3] = [2 5 8]
x[:] = [0 1 2 3 4 5 6 7 8 9]
x[::-1] = [9 8 7 6 5 4 3 2 1 0]
x[-3:-7:-1] = [7 6 5 4]
3-4-4 使用参数 copy=True 复制数据
使用 np.array() 建立阵列时,若参数 copy=True,会复制原始资料。复制后的阵列修改时,不会影响原阵列。
语法
x2 = np.array(x1, copy=True)
ch3_9.py:使用 copy=True 复制阵列
import numpy as np
x1 = np.array([1, 2, 3])
x2 = np.array(x1, copy=True)
print("x1 =", x1)
print("x2 =", x2)
print("----------------")
x2[0] = 9
print("x1 =", x1)
print("x2 =", x2)
执行结果
x1 = [1 2 3]
x2 = [1 2 3]
----------------
x1 = [1 2 3]
x2 = [9 2 3]
3-4-5 使用 copy() 函数复制阵列
copy() 也可以复制阵列,常用于影像处理。先复制原图,再对复制图执行处理,可以保留原始影像。
ch3_10.py:使用 copy() 复制阵列
import numpy as np
x1 = np.array([1, 2, 3])
x2 = x1.copy()
print("x1 =", x1)
print("x2 =", x2)
print("----------------")
x2[0] = 9
print("x1 =", x1)
print("x2 =", x2)
执行结果
x1 = [1 2 3]
x2 = [1 2 3]
----------------
x1 = [1 2 3]
x2 = [9 2 3]
3-5
多维阵列的索引与切片
3-5-1 认识 axis 的定义
在多维阵列中,axis 表示轴的方向。二维阵列可以看成列与行,axis=0 表示垂直方向,axis=1 表示水平方向。
ch3_11.py:二维阵列与 axis
import numpy as np
x = np.array([[0, 1, 2, 3, 4],
[5, 6, 7, 8, 9],
[10, 11, 12, 13, 14]])
print(x)
执行结果
[[ 0 1 2 3 4]
[ 5 6 7 8 9]
[10 11 12 13 14]]
上例中,外层阵列有 3 个子阵列,所以 axis=0 的长度为 3;每个子阵列内有 5 个元素,所以 axis=1 的长度为 5。
ch3_12.py:三维阵列与 axis
import numpy as np
x = np.array([[[0, 1, 2, 3, 4],
[5, 6, 7, 8, 9],
[10, 11, 12, 13, 14]],
[[0, 1, 2, 3, 4],
[5, 6, 7, 8, 9],
[10, 11, 12, 13, 14]]])
print(x)
print(x.shape)
执行结果
[[[ 0 1 2 3 4]
[ 5 6 7 8 9]
[10 11 12 13 14]]
[[ 0 1 2 3 4]
[ 5 6 7 8 9]
[10 11 12 13 14]]]
(2, 3, 5)
三维阵列有三个轴,上例的 shape 是 (2, 3, 5),分别对应 axis=0、axis=1、axis=2。
3-5-2 多维阵列索引
多维阵列可以连续使用中括号索引,也可以使用逗号分隔各维索引。
ch3_13.py:二维阵列索引
import numpy as np
x4 = np.array([[0, 1, 2, 3, 4],
[5, 6, 7, 8, 9],
[10, 11, 12, 13, 14]])
print(x4[2][1])
print(x4[1][3])
ch3_13_1.py:二维阵列逗号索引
import numpy as np
x4 = np.array([[0, 1, 2, 3, 4],
[5, 6, 7, 8, 9],
[10, 11, 12, 13, 14]])
print(x4[2, 1])
print(x4[1, 3])
ch3_14.py:三维阵列索引
import numpy as np
x5 = np.array([[[0, 1, 2, 3, 4],
[5, 6, 7, 8, 9],
[10, 11, 12, 13, 14]],
[[0, 1, 2, 3, 4],
[5, 6, 7, 8, 9],
[10, 11, 12, 13, 14]]])
print(x5[0][2][1])
print(x5[0][1][3])
print(x5[1][0][1])
print(x5[1][1][4])
ch3_14_1.py:三维阵列逗号索引
import numpy as np
x5 = np.array([[[0, 1, 2, 3, 4],
[5, 6, 7, 8, 9],
[10, 11, 12, 13, 14]],
[[0, 1, 2, 3, 4],
[5, 6, 7, 8, 9],
[10, 11, 12, 13, 14]]])
print(x5[0, 2, 1])
print(x5[0, 1, 3])
print(x5[1, 0, 1])
print(x5[1, 1, 4])
3-5-3 多维阵列切片
多维阵列切片时,可以对每一个维度分别指定切片范围。不同维度的切片条件用逗号分隔。
ch3_15.py:二维阵列切片
import numpy as np
x = np.array([[0, 1, 2, 3, 4],
[5, 6, 7, 8, 9],
[10, 11, 12, 13, 14]])
print("x[:2, :] =")
print(x[:2, :])
print("x[2, :4] =")
print(x[2, :4])
print("x[:2, :1] =")
print(x[:2, :1])
print("x[:, 4:] =")
print(x[:, 4:])
print("x[:, 4] =")
print(x[:, 4])
执行结果
x[:2, :] =
[[0 1 2 3 4]
[5 6 7 8 9]]
x[2, :4] =
[10 11 12 13]
x[:2, :1] =
[[0]
[5]]
x[:, 4:] =
[[ 4]
[ 9]
[14]]
x[:, 4] =
[ 4 9 14]
x[:, 4:] 取出的仍是二维阵列;x[:, 4] 取出的是一维阵列。是否保留维度会影响后续影像处理时的资料形状。
ch3_16.py:连续中括号切片的差异
import numpy as np
x = np.array([[0, 1, 2, 3, 4],
[5, 6, 7, 8, 9],
[10, 11, 12, 13, 14]])
print("x[:2, 4] =")
print(x[:2, 4])
print("x[:2][4] =")
print(x[:2][4])
执行结果
x[:2, 4] =
[4 9]
x[:2][4] =
IndexError: index 4 is out of bounds for axis 0 with size 2
x[:2][4] 会先执行 x[:2],此时结果只有 2 列,再取第 4 列就会发生索引错误。
3-6
阵列水平与垂直合并
3-6-1 使用 vstack() 垂直合并阵列
vstack() 可以将多个阵列依垂直方向合并。在影像处理中,可用于将影像上下拼接。
ch3_17.py:垂直合并阵列
import numpy as np
x1 = np.array([[1, 2, 3],
[4, 5, 6]])
x2 = np.array([[7, 8, 9],
[10, 11, 12]])
x = np.vstack((x1, x2))
print(x)
ch3_17.py 执行结果
3-6-2 使用 hstack() 水平合并阵列
hstack() 可以将多个阵列依水平方向合并。在影像处理中,可用于将影像左右拼接。
ch3_18.py:水平合并阵列
import numpy as np
x1 = np.array([[1, 2, 3],
[4, 5, 6]])
x2 = np.array([[7, 8, 9],
[10, 11, 12]])
x = np.hstack((x1, x2))
print(x)
ch3_18.py 执行结果
1. 使用 huang.jpg 影像,将影像水平合并,最后输出合并结果。
import cv2
import numpy as np
img = cv2.imread("huang.jpg")
dst = np.hstack((img, img))
cv2.imshow("Dst", dst)
cv2.waitKey(0)
cv2.destroyAllWindows()
习题 1 参考结果
2. 使用 flower1.jpg 影像,将影像合并成 4 张排列的结果。
import cv2
import numpy as np
img = cv2.imread("flower1.jpg")
row = np.hstack((img, img))
dst = np.vstack((row, row))
cv2.imshow("Dst", dst)
cv2.waitKey(0)
cv2.destroyAllWindows()
习题 2 参考结果