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() —— 用给定值填充。

  • empty() —— zeros() 的别名(ulab 不会让缓冲区处于未初始化状态)。

  • eye() —— 类似单位矩阵的 NM 列矩阵,在第 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.0
    
  • logspace() —— 几何间隔的点。startstop指数,而非端点;结果从 base ** startbase ** stop::

    np.logspace(0, 3, num=4)       # 1.0, 10.0, 100.0, 1000.0
    
  • meshgrid() —— 从两个一维数组构建两个坐标矩阵,从而可以在一次向量化调用中对整个网格求取逐像素函数 f(x, y)。给定长度为 W 的 x 向量和长度为 H 的 y 向量,meshgrid 会返回两个 HW 列的矩阵: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