5.16. Kernel konvolusi kustom

Filter tetangga yang telah dibahas sejauh ini masing-masing memiliki statistik bawaan yang diterapkan filter ke jendela di setiap posisi -- rata-rata, rata-rata berbobot Gaussian, median. morph() adalah satu-satunya filter yang memungkinkan aplikasi menyediakan statistik itu sendiri, dalam bentuk kernel: matriks kecil berisi bobot yang mendeskripsikan bagaimana filter harus menggabungkan piksel-piksel tetangga menjadi satu nilai keluaran tunggal.

Mekanismenya adalah operasi konvolusi klasik. Di setiap posisi keluaran, setiap piksel tetangga dikalikan dengan bobot yang sesuai dalam kernel, hasilnya dijumlahkan, kemudian secara opsional diskala dan diimbangi, dan nilainya ditulis ke piksel keluaran. Kernel yang berbeda menghasilkan hasil berbeda dari masukan yang sama. Kernel dengan semua bobot positif yang sama mereproduksi filter mean(); yang berbentuk lonceng mereproduksi gaussian(). Pola di luar itu menghasilkan respons tepi, emboss, gradien, penajaman, motion blur, dan katalog panjang efek lainnya -- semua yang pernah ingin dilakukan pemrosesan citra klasik dengan satu lintasan linear.

5.16.1. Metode morph

Tanda tangannya terlihat seperti filter tetangga lainnya dengan satu argumen tambahan:

img.morph(size, kernel, mul=1.0, add=0.0)

size adalah radius dengan cara yang sama seperti di tempat lain, sehingga kernel harus memiliki tepat (2 * size + 1) baris kali (2 * size + 1) kolom. Kernel itu sendiri adalah daftar Python datar berisi angka-angka tersebut, dalam urutan baris-utama -- entri (2 * size + 1) pertama adalah baris atas, yang berikutnya (2 * size + 1) adalah baris kedua, dan seterusnya, hingga baris bawah. mul menskalakan jumlah hasil kali sebelum ditulis ke piksel keluaran, dan add menambahkan konstanta. Nilai default mul=1.0 dan add=0.0 membiarkan keluaran konvolusi tidak berubah.

Satu detail yang perlu dijelaskan secara eksplisit: metode ini secara otomatis membagi jumlah hasil kali dengan jumlah entri kernel sebelum menulis keluaran. Pembagian otomatis itu berarti kernel rata-rata yang entri-entrinya berjumlah sembilan -- blur kotak 3-kali-3, misalnya -- keluar pada skala satu per sembilan tanpa usaha ekstra, dan kernel aproksimasi Gaussian yang berjumlah enam belas keluar pada skala satu per enam belas, keduanya tanpa aplikasi harus menghitung pembagian itu sendiri. Aplikasi menetapkan mul hanya ketika menginginkan skala tambahan di atas normalisasi otomatis -- atau, lebih umum, ketika kernel berjumlah nol (kernel respons tepi) dan pembagian otomatis akan menjadi pembagian dengan nol. Framework memperlakukan jumlah sebagai satu dalam kasus tersebut, dan mul menjadi satu-satunya kontrol untuk menjaga jumlah hasil kali yang tidak diskalakan dalam batas.

Pasangan threshold=True / offset=N dari bagian ambang batas adaptif juga berfungsi pada morph(), sehingga framework kernel kustom yang sama dapat menghasilkan ambang batas biner yang batasnya dihitung dengan statistik kustom.

5.16.2. Tata letak kernel

Kernel 3-kali-3 (size=1) adalah daftar datar sembilan angka yang disusun dari kiri ke kanan, dari atas ke bawah. Konvensi ini terbaca alami jika daftar dipecah menjadi tiga baris Python:

sobel_x = [-1,  0,  1,
           -2,  0,  2,
           -1,  0,  1]

Ini adalah operator gradien Sobel-x -- kernel standar pertama yang ingin digunakan aplikasi mana pun dan berguna untuk ditelusuri dari awal hingga akhir. Polanya mudah dipahami: bobot negatif di kolom kiri, bobot positif di kolom kanan, dengan kolom tengah nol. Bobot baris -1, -2, -1 (atau 1, 2, 1 di sisi kanan) lebih tinggi di tengah dibanding di sudut, yang memberi baris tengah lebih banyak pengaruh atas hasil dibanding baris sudut.

Ketika kernel menyapu tepi vertikal -- kolom piksel yang beralih dari gelap di sisi kiri ke terang di sisi kanan -- bobot negatif mengambil sisi gelap dan bobot positif mengambil sisi terang. Jumlah hasil kali adalah angka positif besar, yang dituliskan filter sebagai piksel keluaran terang. Patch horizontal dengan kecerahan seragam menghasilkan nol, karena setiap bobot positif diimbangi oleh bobot negatif dengan besaran yang sama pada piksel dengan nilai yang sama.

