Gambar grayscale sebenarnya adalah matriks: satu sel per piksel, berisi kecerahan dari 0 (hitam) sampai 255 (putih) dalam tipe uint8. Gambar warna menambah satu sumbu untuk kanal merah, hijau, biru. Karena itulah pustaka seperti Pillow, OpenCV, dan scikit-image menerima serta mengembalikan array NumPy — semua operasi citra pada dasarnya adalah operasi array.

Agar tidak bergantung pada file gambar, kita membuat citra sintetis kecil berukuran 6×6 piksel berpola gradien. Dari citra ini kita praktikkan operasi dasar pengolahan gambar: kecerahan, kontras, pemangkasan, pembalikan, penurunan resolusi, dan normalisasi.

Membuat Citra Sintetis #

Pola gradien dibuat dari np.arange yang dikalikan konstanta, lalu di-reshape menjadi matriks. Setiap baris lanjut dari baris sebelumnya, dan setiap kolom naik 7 tingkat kecerahan:

python
import numpy as np

citra = (np.arange(36) * 7).reshape(6, 6).astype(np.uint8)
print(citra)
print(citra.dtype, citra.min(), citra.max())

Output yang Dihasilkan:

text
[[  0   7  14  21  28  35]
 [ 42  49  56  63  70  77]
 [ 84  91  98 105 112 119]
 [126 133 140 147 154 161]
 [168 175 182 189 196 203]
 [210 217 224 231 238 245]]
uint8 0 245

Mari kita bedah:

  1. Baris 3 ((np.arange(36) * 7)): deret 0-35 dikalikan 7 menjadi deret kecerahan 0-245; konstanta pengali menentukan seberapa kasar gradasinya.
  2. Baris 3 (.reshape(6, 6)): deret satu dimensi ditata menjadi matriks piksel baris demi baris.
  3. Baris 3 (.astype(np.uint8)): tipe data piksel mengikuti konvensi gambar digital — satu byte per piksel, rentang 0-255.
  4. Baris 5: dtype, nilai terkecil (0, hitam), dan nilai terbesar (245, mendekati putih) memastikan citra berada di rentang yang sah.

Mengubah Kecerahan dan Kontras #

Menambah kecerahan berarti menambah nilai piksel dengan konstanta. Pada tipe uint8, penjumlahan yang melampaui 255 tidak berhenti di putih, melainkan melompati batas dan kembali dari bawah — perbaikannya: hitung dulu di tipe lebih lebar, lalu np.clip sebelum kembali ke uint8 (pola dari Chapter Clamp dan clip):

python
import numpy as np

citra = (np.arange(36) * 7).reshape(6, 6).astype(np.uint8)
print("tanpa clip:", citra[-1] + 50)
aman = np.clip(citra.astype(np.int16) + 50, 0, 255).astype(np.uint8)
print("dengan clip:", aman[-1])
kontras = np.clip((citra.astype(np.float64) - 120) * 1.4 + 120, 0, 255).astype(np.uint8)
print("kontras baris-0:", kontras[0])
print("rentang kontras:", kontras.min(), "-", kontras.max())

Output yang Dihasilkan:

text
tanpa clip: [ 4 11 18 25 32 39]
dengan clip: [255 255 255 255 255 255]
kontras baris-0: [0 0 0 0 0 1]
rentang kontras: 0 - 255

Penjelasan tiap langkah:

  1. Baris 4 (citra[-1] + 50): baris terakhir bernilai 210-245; setelah ditambah 50, uint8 mengulang dari bawah sehingga hasilnya justru gelap (4-39) — piksel putih berubah hitam.
  2. Baris 5-6 (.astype(np.int16) + np.clip): penjumlahan dilakukan di int16 yang tidak meluap, lalu np.clip(..., 0, 255) menahan nilai di batas atas; hasilnya baris terakhir menjadi putih penuh.
  3. Baris 7 (rumus kontras): setiap piksel digeser dari titik tengah 120 lalu diperbesar 1,4 kali; piksel yang jauh dari tengah makin ekstrem, piksel di dekat tengah hampir tidak berubah.
  4. Baris 8-9: baris pertama (nilai rendah) jatuh ke 0 dan rentang hasil memenuhi 0-255 — tanda kontras kini lebih tinggi.

