[计算机视觉基础] Numpy

\color{red}{计算机视觉类文章会不定期的做更新,欢迎关注}

Somethings To Know

无论是 ML 还是 CV,Numpy 都是极其重要、极其基础的 Python 发行库。尤其是图片处理,通过鼎鼎大名的 opencv 库加载的图片或者视频,都是由 Numpy 库中的数据类型组成。可以说,对 Numpy 有一个充分的认识,对于后面入门、进阶 CV 都是事半功倍之事。

俗话说的好,世间万物存在都是有道理的。学习 Numpy 也必须追根溯源,了解它为什么被巨佬造出来,使用场景是什么,解决了什么问题。

NumPy 是用 Python 进行科学计算的基础软件包。看到这里我心生疑问:为什么在 Python 语言中需要存在这么一个无法替代的软件包,会不会和 Python 语言本身存在的问题有关。科学计算,无论是机器学习数以万计的特征和迭代次数,还是图片、视频处理,都是海量的数据处理和计算。当今计算机硬件飞速发展,虽说超线程,高性能显卡层出不穷,但软件程序本身的运行速度,永远是第一个需要被优化的,而 Python 作为解释性语言,运行速度并不能称得上迅速,而且,作为动态语言,每一个变量的加载都需要判断类型,速度会比编译型语言慢上很多。就如 List 而言,我们可以将各种不同类型的对象存入其中

a = list()
a.append(1)
a.append("hello world")
a.append({"key":"value"})
a.append(None)

当我们使用这个 List 的时候,对于每一个元素,都需要读取对象的类型,增减引用计数,获得尺寸,最后才是值,对于数字1,Python 仍然要使用 32 bit 的存储空间存放(这也算是动态类型的代价吧),并且对于一个 List 对象,里面的元素在内存中并不是按地址顺序存放,这对后来内存空间的访问,无疑是再一次的降速。
而 Numpy 中的一些数据类型,可以指定集合中元素的类型到 int8 ,并且在内存中连续存放,这也大大提升了日后访问元素的速度。当然 Numpy 的高速并不仅仅如此,作为科学计算工具,底层通过编译型语言 C 以及 Fortran实现,并且对于矩阵计算进行了高度的优化,使用了 Intel MKL(全称Intel Math Kernel Library,提供经过高度优化和大量线程化处理的数学例程,面向性能要求极高的科学、工程及金融等领域的应用)等等。总之,Numpy 并不是一个建立在 Python 应用层之上的软件包,可以说,它是能够供给 Python 使用的另一门语言技术。
看下面这个例子:

import numpy as np
a = np.array([1,2,3])
b = np.array([4,5,6])
a * b 

output:

array([ 4, 10, 18])

Numpy 为科学计算而生,这里的乘法即为针对a、b两个数组的各个元素依次做乘法。视为向量,其实这就是向量乘法。

说了 Numpy 的一些有点和解决的问题,那么它目前已经有哪些知名的使用场景呢?

  1. OpenCV
  2. 矩阵、向量计算(类似 MATLAB)
  3. Matplotlib
  4. Pandas
  5. ML

是的,都是鼎鼎大名,如雷贯耳的AI,CV,数据处理相关方向的库。掌握 Numpy 对于进一步迈入这些领域,有着不可替代的作用。好了,撤了那么多价值,下面该开始进入正题了。

The Basic

定义一个 Numpy array:

a = np.array([1,2,3])
print(a)
print(type(a))
print(a.shape)
print(a.dtype)
print(a.ndim)

output:

[1 2 3]
<class 'numpy.ndarray'>
(3,)
int64
1

这是一个一维的 Vector,拥有 3 个 元素,每个元素是有 int64 构成。
下面是定义一个二维对象:

a = np.array([[1,2,3], [4,5,6]])
print(a)
print(type(a))
print(a.shape)
print(a.dtype)

output:

[[1 2 3]
 [4 5 6]]
<class 'numpy.ndarray'>
(2, 3)
int64
2

shape 代表数组的形状,这里是 2 rows x 3 columns,了解矩阵概念的可以将此对应于矩阵的行列,依然是由 int64 构成。
前面我们提到, Numpy 的一个优点就是他的非动态类型,我们完全可以指定数组中每一个元素所使用的类型,例如:

a = np.array([1,2,3], dtype='int8')

通过构造函数中指定 dtype 为 int8,可以将数组的每一个元素限定为只占8个bit的空间,这对于已知范围的巨量数组,是空间的极大减少(例如对于BGR 图片对象而言,每个像素的值为 0 ~ 255, 那么我们完全可以指定 dtype 为 uint8 已达到充分节约空间,加速访问的效果)。

Vary Size

对于一个 ndarray,存在着各种大小,下面通过示例进行罗列:

a = np.array([[1,2,3], [4,5,6]], dtype='int16')
print(a.ndim) # 维度
print(a.shape) # 形状
print(a.shape[0]) # 行
print(a.shape[1]) # 列
print(a.itemsize) # 每个元素的字节数
print(a.size) # 元素数
print(a.nbytes) # 元素数 * 每个元素的字节数

