kind: reference search: numpy, Array, ndarray, Slice, Broadcasting, dtype, uint8, FFT

numpy: Rechnen mit ganzen Arrays

numpy rechnet in Python mit ganzen Reihen von Zahlen auf einmal: mit den Samples eines Tons, den Pixeln eines Bilds, den Bildern eines Videos. Statt einer Schleife über eine Million Zahlen steht eine Zeile, und sie läuft oft zehn- bis zwanzigmal so schnell. Im Workspace ist numpy installiert. Hier steht, was Audio komprimieren, Audioeffekte, Video komprimieren, Fraktale Bildkompression und Gaussian Splatting davon brauchen.

import numpy as np

1. Arrays

Ein Array anlegen

Ein Array ist eine Reihe von Zahlen, alle vom selben Typ. Anders als eine Liste kann es mehrere Achsen haben: ein Ton eine, ein Graustufenbild zwei (Zeile, Spalte), ein Farbbild drei (Zeile, Spalte, Farbe).

Ausdruck Ergebnis
np.array([3, 1, 4]) [3 1 4], aus einer Liste
np.zeros(4) [0. 0. 0. 0.], lauter Nullen
np.full(3, 7) [7 7 7], lauter Siebenen
np.arange(5) [0 1 2 3 4], wie range
np.arange(0, 1, 0.25) [0. 0.25 0.5 0.75], auch mit Kommazahlen
np.linspace(0, 1, 5) [0. 0.25 0.5 0.75 1.], 5 Zahlen in gleichen Abständen, das Ende mitgezählt
np.zeros((400, 960, 3)) ein schwarzes Bild: 400 Zeilen, 960 Spalten, 3 Farben
np.zeros_like(x) Nullen in der Form und dem Typ von x

Form und Typ

bild = np.zeros((400, 960, 3), dtype=np.uint8)
print(bild.shape, bild.ndim, bild.dtype, bild.size)
(400, 960, 3) 3 uint8 1152000

shape ist die Länge jeder Achse, ndim die Zahl der Achsen, size die Zahl aller Werte. dtype ist der Typ jedes Werts: float64 für Kommazahlen, int64 für ganze Zahlen, uint8 für Bytes von 0 bis 255, wie in Bildern, int16 für Samples mit 16 Bit, wie in WAV-Dateien. x.astype(np.uint8) macht ein Array eines anderen Typs daraus.

Ein uint8 hat nur 256 Werte. Was darüber hinausgeht, beginnt wieder bei 0:

a = np.array([200, 100], dtype=np.uint8)
print(a[0] + a[1])
print(a.astype(int).sum())
44
300

Darum rechnet man mit Pixeln in int oder float und macht erst am Ende wieder Bytes daraus, gerundet und auf 0 bis 255 begrenzt:

print(np.clip(np.round(np.array([-20.4, 99.6, 300.0])), 0, 255).astype(np.uint8))
[  0 100 255]

2. Auf Teile zugreifen

Index und Slice

Wie bei einer Liste zählt der Index ab 0, ein negativer von hinten; a:b ist ein Slice, von a bis vor b, ::2 jeder zweite Wert.

x = np.array([10, 20, 30, 40, 50])
print(x[0], x[-1], x[1:3], x[::2], x[::-1])
10 50 [20 30] [10 30 50] [50 40 30 20 10]

Mit mehreren Achsen steht für jede Achse ein Index, durch Kommas getrennt; : heißt die ganze Achse.

m = np.arange(12).reshape(3, 4)
print(m[1, 2], m[1], m[:, 0], m[0:2, 1:3])
6 [4 5 6 7] [0 4 8] [[1 2]
 [5 6]]

m[1, 2] ist Zeile 1, Spalte 2; m[:, 0] die erste Spalte. In einem Bild ist das bild[y, x]: zuerst die Zeile, dann die Spalte. bild[:, :, 0] ist der Rotanteil aller Pixel, kurz bild[..., 0]. Wer einem Slice etwas zuweist, ändert das Array: bild[:, :, 0] = 255 macht alles rot.

Mit Bedingungen

x = np.array([-2, 5, -1, 8])
print(x > 0, x[x > 0], np.where(x > 0, x, 0), np.clip(x, 0, 6), np.maximum(x, 0))
[False  True False  True] [5 8] [0 5 0 8] [0 5 0 6] [0 5 0 8]

x > 0 vergleicht jeden Wert und gibt ein Array aus True und False; x[x > 0] behält nur die Werte, bei denen es True ist. np.where(bedingung, a, b) nimmt für jeden Wert a, wo die Bedingung gilt, sonst b. np.clip(x, 0, 6) begrenzt auf 0 bis 6, np.maximum(x, 0) nimmt für jeden Wert den größeren von beiden.

3. Rechnen

Mit ganzen Arrays

Jede Rechnung gilt für jeden Wert, ohne Schleife; zwei Arrays derselben Form werden Wert für Wert verrechnet.

x = np.array([1.0, 2.0, 3.0])
print(x * 2, x + x, x ** 2, np.sqrt(x))
t = np.arange(4) / 4
print(np.sin(2 * np.pi * t).round(3))
[2. 4. 6.] [2. 4. 6.] [1. 4. 9.] [1.         1.41421356 1.73205081]
[ 0.  1.  0. -1.]

np.sin, np.cos, np.exp, np.log2, np.log10, np.abs und np.sqrt rechnen ebenso für jeden Wert. Die zweite Zeile ist eine Sinusschwingung mit 4 Samples je Schwingung: So entsteht ein Ton aus Zahlen.

