Gambar sebagai array

Memuat gambar, memeriksa shape, dan memahami kanal warna.

Daftar isi

Gambar digital pada dasarnya adalah kumpulan angka yang tersusun rapi, sehingga array NumPy menjadi wadah alami untuk menyimpan dan mengolahnya. Topik ini membahas bentuk array gambar grayscale dan RGB, dtype uint8 beserta rentang nilainya, posisi kanal warna sebagai axis ketiga, serta varian float dengan rentang 0.0 sampai 1.0 yang lazim dipakai pada pemrosesan gambar. Seluruh contoh memakai array sintetis berukuran kecil agar setiap keluaran dapat direproduksi, dan topik ditutup dengan uji coba menyimpan serta memuat kembali berkas PNG.

Gambar grayscale sebagai array dua dimensi#

Gambar grayscale, yaitu gambar hitam putih tanpa informasi warna, direpresentasikan sebagai array dua dimensi dengan shape (tinggi, lebar). Setiap elemen array adalah satu piksel (pixel), yaitu titik terkecil penyusun gambar, dan nilainya menyatakan derajat terang titik tersebut. Nilai 0 berarti hitam penuh, nilai 255 berarti putih penuh, dan nilai di antaranya adalah gradasi abu-abu. Tipe data yang lazim adalah uint8 karena tipe itu menampung tepat 256 nilai dengan 1 byte per piksel, sehingga representasinya sama persis dengan cara sebagian besar format berkas menyimpan gambar.

Array gambar tidak harus berasal dari berkas. Untuk pembelajaran, array sintetis berukuran kecil lebih praktis karena isinya dapat dilihat seluruhnya dan keluarannya pasti sama di setiap eksekusi. Kode berikut membuat gambar gradien 5x4, kemudian memeriksa atribut-atribut pokoknya.

python
import numpy as np

gradasi = np.arange(20, dtype=np.uint8).reshape(5, 4)
print(gradasi)
print(gradasi.shape, gradasi.dtype)
print(gradasi.min(), gradasi.max())
print(gradasi[2, 3])
text Keluaran
[[ 0  1  2  3]
 [ 4  5  6  7]
 [ 8  9 10 11]
 [12 13 14 15]
 [16 17 18 19]]
(5, 4) uint8
0 19
11

Keluaran memperlihatkan array 5x4 dengan 20 piksel bernilai 0 sampai 19, dtype uint8, serta nilai minimum 0 dan maksimum 19. Ingat bahwa dimensi pertama adalah baris dan dimensi kedua adalah kolom, sehingga gradasi[2, 3] berarti baris indeks 2, kolom indeks 3, dan menghasilkan 11. Urutan ini sama dengan aturan [baris, kolom] pada pengindeksan 2D umum, tetapi sering terbalik dilakukan karena kebiasaan menyebut koordinat x terlebih dahulu.

Kanal warna sebagai axis ketiga#

Gambar berwarna menambahkan satu dimensi lagi pada arraynya. Representasi RGB menyusun setiap piksel sebagai campuran tiga kanal (channel), yaitu merah, hijau, dan biru, sehingga arraynya berdimensi tiga dengan shape (tinggi, lebar, 3). Axis ketiga inilah yang menyimpan kanal warna, dan satu piksel berwarna kini berupa tiga nilai sekaligus, bukan satu nilai. Gambar berikut memetakan hubungan indeks baris, kolom, dan kanal pada kedua jenis array gambar.

Grayscale: [baris, kolom]0123kolom012barisabu[1, 2]RGB: [baris, kolom, kanal]kanal ke-3 menentukan warna0merah1hijau2birurgb[1, 2, k]
Peta indeks array gambar: [baris, kolom] untuk grayscale dan [baris, kolom, kanal] untuk RGB

Kode berikut membentuk gambar RGB 4x4 dari satu array grayscale yang digandakan menjadi tiga kanal dengan komposisi berbeda.

python
import numpy as np

abu = np.arange(16, dtype=np.uint8).reshape(4, 4)
rgb = np.dstack((abu, abu * 2, abu * 4))
print(rgb.shape, rgb.dtype)
print(rgb[1, 2])
print(rgb[:, :, 0])
text Keluaran
(4, 4, 3) uint8
[ 6 12 24]
[[ 0  1  2  3]
 [ 4  5  6  7]
 [ 8  9 10 11]
 [12 13 14 15]]

Array rgb memiliki shape (4, 4, 3), dan rgb[1, 2] menghasilkan tiga nilai sekaligus, yaitu [ 6 12 24], karena piksel tersebut dibaca pada ketiga kanalnya. Nilainya bertingkat karena komponen hijau dihitung dua kali lipat dan biru empat kali lipat dari komponen merahnya. Sementara itu, rgb[:, :, 0] mengambil seluruh baris dan kolom pada kanal pertama saja, sehingga hasilnya kembali menjadi array dua dimensi yang mewakili komponen merah. Slice semacam ini merupakan view atas rgb, sebagaimana aturan umum slicing pada NumPy, sehingga perubahan pada hasilnya juga mengubah rgb.

Rentang nilai uint8 dan varian float#

Konvensi 0 sampai 255 pada uint8 mendominasi penyimpanan berkas karena hemat memori dan sesuai dengan spesifikasi format yang umum. Namun, banyak pustaka pemrosesan gambar bekerja dengan konvensi kedua, yaitu float dengan rentang 0.0 sampai 1.0. Konversinya cukup membagi array dengan 255. Alasan konvensi float dipakai pada pemrosesan adalah aman terhadap overflow, karena hasil perkalian dan pembagian antar nilai pecahan tidak akan keluar dari jangkauan float64, sedangkan aritmetika uint8 dapat melipat nilai diam-diam. Kunci bermain aman adalah mengetahui konvensi mana yang sedang dipegang, dan itu selalu bisa diperiksa dari dtype serta nilai maksimalnya.