output:

2
(2, 3)
2
3
2
6
12

Index

ndarray 的索引简单直接,直接看例子:

a = np.array([[1,2,3,4,5,6,7,8], [9,10,11,12,13,14,15,16]], dtype='int16')
print(a[1, 2]) # 获取第二行第三列对应的元素
print(a[0,:]) # 获取第一行
print(a[:,1]) # 获取第二列
print(a[1,1:4]) # 获取第二行下标在 1 <= index < 4 的元素
print(a[1,1:6:2]) # 获取第二行下标在 1 <= index < 6 的元素, step 为2

output:

11
[1 2 3 4 5 6 7 8]
[ 2 10]
[10 11 12]
[10 12 14]

Change

就如索引一样,改变 ndarray 的元素也是简单并且方法多,直接看例子:

a = np.array([[[1,2,3],[4,5,6]],[[7,8,9],[10,11,12]]], dtype='int16')
print(a)
print("\n修改某一个元素:")
a[0,1,1] = 15
print(a)
print("\n修改某一维元素:")
a[:,1,:] = [[21,22,23],[24,25,26]]
print(a)

output:

[[[ 1  2  3]
  [ 4  5  6]]

 [[ 7  8  9]
  [10 11 12]]]

修改某一个元素:
[[[ 1  2  3]
  [ 4 15  6]]

 [[ 7  8  9]
  [10 11 12]]]

修改某一维元素:
[[[ 1  2  3]
  [21 22 23]]

 [[ 7  8  9]
  [24 25 26]]]

Some Special Matric

因为 Numpy 与矩阵计算息息相关,库也同时提供了一些方便使用的构造方法构造多维特殊的矩阵:

from pprint import pprint
a = np.array([[1,2,3], [4,5,6]])
b = np.array([[1,2,3]])

pprint(np.zeros((2,2), dtype='uint8')) # 构造一个 2 x 2,以指定类型 0 值填充的数组
pprint(np.zeros_like(a, dtype='uint8')) # 构造一个与 a 同形,以指定类型 0 值填充的数组
pprint(np.ones((2,2), dtype='uint8')) # 构造一个 2 x 2,以指定类型 1 值填充的数组
pprint(np.ones_like(a, dtype='uint8')) # 构造一个与 a 同形,以指定类型 1 值填充的数组
pprint(np.identity(3)) # 构造一个 3 x 3 的单位矩阵
pprint(np.full((2,2), 3, dtype='uint8')) # 构造一个 2 x 2,以指定类型 3 值填充的数组
pprint(np.full_like(a, 3, dtype='uint8')) # 构造一个与 a 同形,以指定类型 3 值填充的数组
pprint(np.random.rand(3,2)) # 构造一个由随机数组成的 3 x 2 矩阵
pprint(np.random.random_sample(a.shape)) # 构造一个与 a 同形,由随机数组成矩阵
pprint(np.random.randint(1,8, size=a.shape)) # 构造一个与 a 同形,由范围1~8的随机数字填充的矩阵
pprint(np.repeat(b, 3, axis=0)) # 复制行3次
pprint(np.repeat(b, 3, axis=1)) # 复制列3次

output:

array([[0, 0],
       [0, 0]], dtype=uint8)
array([[0, 0, 0],
       [0, 0, 0]], dtype=uint8)
array([[1, 1],
       [1, 1]], dtype=uint8)
array([[1, 1, 1],
       [1, 1, 1]], dtype=uint8)
array([[1., 0., 0.],
       [0., 1., 0.],
       [0., 0., 1.]])
array([[3, 3],
       [3, 3]], dtype=uint8)
array([[3, 3, 3],
       [3, 3, 3]], dtype=uint8)
array([[0.94942891, 0.54141901],
       [0.21244184, 0.29175129],
       [0.37956357, 0.77184397]])
array([[0.62711458, 0.42293761, 0.64249381],
       [0.68692789, 0.4358814 , 0.22340343]])
array([[2, 6, 1],
       [2, 4, 5]])
array([[1, 2, 3],
       [1, 2, 3],
       [1, 2, 3]])
array([[1, 1, 1, 2, 2, 2, 3, 3, 3]])

View and Copy

首先,这是一个重要的概念。如果掌握不透彻,可能会使你的代码陷入无法预料的错误中。
视图,顾名思义,它只是查看数组数据的另一种方式。这意味着视图数组与原数组的数据是共享的,你可以通过选择原始数组的一部分来创建视图,也可以通过更改 dtype(或两者的组合)来创建视图。
Slice 是最为常见的数组视图的场景:

a = np.array([0,1,2,3,4,5,6,7])
b = a[1:3]
pprint(b)
a[2] = 22
pprint(b) 

output:

array([1, 2])
array([1, 22])

