6.3. 建立陣列

這些頁面其餘部分的每個範例,一開始都已經手握一個 ndarray。本頁就是該陣列如何產生的目錄。建構函式共有四大類:

  • 從 Python 可迭代物件 —— 一般的字面量/list/tuple 形式。

  • 以指定形狀預先填入 —— 全零、全一、某個常數值、單位矩陣。

  • 以序列方式產生 —— 範圍式或等距分佈的值。

  • 包裝 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 引數可以是單一整數(用於 1 維陣列)或一個 tuple。

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 ** start 跑到 base ** stop::

    np.logspace(0, 3, num=4)       # 1.0, 10.0, 100.0, 1000.0
    
  • meshgrid() —— 從兩個 1 維陣列建構兩個座標矩陣,讓逐像素函式 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() 會沿著一個既有的軸連接一個 tuple 的陣列::

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()。它會把一個既有的類位元組緩衝區重新解讀為 1 維的 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