6.3. 创建数组¶
接下来这些页面上的每个示例都是从已经在手的 ndarray 开始的。本页则是关于该数组如何产生的目录。构造函数共有四大类:
从 Python 可迭代对象创建 —— 常见的字面量 / 列表 / 元组形式。
按给定形状预填充 —— 零、一、某个常量值、单位矩阵。
作为序列生成 —— 范围值或均匀间隔的值。
包装 RAM 中已有的缓冲区 —— 外设的情形。
每个构造函数都接受一个 dtype= 关键字,并默认为 float。传感器数据几乎总是需要比默认值更小的 dtype。
下面的每个示例都以如下内容开头::
from ulab import numpy as np
6.3.1. 从 Python 可迭代对象创建¶
array() 会从任何数字可迭代对象构建一个 ndarray::
a = np.array([1, 2, 3, 4])
print(a)
输出::
array([1.0, 2.0, 3.0, 4.0], dtype=float)
嵌套的可迭代对象会产生多维数组。内层的可迭代对象必须全部具有相同的长度,否则会引发 ValueError::
m = np.array([[1, 2, 3],
[4, 5, 6]], dtype=np.uint8)
已有的 ndarray 也是有效的输入;array() 总是会复制。若想在不需要复制时避免复制,请使用 asarray()::
b = np.asarray(a, dtype=np.float) # same dtype -> no copy
6.3.2. 按给定形状预填充¶
当目标形状已知但内容尚未确定时,可以预先分配缓冲区,稍后再写入:
zeros()—— 用零填充。ones()—— 用一填充。full()—— 用给定值填充。eye()—— 类似单位矩阵的N行M列矩阵,在第k条对角线上为一。diag()—— 由向量构成的对角矩阵,或矩阵的对角线。
np.zeros((3, 3)) # 3x3 of zeros
np.ones(5, dtype=np.uint8) # length-5 vector of ones
np.full((2, 3), 7, dtype=np.int8) # 2x3, all 7
np.eye(4) # 4x4 identity
np.diag([1, 2, 3]) # 3x3, [1, 2, 3] on the diagonal
shape 参数可以是单个整数(用于一维数组)或一个元组。
6.3.3. 作为序列生成¶
arange()—— 像内置的range()那样生成均匀间隔的值,但始终返回一个ndarray::np.arange(0, 10, 2) # array([0, 2, 4, 6, 8])linspace()—— 在两个界限之间生成num个均匀间隔的点,当endpoint=True时包含上界::np.linspace(0, 1, num=11) # 0.0, 0.1, ..., 1.0logspace()—— 几何间隔的点。start和stop是 指数,而非端点;结果从base ** start到base ** stop::np.logspace(0, 3, num=4) # 1.0, 10.0, 100.0, 1000.0meshgrid()—— 从两个一维数组构建两个坐标矩阵,从而可以在一次向量化调用中对整个网格求取逐像素函数f(x, y)。给定长度为W的 x 向量和长度为H的 y 向量,meshgrid会返回两个H行W列的矩阵:X是沿每一行向下重复的 x 向量,Y是横跨每一列重复的 y 向量,因此X[i, j]是第i行第j列单元格的 x 坐标,Y[i, j]是其 y 坐标::x = np.arange(4) # [0, 1, 2, 3] y = np.arange(3) # [0, 1, 2] X, Y = np.meshgrid(x, y) # X = [[0, 1, 2, 3], # [0, 1, 2, 3], # [0, 1, 2, 3]] # Y = [[0, 0, 0, 0], # [1, 1, 1, 1], # [2, 2, 2, 2]]
f(X, Y)随后会在一个表达式中对网格的每个单元格求值。例如,在一个(H, W)帧上的中心距离图,就是针对meshgrid()返回的矩阵计算np.sqrt((X - cx)**2 + (Y - cy)**2)。
6.3.4. 拼接¶
concatenate() 沿现有轴拼接一个数组元组::
a = np.array([[1, 2], [3, 4]], dtype=np.uint8)
b = np.array([[5, 6]], dtype=np.uint8)
np.concatenate((a, b), axis=0)
# array([[1, 2], [3, 4], [5, 6]], dtype=uint8)
所有输入必须共享相同的 dtype 和 ndim,并且除拼接轴之外的每个轴上都要匹配。concatenate() 会分配一个足够大、能容纳每个输入的新数组并把数据复制进去,因此它是对已有数组进行一次性拼接的合适工具;它不适用于流式循环中,在那种情况下,应一次性预分配目标数组并通过切片赋值写入其中,才是正确的模式。
6.3.5. 包装已有缓冲区¶
在摄像头上最有用的构造函数是 frombuffer()。它会将一个已有的类字节缓冲区重新解释为一维 ndarray,而不复制 哪怕一个字节::
buf = bytearray(8)
audio = np.frombuffer(buf, dtype=np.int16)
# 4 int16 samples, sharing memory with buf
通过 audio 进行的写入在 buf 中可见,反之亦然。所选的 dtype 必须能整除缓冲区长度。
offset= 会跳过缓冲区开头的头部;count= 会限制读取多少个元素::
np.frombuffer(buf, dtype=np.uint8, offset=2, count=4)
每当某个外设把原始缓冲区交给应用程序时 —— 比如 bytearray 中的 ADC 采样、从 SPI 取出的负载 —— 这就是合适的构造函数。外设写入的字节本身就是该数组。
当外设以摄像头 CPU 本身不能直接读取的字节序写入多字节值时,byteswap() 会反转每个元素的字节序,使这些值能被正确读取。它默认返回一个新数组;传入 inplace=True 则会就地修改源数组。
frombuffer() 只处理 numpy 自身定义的 dtype。对于产生 32 位整型采样的外设,from_int32_buffer() 及其同类函数会一次性转换为 float。
6.3.6. 打印截断¶
打印一个大数组时只会显示其开头和结尾的少数几个元素,中间用 ... 表示,这样 IDE 终端就不会被成千上万的值填满::
>>> print(np.arange(1000, dtype=np.uint16))
array([0, 1, 2, ..., 997, 998, 999], dtype=uint16)
set_printoptions() 会在调试需要查看整个缓冲区时覆盖这些阈值::
np.set_printoptions(threshold=2000) # print up to 2000 elements in full
np.set_printoptions(edgeitems=10) # 10 items at each end, not 3
get_printoptions() 会以字典形式读回当前设置。