Operasi gambar

Crop, flip, dan pengaturan kecerahan melalui aritmetika array.

Daftar isi

Operasi gambar yang paling sering dipakai pada pengolahan berbasis NumPy sebenarnya hanyalah kombinasi tiga hal, yaitu slicing untuk memotong, slicing dengan langkah negatif untuk membalik, dan aritmetika array untuk mengatur kecerahan. Topik ini membahas ketiganya secara berurutan pada array gambar sintetis, membuktikan sifat view pada hasil pemotongan, menunjukkan jebakan overflow pada aritmetika uint8, lalu menyusun semuanya menjadi satu rangkaian operasi yang utuh.

Crop: memotong area dengan slicing#

Crop, yaitu memotong sebagian area gambar, dilakukan dengan slicing dua dimensi sekaligus, yaitu gambar[baris_awal:baris_akhir, kolom_awal:kolom_akhir]. Tidak ada fungsi khusus yang dibutuhkan karena aturannya persis sama dengan slicing array biasa: batas awal ikut, batas akhir tidak ikut. Teknik ini adalah fondasi bagi kebutuhan yang lebih besar, misalnya mengambil jendela pemindai (scan window) pada deteksi objek atau memangkas tepi gambar yang mengandung noise.

Ukuran hasil crop dapat dihitung dari batasnya tanpa menjalankan kode, yaitu tingginya selisih batas baris dan lebarnya selisih batas kolom. Indeks negatif tetap berlaku, sehingga pemangkasan tepi bawah dan kanan dapat ditulis pola[:-2, :-3] tanpa mengetahui ukuran gambarnya lebih dahulu.

python
import numpy as np

pola = np.arange(48, dtype=np.uint8).reshape(6, 8) * 5
potongan = pola[1:4, 2:6]
print(potongan)
print(potongan.shape)
print(np.shares_memory(pola, potongan))
text Keluaran
[[ 50  55  60  65]
 [ 90  95 100 105]
 [130 135 140 145]]
(3, 4)
True

Hasil crop berukuran 3x4, sesuai banyaknya baris dan kolom yang diminta. Yang penting dicermati adalah True dari np.shares_memory, yang membuktikan bahwa potongan adalah view atas pola, bukan array berdiri sendiri. Konsekuensinya, mengubah nilai di potongan juga mengubah pola. Bila hasil crop akan dimodifikasi sementara gambar asal harus utuh, panggil .copy() pada hasil pemotongan sebelum apa pun dilakukan terhadapnya.

Flip: pencerminan dengan langkah negatif#

Flip, yaitu pencerminan gambar, dicapai dengan langkah negatif pada slicing, yaitu ::-1. Slicing pola[::-1] membalik urutan baris sehingga gambar tercermin secara vertikal, yaitu atas dan bawah bertukar tempat, sedangkan pola[:, ::-1] membalik urutan kolom sehingga kiri dan kanan bertukar tempat. Fungsi np.flip melakukan hal yang sama dengan penulisan yang lebih eksplisit melalui parameter axis, sehingga pilihan keduanya sebaiknya konsisten dalam satu proyek. Pencerminan semacam ini sering dipakai untuk memperbanyak contoh latihan pada tahap data augmentation gambar.

Aturan yang sama berlaku pada gambar RGB tiga dimensi, karena pencerminan atas-bawah dan kiri-kanan cukup menyentuh axis pertama dan kedua, sementara axis kanal dibiarkan utuh. Langkah negatif pada axis ketiga justru punya makna lain, yaitu membalik urutan kanal dari RGB menjadi BGR, sebuah variasi yang memang muncul pada beberapa pustaka gambar.

python
import numpy as np

pola = np.arange(48, dtype=np.uint8).reshape(6, 8) * 5
cermin_atas = pola[::-1]
cermin_samping = pola[:, ::-1]
print(cermin_atas[0])
print(cermin_samping[0])
print(np.array_equal(pola[::-1], np.flip(pola, axis=0)))
text Keluaran
[200 205 210 215 220 225 230 235]
[35 30 25 20 15 10  5  0]
True

Baris pertama cermin_atas berisi nilai 200 sampai 235, yaitu baris terakhir pola, karena urutan barisnya sudah dibalik. Baris pertama cermin_samping tetap berasal dari baris pertama pola, tetapi urutan kolomnya terbalik, tampak dari nilai 35 sampai 0. Keluaran True mengonfirmasi bahwa slicing negatif dan np.flip(pola, axis=0) menghasilkan isi yang sama persis.

Kecerahan: aritmetika dan jebakan uint8#

Menaikkan kecerahan berarti menambahkan konstanta ke seluruh piksel, dan di situlah dtype uint8 menjadi sumber jebakan. Rentang uint8 hanya 0 sampai 255, sedangkan aritmetika NumPy pada tipe ini melipat hasil yang melebihinya tanpa error maupun warning, sehingga piksel yang seharusnya makin terang justru berbalik menjadi gelap.

python
import numpy as np

piksel = np.array([200, 230, 255], dtype=np.uint8)
print(piksel + 40)
text Keluaran
[240  14  39]