这里要充分的体会数据共享的含义,视图与原数组的数据在内存中是同片空间,但视图有属于自己的数据类型选择,相当于用不同的显微镜倍数看玻片上的样本,不同的放大倍数看到的也不同:

a = np.array([0,-1,2,-3,4,-5,6,-7], dtype='int8')
pprint(a)
b = a.view('uint8')
pprint(b)
a[2] = 22
pprint(b)

output:

array([ 0, -1,  2, -3,  4, -5,  6, -7], dtype=int8)
array([  0, 255,   2, 253,   4, 251,   6, 249], dtype=uint8)
array([  0, 255,  22, 253,   4, 251,   6, 249], dtype=uint8)

这里通过 int8 类型写入的数组,通过构造出 uint8 的视图,通过视图观察到的数组发生了变化,负数都变为无符号数(-1变为255,-3变为253等)。其实内存中的数据同属一份,只是使用了不同的视图。
Copy 即为深拷贝,相当于新申请了一片内存存放相同数据的拷贝。

a = np.array([1,2,3,4,5])
b = a.copy()
print(id(a))
print(id(b))
b[1] = -2
a

output:

4788660816
4784398864
array([1, 2, 3, 4, 5])

Math

使用 Numpy 计算数据通常是必经之路。简单的四则运算通俗易懂:

a = np.array([1,2,3,4,5])
b = np.ones(5, dtype='uint8')
pprint(a + 2) # 元素范围的加法
pprint(a * 2) # 元素范围的乘法
pprint(a ** 2) # 元素范围的乘方
pprint(np.sin(a)) # 素范围的 sin
pprint(a + b) # 两个 array 相加

output:

array([3, 4, 5, 6, 7])
array([ 2,  4,  6,  8, 10])
array([ 1,  4,  9, 16, 25])
array([ 0.84147098,  0.90929743,  0.14112001, -0.7568025 , -0.95892427])
array([2, 3, 4, 5, 6])

Linear algebra

矩阵的各种计算依赖于线性代数的各种名词和方法,行列式,SVD,特征值,特征向量。本文并不想展开这些概念和使用,这需要很多的基础知识,Numpy 的文档可供参考:https://numpy.org/doc/stable/reference/routines.linalg.html

Statistics

有数据就一定有统计的价值,Numpy 自然也能根据数据进行各种统计学的处理:

a = np.array([[1,2,3],[4,5,6]])
pprint(np.min(a)) # 如果需要知道行列的最值,可以通过指定 axis
pprint(np.min(a, axis=0)) # 输出成行,即为统计列的最小值
pprint(np.min(a, axis=1)) # 输出成列,即为统计行的最小值
pprint(np.max(a))
pprint(np.sum(a))
pprint(np.mean(a))

output:

1
array([1, 2, 3])
array([1, 4])
6
21
3.5

Stack

分为垂直堆叠和水平堆叠:

a = np.array([1,2,3,4])
b = np.array([5,6,7,8])
pprint(np.vstack((a,b))) # 可以理解为垂直堆起来
pprint(np.hstack((a,b))) # 可以理解为水平堆起来

output:

array([[1, 2, 3, 4],
       [5, 6, 7, 8]])
array([1, 2, 3, 4, 5, 6, 7, 8])

File

从文件中读取数据到数组

data = np.genfromtxt("a.txt", delimiter=',') # csv 文件

Bool Masking and Advanced Indexing

有点类似对 Numpy 数组进行查找,过滤,获得所需的数据。

a = np.random.randint(1,99, size=(3,10))
pprint(a)
pprint(a > 50) # 针对每一个元素判断是否大于50
pprint((a > 50).size) # 大于 50 的元素个数
pprint(np.any(a > 50, axis=0)) # 每列是否都有大于50
pprint(np.all(a > 50, axis=0)) # 每列是否都全大于50
pprint(a[a > 50]) # 输出所有大于50的元素

output:

array([[34, 40, 10, 74, 35, 79, 60, 70, 96, 89],
       [87, 55, 95, 80, 72, 70, 82, 96, 88,  2],
       [ 5, 74, 98, 22, 16, 88, 93, 97, 47, 70]])
array([[False, False, False,  True, False,  True,  True,  True,  True,
         True],
       [ True,  True,  True,  True,  True,  True,  True,  True,  True,
        False],
       [False,  True,  True, False, False,  True,  True,  True, False,
         True]])
30
array([ True,  True,  True,  True,  True,  True,  True,  True,  True,
        True])
array([False, False, False, False, False,  True,  True,  True, False,
       False])
array([74, 79, 60, 70, 96, 89, 87, 55, 95, 80, 72, 70, 82, 96, 88, 74, 98,
       88, 93, 97, 70])

Last Words

blu blu 讲了那么多琐碎的基础知识点,这些也许只占 Numpy Big Picture 的0.01%都不到,在使用的过程中不停地去积累,逐渐的拨开云雾,见青天吧

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。
禁止转载,如需转载请通过简信或评论联系作者。

推荐阅读更多精彩内容