Memangkas dan Membalik Citra #

Pemangkasan (crop) dan pembalikan (flip) tidak butuh fungsi khusus — keduanya hanyalah slicing dan langkah negatif dari Chapter Slicing Array:

python
import numpy as np

citra = (np.arange(36) * 7).reshape(6, 6).astype(np.uint8)
potongan = citra[1:4, 2:5]
cermin = citra[:, ::-1]
print(potongan)
print("bentuk potongan:", potongan.shape)
print("baris-0 cermin :", cermin[0])

Output yang Dihasilkan:

text
[[ 56  63  70]
 [ 98 105 112]
 [140 147 154]]
bentuk potongan: (3, 3)
baris-0 cermin : [35 28 21 14  7  0]
  1. Baris 4 (citra[1:4, 2:5]): baris 1-3 dan kolom 2-4 diambil menjadi citra 3×3; nilai awalnya 56 karena indeks baris 1, kolom 2 = 7 * (6 + 2).
  2. Baris 5 (citra[:, ::-1]): langkah -1 pada sumbu kolom membalik urutan piksel dari kiri ke kanan — cermin horizontal.
  3. Catatan penting: potongan dan cermin adalah view, bukan salinan. Mengubah salah satu piksel pada potongan ikut mengubah citra asal; gunakan .copy() bila hasilnya harus berdiri sendiri (lihat Chapter View vs Copy).

Pola serupa untuk arah lain: citra[::-1, :] membalik secara vertikal, dan citra[::-1, ::-1] melakukan rotasi 180 derajat.

Dua operasi penutup yang sering muncul sebelum citra masuk ke model machine learning:

python
import numpy as np

citra = (np.arange(36) * 7).reshape(6, 6).astype(np.uint8)
kecil = citra[::2, ::2]
normal = citra.astype(np.float64) / 255.0
print(kecil)
print("bentuk baru  :", kecil.shape)
print("sampel float :", normal[0, :4])
print("rentang float:", normal.min(), "-", normal.max())

Output yang Dihasilkan:

text
[[  0  14  28]
 [ 84  98 112]
 [168 182 196]]
bentuk baru  : (3, 3)
sampel float : [0.         0.02745098 0.05490196 0.08235294]
rentang float: 0.0 - 0.9607843137254902
  1. Baris 4 (citra[::2, ::2]): langkah 2 pada kedua sumbu mengambil satu piksel dari setiap blok 2×2, memperkecil citra menjadi 3×3 tanpa loop; pola striding ini dibahas pada Chapter Strides dan Memory Access.
  2. Baris 5 (.astype(np.float64) / 255.0): piksel dikonversi ke float lalu dibagi 255 sehingga rentangnya menjadi 0.0-1.0; model machine learning umumnya lebih stabil dengan masukan rentang kecil seperti ini.
  3. Baris 9: nilai terbesar 0.9608 (bukan 1.0) karena citra asal memang tidak memiliki piksel 255.

Ringkasan Operasi Citra NumPy #

Operasi citraSintaks NumPySifatnya
Kecerahannp.clip(c.astype(np.int16) + n, 0, 255)array baru, aman dari meluap
Kontrasnp.clip((c - 120) * f + 120, 0, 255)faktor f > 1 menambah kontras
Cropc[a:b, x:y]view
Flip horizontal / vertikalc[:, ::-1] / c[::-1, :]view
Downsamplec[::2, ::2]view berlangkah
Normalisasi 0-1c.astype(np.float64) / 255.0array baru, float

Untuk gambar warna, setiap operasi di atas tetap bekerja — array tinggal berbentuk (tinggi, lebar, 3), dan parameter axis yang menentukan apakah pengurangan dilakukan per piksel atau per kanal.