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:
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:
[[ 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 245Mari kita bedah:
- Baris 3 (
(np.arange(36) * 7)): deret 0-35 dikalikan 7 menjadi deret kecerahan 0-245; konstanta pengali menentukan seberapa kasar gradasinya. - Baris 3 (
.reshape(6, 6)): deret satu dimensi ditata menjadi matriks piksel baris demi baris. - Baris 3 (
.astype(np.uint8)): tipe data piksel mengikuti konvensi gambar digital — satu byte per piksel, rentang 0-255. - 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):
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:
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 - 255Penjelasan tiap langkah:
- Baris 4 (
citra[-1] + 50): baris terakhir bernilai 210-245; setelah ditambah 50,uint8mengulang dari bawah sehingga hasilnya justru gelap (4-39) — piksel putih berubah hitam. - Baris 5-6 (
.astype(np.int16)+np.clip): penjumlahan dilakukan diint16yang tidak meluap, lalunp.clip(..., 0, 255)menahan nilai di batas atas; hasilnya baris terakhir menjadi putih penuh. - 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.
- 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:
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:
[[ 56 63 70]
[ 98 105 112]
[140 147 154]]
bentuk potongan: (3, 3)
baris-0 cermin : [35 28 21 14 7 0]- 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). - Baris 5 (
citra[:, ::-1]): langkah-1pada sumbu kolom membalik urutan piksel dari kiri ke kanan — cermin horizontal. - Catatan penting:
potongandancerminadalah 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.
Menurunkan Resolusi dan Menormalkan Piksel #
Dua operasi penutup yang sering muncul sebelum citra masuk ke model machine learning:
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:
[[ 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- 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. - 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. - Baris 9: nilai terbesar 0.9608 (bukan 1.0) karena citra asal memang tidak memiliki piksel 255.
Ringkasan Operasi Citra NumPy #
| Operasi citra | Sintaks NumPy | Sifatnya |
|---|---|---|
| Kecerahan | np.clip(c.astype(np.int16) + n, 0, 255) | array baru, aman dari meluap |
| Kontras | np.clip((c - 120) * f + 120, 0, 255) | faktor f > 1 menambah kontras |
| Crop | c[a:b, x:y] | view |
| Flip horizontal / vertikal | c[:, ::-1] / c[::-1, :] | view |
| Downsample | c[::2, ::2] | view berlangkah |
| Normalisasi 0-1 | c.astype(np.float64) / 255.0 | array 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.