Menjalankan kernel:

img.morph(1, sobel_x, mul=0.25)

Kernel Sobel berjumlah nol -- setiap bobot negatif di sisi kiri diimbangi oleh bobot positif yang sama di sisi kanan -- sehingga pembagian otomatis tidak membagi dengan apapun, dan mul adalah satu-satunya skala pada jumlah hasil kali. mul=0.25 menjaga respons dalam batas: jumlah absolut terbesar yang dapat dihasilkan Sobel-x dari patch 3-kali-3 adalah sekitar 4 * 255 = 1020 (delapan piksel terang berbobot hingga 2), dan membaginya dengan empat menempatkan kasus ekstrem di 255, di mana format memotongnya dengan bersih.

Kernel Sobel-y yang sesuai mendeteksi tepi horizontal dengan memutar pola bobot yang sama 90 derajat:

sobel_y = [-1, -2, -1,
            0,  0,  0,
            1,  2,  1]

Aplikasi yang ingin mendeteksi tepi manapun, tanpa memandang arah, biasanya menjalankan kedua Sobel dan menggabungkan responsnya.

5.16.3. Mengimbangi keluaran

add adalah bagian lain dari cerita penskalaan. Respons kernel berjumlah nol bersifat bertanda -- positif di satu sisi tepi, negatif di sisi lain -- dan bagian negatifnya terpotong ke nol saat ditulis ke piksel tak bertanda. add=128 menggeser respons agar berpusat di abu-abu tengah, sehingga respons negatif bertahan sebagai nilai di bawah 128 dan yang positif berada di atasnya: respons tepi atau emboss menjadi terlihat di kedua arah, dengan biaya setengah rentang di masing-masing.

Kombinasi mul dan add yang diharapkan kernel merupakan bagian dari desain kernel; katalog kernel standar mencantumkan pengaturan yang tepat untuk setiap kernel umum.

5.16.4. Kernel yang lebih besar

Semua yang ada di halaman ini telah dijelaskan dengan kernel 3-kali-3 (size=1), karena itulah ukuran yang digunakan katalog standar dan karena tata letak baris-utama mudah ditulis tangan pada ukuran tersebut. Namun tidak ada dalam mekanismenya yang membatasi kernel pada 3-kali-3. size=2 menjalankan kernel 5-kali-5, dengan dua puluh lima entri dalam daftar datar; size=3 menjalankan 7-kali-7 dengan empat puluh sembilan; dan seterusnya, hingga radius berapa pun yang bersedia dibayar oleh aplikasi. Framework menangani tata letak daftar datar maupun baris-bersarang pada ukuran ganjil berapa pun.

Alasan untuk menggunakan kernel yang lebih besar sama dengan alasan menggunakan tetangga yang lebih besar pada salah satu filter bawaan: lebih banyak perataan, deteksi fitur yang lebih luas, kurang sensitif terhadap noise satu piksel. Biayanya tumbuh sebagai kuadrat dari radius -- 5-kali-5 melakukan sekitar 2,8 kali lebih banyak pekerjaan per piksel dibanding 3-kali-3, 7-kali-7 sekitar 5,4 kali -- dan pengali tersebut langsung mengurangi frame rate.

Pola praktisnya adalah tetap di size=1 untuk katalog standar dan menggunakan ukuran yang lebih besar hanya ketika algoritma membutuhkan tetangga yang lebih besar. Detektor tepi jarang mendapat manfaat di atas 3-kali-3; filter penghalus kadang-kadang ya; ukuran yang tepat bergantung pada skala fitur yang ingin ditekankan atau ditekan oleh aplikasi.

5.16.5. Kapan menggunakan morph

Untuk penghalusan sehari-hari, mean(), gaussian(), dan bilateral() lebih cepat dan lebih bersih. Untuk deteksi tepi, laplacian() dan find_edges() dibuat khusus untuk tujuan tersebut. Kasus untuk langsung menggunakan morph() adalah ketika aplikasi membutuhkan konvolusi tertentu yang tidak diekspos oleh filter bawaan -- Sobel terarah, template tepi kustom, kernel yang disetel ke tekstur tertentu yang akan dicari oleh sisa pipeline, atau salah satu katalog standar kernel berguna yang telah dibangun pemrosesan citra klasik selama beberapa dekade. Fleksibilitas penuh dari kernel arbitrer tersedia; harganya adalah bahwa aplikasi bertanggung jawab untuk memilih nilai kernel yang menghasilkan hasil yang diinginkan.