Passen die Formen nicht genau, dehnt numpy die kleinere aus, wo sie eine Achse weniger hat oder eine Achse der Länge 1. Das heißt Broadcasting:

m = np.arange(6).reshape(2, 3)
print(m * np.array([1, 10, 100]))
print(m - m.mean())
[[  0  10 200]
 [  3  40 500]]
[[-2.5 -1.5 -0.5]
 [ 0.5  1.5  2.5]]

Die Zeile [1, 10, 100] wird mit jeder Zeile von m multipliziert, und eine einzelne Zahl wie der Mittelwert mit allem.

Zusammenfassen

m = np.array([[1, 5, 2], [7, 0, 3]])
print(m.sum(), m.sum(axis=0), m.sum(axis=1), m.max(), m.argmax(), m.mean(axis=1))
18 [8 5 5] [ 8 10] 7 3 [2.66666667 3.33333333]

Ohne axis fasst eine Funktion alles zusammen; mit axis=0 jede Spalte (über die Zeilen hinweg), mit axis=1 jede Zeile. argmax gibt den Index des größten Werts, gezählt, als wäre das Array eine einzige Reihe. Ebenso: sum, mean, max, min, argmin, np.count_nonzero.

Zusammensetzen und umformen

Ausdruck Ergebnis
np.concatenate([a, b]) [1 2 3 4] für a = [1, 2] und b = [3, 4]: hintereinander
np.stack([a, b]) [[1 2] [3 4]]: übereinander, mit einer neuen Achse
np.repeat(a, 3) [1 1 1 2 2 2]: jeden Wert dreimal
np.tile(a, 3) [1 2 1 2 1 2]: das Ganze dreimal
np.arange(6).reshape(2, 3) [[0 1 2] [3 4 5]]: dieselben Werte in einer anderen Form
x.reshape(-1, 2) so viele Zeilen wie nötig, je 2 Werte; -1 rechnet numpy aus

4. Bytes, Bilder und Töne

Aus Bytes und zurück

data = bytes([0, 1, 255, 0, 128, 64])
print(np.frombuffer(data, dtype=np.uint8).reshape(2, 3))
samples = np.frombuffer(bytes([0, 1, 0, 128]), dtype='<i2')
print(samples, samples / 32768)
print(np.array([1, 256], dtype='<i2').tobytes())
[[  0   1 255]
 [  0 128  64]]
[   256 -32768] [ 0.0078125 -1.       ]
b'\x01\x00\x00\x01'

np.frombuffer liest Bytes als Zahlen eines Typs, ohne sie zu kopieren; '<i2' heißt ganze Zahlen mit 2 Bytes, Little Endian, wie die Samples einer WAV-Datei (Handbuch, Bits und Bytes). tobytes() geht den Weg zurück. Ein Array aus frombuffer lässt sich nicht ändern; .copy() macht ein eigenes daraus.

Bilder mit Pillow

from PIL import Image

bild = np.asarray(Image.open('foto.png'))
print(bild.shape, bild.dtype)
Image.fromarray(bild[::2, ::2]).save('klein.png')

np.asarray(Image.open(…)) macht aus einem Bild ein Array mit der Form (Zeilen, Spalten, Farben) und dem Typ uint8; Image.fromarray macht aus einem solchen Array wieder ein Bild. bild[::2, ::2] nimmt jede zweite Zeile und Spalte: halb so breit und halb so hoch. Wie man die Bilder eines Videos als Array bekommt, steht bei ffmpeg.

Frequenzen mit der FFT

rate = 8000
t = np.arange(rate) / rate
ton = np.sin(2 * np.pi * 1000 * t) + 0.5 * np.sin(2 * np.pi * 2500 * t)
spektrum = np.abs(np.fft.rfft(ton))
freq = np.fft.rfftfreq(len(ton), 1 / rate)
print(freq[np.argmax(spektrum)])
zurueck = np.fft.irfft(np.fft.rfft(ton), len(ton))
print(np.allclose(zurueck, ton))
1000.0
True

np.fft.rfft zerlegt Samples in ihre Frequenzen: Aus 8.000 Samples werden 4.001 komplexe Zahlen, eine für jede Frequenz von 0 Hz bis zur halben Abtastrate, und ihr Betrag sagt, wie stark sie ist. np.fft.rfftfreq gibt die Frequenz zu jeder dieser Zahlen, hier 0, 1, 2 … Hz. np.fft.irfft macht aus den Frequenzen wieder Samples; np.allclose vergleicht zwei Arrays bis auf winzige Rundungsfehler.

5. Zufall und Dateien

Zufall

rng = np.random.default_rng(42)
print(rng.integers(1, 7, size=5), rng.uniform(-1, 1, 3))

default_rng(42) ist ein Zufallsgenerator mit dem Startwert 42: Mit demselben Startwert kommen bei jedem Lauf dieselben Zahlen, praktisch zum Vergleichen. integers(1, 7, size=5) würfelt fünfmal, von 1 bis vor 7; uniform(-1, 1, 3) gibt 3 Kommazahlen zwischen −1 und 1, etwa für Rauschen.

Speichern und laden

np.save('feld.npy', x)
x = np.load('feld.npy')
np.savez('alles.npz', bloecke=bloecke, werte=werte)
daten = np.load('alles.npz')
print(daten['bloecke'])

np.save schreibt ein Array mit Form und Typ in eine Datei, np.load liest es wieder; np.savez schreibt mehrere unter Namen in eine Datei.

6. Zum Nachschlagen