python
import numpy as np

abu = np.arange(16, dtype=np.uint8).reshape(4, 4)
abu_float = abu / 255
print(abu_float.dtype)
print(abu_float[0, 0], abu_float[1, 2])
print(abu_float.min(), abu_float.max())
text Keluaran
float64
0.0 0.023529411764705882
0.0 0.058823529411764705

Pembagian dengan 255 menghasilkan array float64, dan piksel bernilai 6 kini menjadi 0.023529411764705882, yaitu 6/255. Rasio antar piksel tidak berubah, hanya skalanya yang bergeser dari 0-255 menjadi 0.0-1.0.

Menyimpan dan memuat kembali gambar kecil#

Alur kerja dengan berkas nyata mencakup dua arah, yaitu menyimpan array menjadi berkas gambar dan memuat berkas kembali menjadi array. Pustaka imageio membaca berkas gambar dan mengembalikan ndarray, sedangkan Pillow menyediakan penyimpanan berkas dari ndarray. Contoh berikut membuat PNG kecil dari array sintetis dengan Image.fromarray, memuatnya kembali dengan imageio, membandingkan isinya, lalu membersihkan berkas sementara melalui tempfile.

python
import os
import tempfile

import imageio.v3 as iio
import numpy as np
from PIL import Image

pola = np.arange(48, dtype=np.uint8).reshape(6, 8) * 5
print(pola.shape, pola.dtype)

with tempfile.TemporaryDirectory() as folder:
    berkas = os.path.join(folder, "pola.png")
    Image.fromarray(pola).save(berkas)
    muat = iio.imread(berkas)
    print(muat.shape, muat.dtype)
    print(np.array_equal(pola, muat))
text Keluaran
(6, 8) uint8
(6, 8) uint8
True

Array yang disimpan dan array yang dimuat memiliki shape, dtype, dan isi yang identik, ditandai oleh True dari np.array_equal, sehingga dua arah konversinya tidak menghilangkan data. Pemeriksaan kesetaraan semacam ini menjadi kebiasaan yang berguna saat mencoba pustaka pemuatan baru, karena pustaka gambar di ekosistem Python memiliki kebiasaan keluaran yang berbeda. Sebagian pustaka mengembalikan ndarray, sedangkan sebagian lainnya mengembalikan objek gambar pustakanya sendiri, sehingga imageio dipakai di sini karena keluarannya berupa ndarray yang langsung siap diolah.

Untuk berkas foto sungguhan, polanya sama saja, yaitu memanggil fungsi pemuatan dan mendapatkan ndarray.

python
# tidak-dijalankan
import imageio.v3 as iio

foto = iio.imread("foto.jpg")
print(foto.shape, foto.dtype)

Keluaran yang diharapkan dari pola di atas berupa shape bertingkat tiga untuk berkas JPEG berwarna, misalnya (1080, 1920, 3), dengan dtype uint8. Bentuk persisnya bergantung pada resolusi berkas yang dimuat.

Kesalahan umum#

Menganggap setiap gambar berwarna otomatis berdimensi tiga. Gambar grayscale yang dimuat tetap berbentuk (tinggi, lebar), dan kode yang memotong kanal ketiga akan gagal atau salah memproses. Perbaikannya adalah memeriksa foto.ndim lebih dahulu, kemudian menangani kasus dua dimensi dan tiga dimensi secara terpisah.

Mencampur konvensi rentang 0-255 dan 0.0-1.0 dalam satu alur. Menambahkan konstanta kecil ke array float 0.0-1.0 seolah-olah masih 0-255 menghasilkan nilai di atas 1.0 yang tampak putih penuh saat ditampilkan. Perbaikannya adalah mengunci satu konvensi per alur kerja, dan memeriksa dtype beserta max() pada awal maupun akhir pemrosesan.

Membalik urutan indeks menjadi [kolom, baris]. Pada gambar yang lebar dan tingginya berbeda, indeks terbalik dapat menunjuk piksel lain tanpa error apa pun, sehingga hasil olahan tidak terduga. Perbaikannya adalah mengingat bahwa axis 0 selalu baris, sehingga indeksnya ditulis [baris, kolom] atau [baris, kolom, kanal]. Kebiasaan yang membantu adalah menyusun indeks dalam variabel bernama jelas, misalnya b untuk baris dan k untuk kolom, sebelum digabung menjadi satu ekspresi pengindeksan.

Ringkasan#

  • Gambar grayscale adalah array dua dimensi dengan shape (tinggi, lebar), sedangkan gambar RGB adalah array tiga dimensi dengan shape (tinggi, lebar, 3) dan kanal warna berada pada axis ketiga.
  • dtype uint8 menyimpan satu piksel sebagai 1 byte dengan rentang 0 sampai 255, sedangkan konvensi float memakai rentang 0.0 sampai 1.0 yang diperoleh dengan membagi array dengan 255.
  • Inspeksi shape, dtype, min(), dan max() sebelum mengolah menentukan konvensi rentang yang sedang dipakai dan mencegah kesalahan arah indeks.
  • imageio mengembalikan ndarray saat memuat berkas gambar, dan Pillow menyimpan ndarray sebagai berkas, sehingga array dan berkas saling dipertukarkan tanpa kehilangan data.

Referensi#