Sejauh ini sinyal yang kita olah berupa deretan sampel satu dimensi. Data citra berbentuk lain: tabel piksel dengan baris dan kolom, alias array dua dimensi. NumPy menyediakan np.fft.fft2 untuk data seperti ini — citra diuraikan menjadi pola-pola gelombang berulang dua arah, lengkap dengan kuat dan fasanya. Fondasi perhitungannya sama seperti pada Dari DFT ke FFT; yang berubah hanyalah jumlah axis, dan bilangan kompleks yang diperkenalkan di Bilangan Kompleks kembali berperan sebagai isi array spektrumnya.
Spektrum dua dimensi punya kebiasaan tata letak yang sama seperti spektrum 1-D: komponen frekuensi nol menumpang di pojok array. np.fft.fftshift menata ulang spektrum agar komponen itu berada di tengah — konvensi tampilan yang lazim di pengolahan citra. Supaya setiap angka bisa dilacak, kita memakai pola sintetis berukuran kecil, bukan foto.
Yang kita kerjakan pada chapter ini:
np.fft.fft2sebagai FFT per baris lalu per kolom, dengan satuan siklus per pikselnp.fft.fftshiftpada 1-D dan 2-D untuk memindahkan komponen frekuensi nol ke tengahnp.fft.ifft2untuk menyusun kembali citra dari spektrum
FFT Dua Dimensi: Baris Dahulu, lalu Kolom #
np.fft.fft2 bekerja dengan dua langkah berturut-turut: menghitung np.fft.fft sepanjang axis terakhir (tiap baris), lalu menghitung np.fft.fft sepanjang axis 0 pada hasil antaranya. Kedua langkah ini bisa ditukar urutannya dan hasilnya tetap sama. Karena ada dua arah, ada dua frekuensi pula: frekuensi sepanjang baris (kecepatan perubahan antar kolom) dan sepanjang kolom (antar baris). Satuannya siklus per piksel, padanan siklus per detik (Hz) pada sinyal waktu.
import numpy as np
citra = np.zeros((4, 8))
citra[:, ::2] = 1.0 # garis vertikal tiap 2 piksel
print(citra)
spec = np.fft.fft2(citra)
print("bentuk spektrum :", spec.shape, spec.dtype)
print("|spec[0]| :", np.abs(spec[0]).round(1))
print("maks |spec[1:]| :", np.abs(spec[1:]).max())Output yang Dihasilkan:
[[1. 0. 1. 0. 1. 0. 1. 0.]
[1. 0. 1. 0. 1. 0. 1. 0.]
[1. 0. 1. 0. 1. 0. 1. 0.]
[1. 0. 1. 0. 1. 0. 1. 0.]]
bentuk spektrum : (4, 8) complex128
|spec[0]| : [16. 0. 0. 0. 16. 0. 0. 0.]
maks |spec[1:]| : 0.0Mari kita bedah:
- Baris 4 (
citra[:, ::2] = 1.0): slice[:, ::2]mengisi semua baris pada kolom genap dengan 1, menghasilkan garis vertikal yang berulang tiap 2 piksel. Polanya berubah hanya bila kita bergerak menyamping. - Baris 6 (
spec = np.fft.fft2(citra)): bentuk output sama dengan input, (4, 8), bertipecomplex128. - Baris 8 (
|spec[0]|): energi hanya ada di baris 0 — frekuensi vertikalnya nol, karena antar baris tidak ada perbedaan. Dalam baris itu, bin 0 memuat 16 (jumlah seluruh piksel: 4 baris × nilai 4 per baris), dan bin 4 memuat 16: garis berperiode 2 piksel sama dengan 4 siklus sepanjang 8 kolom, yaitu 0,5 siklus per piksel. - Baris 9 (
maks |spec[1:]|): nilai 0.0 menegaskan baris 1 sampai 3 benar-benar kosong — spektrum 2-D yang bersih untuk pola yang bersih.
Frekuensi spasialnya terbaca langsung dari indeks: bin kolom ke-k mewakili k/N siklus per piksel. Konvensi tata letaknya sama persis dengan np.fft.fftfreq pada 1-D, yang kita bedah di fftfreq dan Spektrum Frekuensi.
Menata Pusat Spektrum dengan fftshift #
Pada array hasil FFT, komponen frekuensi nol (DC) berada di indeks 0, dan frekuensi negatif menempati paruh kedua array. Untuk menampilkan spektrum yang “berpusat”, np.fft.fftshift menggeser seluruh isi array sejauh setengah panjang di tiap axis sehingga frekuensi nol jatuh di tengah. Kita lihat versi 1-D-nya dulu:
import numpy as np
freq = np.fft.fftfreq(8, d=1/8)
print("fftfreq :", freq)
print("fftshift(freq) :", np.fft.fftshift(freq))Output yang Dihasilkan:
fftfreq : [ 0. 1. 2. 3. -4. -3. -2. -1.]
fftshift(freq) : [-4. -3. -2. -1. 0. 1. 2. 3.]Urutannya kini naik dari frekuensi paling negatif ke paling positif, dengan nol tepat di tengah. Pada array dua dimensi, penggeseran yang sama dilakukan pada kedua axis sekaligus, sehingga pojok kiri atas (0, 0) pindah ke pusat array:
import numpy as np
citra = np.zeros((4, 8))
citra[:, ::2] = 1.0
spec = np.fft.fft2(citra)
geser = np.fft.fftshift(spec)
print("DC sebelum : (0, 0) =", np.abs(spec[0, 0]).round(1))
print("DC sesudah : (2, 4) =", np.abs(geser[2, 4]).round(1))
print("garis sebelum: (0, 4) =", np.abs(spec[0, 4]).round(1))
print("garis sesudah: (2, 0) =", np.abs(geser[2, 0]).round(1))Output yang Dihasilkan:
DC sebelum : (0, 0) = 16.0
DC sesudah : (2, 4) = 16.0
garis sebelum: (0, 4) = 16.0
garis sesudah: (2, 0) = 16.0Dua posisi itu menjelaskan mekanismenya: DC pindah dari (0, 0) ke (2, 4) — tepat (4//2, 8//2), pusat array — dan komponen garis vertikal pindah dari (0, 4) ke (2, 0), tergeser sebanyak setengah panjang di tiap axis. Magnitudennya tidak berubah; np.fft.fftshift hanya menata ulang posisi. Dalam praktik, spektrum citra hampir selalu dilihat setelah np.fft.fftshift, biasanya dengan magnitude yang diskalakan logaritmik agar komponen kecil tetap terbaca.
Mengembalikan Citra dengan ifft2 #
Pasangan inversnya adalah np.fft.ifft2: dari spektrum, citra disusun kembali. Sama seperti np.fft.ifft, hasilnya selalu complex128 walaupun citranya riil, jadi bagian riilnya yang diambil.
import numpy as np
citra = np.zeros((4, 8))
citra[:, ::2] = 1.0
pulih = np.fft.ifft2(np.fft.fft2(citra))
print("dtype pulih :", pulih.dtype)
print(np.real(pulih).round(4))
print("selisih maks:", np.abs(pulih - citra).max())
print("imaj maks :", np.abs(pulih.imag).max())Output yang Dihasilkan:
dtype pulih : complex128
[[1. 0. 1. 0. 1. 0. 1. 0.]
[1. 0. 1. 0. 1. 0. 1. 0.]
[1. 0. 1. 0. 1. 0. 1. 0.]
[1. 0. 1. 0. 1. 0. 1. 0.]]
selisih maks: 0.0
imaj maks : 0.0Putaran fft2 lalu ifft2 pada spektrum yang tidak diubah mengembalikan citra persis, dengan selisih 0.0 — pada pola kecil berisi bilangan bulat ini tidak ada residu sama sekali, sedangkan umumnya residu floating-point sekitar 10⁻¹⁶ seperti yang kita lihat di Dari DFT ke FFT.
Kombinasi fft2 → edit spektrum → ifft2 adalah pola kerja utama pengolahan citra di domain frekuensi: menghapus pola periodik yang tidak diinginkan, meredam derau, dan menganalisis tekstur. Pembahasan praktiknya ada pada chapter NumPy untuk Pemrosesan Gambar.