Piksel 200 memang naik menjadi 240, tetapi 230 meluap menjadi 14 dan 255 meluap menjadi 39. Peristiwa itu terjadi karena hasil sebenarnya, yaitu 270 dan 295, melampaui batas atas 255. Perbaikan standar untuk kasus ini terdiri atas empat langkah, yaitu konversi ke tipe yang lebih lebar terlebih dahulu, operasi aritmetikanya, pembatasan ke rentang 0-255 dengan np.clip, dan konversi kembali menjadi uint8.

python
import numpy as np

piksel = np.array([200, 230, 255], dtype=np.uint8)
naik = piksel.astype(np.int16) + 40
aman = np.clip(naik, 0, 255).astype(np.uint8)
print(naik)
print(aman)
text Keluaran
[240 270 295]
[240 255 255]

Pada array int16, hasil 270 dan 295 tersimpan utuh, lalu np.clip menahannya di 255 sehingga piksel yang sudah putih penuh tidak berbalik gelap. Pola yang sama persis, yaitu konversi, operasi, pembatasan, dan konversi kembali, diterapkan pada gambar utuh di bagian penutup topik ini.

Menggabungkan operasi#

Rangkaian crop, flip, dan pengaturan kecerahan ditulis sebagai bentukan-bentukan array yang berurutan, dan urutan tersebut dapat dibaca dari atas ke bawah layaknya resep. Satu kebiasaan diterapkan di sini, yaitu menyalin hasil crop dengan .copy() sebelum operasi berikutnya, sehingga seluruh modifikasi berlangsung pada data yang berdiri sendiri.

Urutan tahapan memengaruhi hasil, sehingga membalik lalu memotong bukanlah hal yang sama dengan memotong lalu membalik, kecuali area yang diminta disesuaikan. Sifat memori setiap tahap juga berbeda, yaitu cermin masih berupa view atas potongan, sedangkan terang adalah array baru hasil konversi dan pembatasan. Membaca rangkaian seperti ini dari atas ke bawah sekaligus menjadi latihan membedakan view dan salinan.

python
import numpy as np

pola = np.arange(48, dtype=np.uint8).reshape(6, 8) * 5
potongan = pola[1:4, 2:6].copy()
cermin = potongan[::-1]
terang = np.clip(cermin.astype(np.int16) + 40, 0, 255).astype(np.uint8)
print(cermin)
print(terang)
text Keluaran
[[130 135 140 145]
 [ 90  95 100 105]
 [ 50  55  60  65]]
[[170 175 180 185]
 [130 135 140 145]
 [ 90  95 100 105]]

cermin membalik urutan baris milik potongan, sehingga baris pertamanya berisi nilai 130 sampai 145. Operasi kecerahan kemudian menambah 40 pada setiap nilai, dan karena nilai maksimalnya, yaitu 145, masih jauh dari 255, tidak ada piksel yang terpotong. Hasil akhirnya adalah array uint8 baru yang siap disimpan atau diproses lebih lanjut.

Kesalahan umum#

Menambahkan konstanta langsung ke gambar uint8. Pelipatan nilai pada 230 + 40 dan 255 + 40 sudah ditunjukkan di atas, dan pada gambar nyata kerusakan ini menampakkan diri sebagai piksel gelap yang mendadak di area yang seharusnya terang. Perbaikannya adalah pola empat langkah, yaitu astype(np.int16), operasi, np.clip ke rentang 0-255, dan konversi kembali ke uint8.

Menganggap hasil crop berdiri sendiri. Karena slicing menghasilkan view, mengubah hasil crop sama dengan mengubah gambar asalnya, dan perubahan itu baru disadari ketika gambar asal dipakai lagi di bagian lain. Perbaikannya adalah memanggil .copy() pada hasil pemotongan, lalu memeriksa dengan np.shares_memory apabila perlu kepastian.

Membalik axis yang keliru. pola[::-1] menukar atas dengan bawah, sedangkan pola[:, ::-1] menukar kiri dengan kanan, dan keduanya sering tertukar saat menulis cepat. Perbaikannya adalah menentukan arah cermin yang diinginkan lebih dahulu, lalu memilih penulisan np.flip dengan parameter axis yang eksplisit agar niatnya terbaca jelas dari kodenya.

Ringkasan#

  • Crop dilakukan dengan slicing dua dimensi gambar[baris_awal:baris_akhir, kolom_awal:kolom_akhir] dan hasilnya adalah view, sehingga .copy() diperlukan sebelum modifikasi.
  • Flip memakai langkah negatif, yaitu pola[::-1] untuk pencerminan atas-bawah dan pola[:, ::-1] untuk kiri-kanan, dengan np.flip sebagai padanan yang eksplisit.
  • Aritmetika uint8 melipat nilai yang melampaui 255 tanpa peringatan, sehingga pengaturan kecerahan memakai pola konversi ke int16, operasi, np.clip, lalu kembali ke uint8.
  • Operasi gambar bersifat komposabel, dan setiap tahapan menghasilkan array baru yang dapat dilanjutkan ke tahapan berikutnya.

Referensi#