Pemrosesan gambar dengan Java 2D

Pengolahan citra adalah seni dan ilmu memanipulasi citra digital. Ia berdiri dengan satu kaki kokoh dalam matematika dan yang lainnya dalam estetika, dan merupakan komponen penting dari sistem komputer grafis. Jika Anda pernah repot membuat gambar Anda sendiri untuk halaman Web, Anda pasti akan menghargai pentingnya kemampuan manipulasi gambar Photoshop untuk membersihkan pindaian dan membersihkan gambar yang kurang optimal.

Jika Anda melakukan pekerjaan pemrosesan gambar apa pun di JDK 1.0 atau 1.1, Anda mungkin ingat bahwa itu agak membosankan. Model lama produsen dan konsumen data citra sulit untuk diproses. Sebelum JDK 1.2, pemrosesan gambar melibatkan MemoryImageSources, PixelGrabbers, dan arcana sejenis lainnya. Java 2D, bagaimanapun, menyediakan model yang lebih bersih dan lebih mudah digunakan.

Bulan ini, kami akan memeriksa algoritme di balik beberapa operasi pemrosesan gambar ( ops ) penting dan menunjukkan kepada Anda bagaimana mereka dapat diimplementasikan menggunakan Java 2D. Kami juga akan menunjukkan kepada Anda bagaimana operasi ini digunakan untuk memengaruhi tampilan gambar.

Karena pemrosesan gambar adalah aplikasi mandiri yang benar-benar berguna dari Java 2D, kami telah membuat contoh bulan ini, ImageDicer, agar dapat digunakan kembali sebanyak mungkin untuk aplikasi Anda sendiri. Contoh tunggal ini menunjukkan semua teknik pemrosesan gambar yang akan kita bahas di kolom bulan ini.

Perhatikan bahwa sesaat sebelum artikel ini dipublikasikan, Sun merilis kit pengembangan Java 1.2 Beta 4. Beta 4 tampaknya memberikan kinerja yang lebih baik untuk contoh operasi pemrosesan gambar kami, tetapi juga menambahkan beberapa bug baru yang melibatkan pemeriksaan batas ConvolveOps. Masalah ini mempengaruhi deteksi tepi dan contoh penajaman yang kita gunakan dalam diskusi kita.

Kami pikir contoh ini berharga, jadi daripada menghilangkannya sama sekali, kami mengkompromikan: untuk memastikannya berjalan, kode contoh mencerminkan perubahan Beta 4, tetapi kami telah mempertahankan angka dari eksekusi 1.2 Beta 3 sehingga Anda dapat melihat operasinya bekerja dengan benar.

Mudah-mudahan, Sun akan mengatasi bug ini sebelum rilis final Java 1.2.

Pemrosesan gambar bukanlah ilmu roket

Pemrosesan gambar tidak harus sulit. Sebenarnya, konsep dasarnya sangat sederhana. Sebuah gambar, bagaimanapun, hanyalah sebuah persegi panjang dari piksel berwarna. Memproses gambar hanyalah masalah menghitung warna baru untuk setiap piksel. Warna baru setiap piksel dapat didasarkan pada warna piksel yang ada, warna piksel sekitarnya, parameter lain, atau kombinasi dari elemen-elemen ini.

API 2D memperkenalkan model pemrosesan gambar langsung untuk membantu pengembang memanipulasi piksel gambar ini. Model ini didasarkan pada java.awt.image.BufferedImagekelas, dan operasi pemrosesan gambar seperti konvolusi dan thresholding diwakili oleh implementasi java.awt.image.BufferedImageOpantarmuka.

Implementasi operasi ini relatif mudah. Misalkan, misalnya, Anda sudah memiliki gambar sumber sebagai a yang BufferedImagedipanggil source. Melakukan operasi yang diilustrasikan pada gambar di atas hanya membutuhkan beberapa baris kode:

001 pendek [] ambang = pendek baru [256]; 002 untuk (int i = 0; i <256; i ++) 003 threshold [i] = (i <128)? (pendek) 0: (pendek) 255; 004 BufferedImageOp thresholdOp = 005 LookupOp baru (ShortLookupTable baru (0, threshold), null); 006 BufferedImage destination = thresholdOp.filter (sumber, null);

Hanya itu yang ada di sana. Sekarang mari kita lihat langkah-langkahnya lebih detail:

  1. Instantiate operasi gambar pilihan Anda (baris 004 dan 005). Di sini kami menggunakan a LookupOp, yang merupakan salah satu operasi gambar yang termasuk dalam implementasi Java 2D. Seperti operasi gambar lainnya, ini mengimplementasikan BufferedImageOpantarmuka. Kami akan membicarakan lebih lanjut tentang operasi ini nanti.

  2. Panggil metode operasi filter()dengan gambar sumber (baris 006). Sumber diproses dan gambar tujuan dikembalikan.

Jika Anda sudah membuat BufferedImageyang akan menampung gambar tujuan, Anda dapat meneruskannya sebagai parameter kedua ke filter(). Jika Anda lulus null, seperti yang kami lakukan pada contoh di atas, tujuan baru BufferedImagedibuat.

API 2D mencakup beberapa operasi gambar bawaan ini. Kami akan membahas tiga di kolom ini: konvolusi, tabel pencarian, dan ambang batas. Silakan merujuk ke dokumentasi Java 2D untuk informasi tentang operasi lainnya yang tersedia di API 2D (Sumber Daya).

Lilitan

Sebuah konvolusi operasi memungkinkan Anda untuk menggabungkan warna dari pixel sumber dan tetangganya untuk menentukan warna dari pixel tujuan. Kombinasi ini ditentukan menggunakan kernel, operator linier yang menentukan proporsi setiap warna piksel sumber yang digunakan untuk menghitung warna piksel tujuan.

Pikirkan kernel sebagai template yang dilapisi pada gambar untuk melakukan konvolusi pada satu piksel dalam satu waktu. Karena setiap piksel berbelit-belit, templat dipindahkan ke piksel berikutnya dalam gambar sumber dan proses konvolusi diulangi. Salinan sumber gambar digunakan untuk nilai input untuk konvolusi, dan semua nilai output disimpan ke dalam salinan tujuan gambar. Setelah operasi konvolusi selesai, gambar tujuan dikembalikan.

Bagian tengah kernel dapat dianggap sebagai overlay piksel sumber yang berbelit-belit. Misalnya, operasi konvolusi yang menggunakan kernel berikut tidak berpengaruh pada gambar: setiap piksel tujuan memiliki warna yang sama dengan piksel sumber yang sesuai.

 0,0 0,0 0,0 0,0 1,0 0,0 0,0 0,0 0,0 

Aturan utama untuk membuat kernel adalah bahwa semua elemen harus berjumlah 1 jika Anda ingin mempertahankan kecerahan gambar.

Dalam API 2D, konvolusi diwakili oleh a java.awt.image.ConvolveOp. Anda dapat membuat sebuah ConvolveOpkernel, yang diwakili oleh sebuah instance java.awt.image.Kernel. Kode berikut membangun ConvolveOpmenggunakan kernel yang disajikan di atas.

001 float [] identityKernel = {002 0.0f, 0.0f, 0.0f, 003 0.0f, 1.0f, 0.0f, 004 0.0f, 0.0f, 0.0f 005}; 006 BufferedImageOp identity = 007 ConvolveOp baru (Kernel baru (3, 3, identityKernel));

Operasi konvolusi berguna dalam melakukan beberapa operasi umum pada gambar, yang akan kami detailkan sebentar lagi. Kernel yang berbeda menghasilkan hasil yang sangat berbeda.

Sekarang kami siap untuk mengilustrasikan beberapa kernel pemrosesan gambar dan efeknya. Gambar kami yang tidak dimodifikasi adalah Lady Agnew dari Lochnaw, dilukis oleh John Singer Sargent pada tahun 1892 dan 1893.

Kode berikut membuat ConvolveOpyang menggabungkan jumlah yang sama dari setiap piksel sumber dan tetangganya. Teknik ini menghasilkan efek kabur.

001 float kesembilan = 1.0f / 9.0f; 002 float [] blurKernel = {003 kesembilan, kesembilan, kesembilan, 004 kesembilan, kesembilan, kesembilan, 005 kesembilan, kesembilan, kesembilan 006}; 007 BufferedImageOp blur = ConvolveOp baru (Kernel baru (3, 3, blurKernel));

Kernel konvolusi umum lainnya menekankan tepi pada gambar. Operasi ini biasa disebut deteksi tepi. Tidak seperti kernel lain yang disajikan di sini, koefisien kernel ini tidak berjumlah 1.

001 float [] edgeKernel = {002 0.0f, -1.0f, 0.0f, 003 -1.0f, 4.0f, -1.0f, 004 0.0f, -1.0f, 0.0f 005}; 006 BufferedImageOp edge = ConvolveOp baru (Kernel baru (3, 3, edgeKernel));

Anda dapat melihat apa yang dilakukan kernel ini dengan melihat koefisien di kernel (baris 002-004). Pikirkan sejenak tentang bagaimana kernel deteksi tepi digunakan untuk beroperasi di area yang seluruhnya satu warna. Setiap piksel akan berakhir tanpa warna (hitam) karena warna piksel di sekitarnya membatalkan warna piksel sumber. Piksel cerah yang dikelilingi oleh piksel gelap akan tetap cerah.

Perhatikan seberapa gelap gambar yang diproses dibandingkan dengan aslinya. Ini terjadi karena elemen kernel deteksi tepi tidak berjumlah 1.

A simple variation on edge detection is the sharpening kernel. In this case, the source image is added into an edge detection kernel as follows:

 0.0 -1.0 0.0 0.0 0.0 0.0 0.0 -1.0 0.0 -1.0 4.0 -1.0 + 0.0 1.0 0.0 = -1.0 5.0 -1.0 0.0 -1.0 0.0 0.0 0.0 0.0 0.0 -1.0 0.0 

The sharpening kernel is actually only one possible kernel that sharpens images.

The choice of a 3 x 3 kernel is somewhat arbitrary. You can define kernels of any size, and presumably they don't even have to be square. In JDK 1.2 Beta 3 and 4, however, a non-square kernel produced an application crash, and a 5 x 5 kernel chewed up the image data in a most peculiar way. Unless you have a compelling reason to stray from 3 x 3 kernels, we don't recommend it.

You may also be wondering what happens at the edge of the image. As you know, the convolution operation takes a source pixel's neighbors into account, but source pixels at the edges of the image don't have neighbors on one side. The ConvolveOp class includes constants that specify what the behavior should be at the edges. The EDGE_ZERO_FILL constant specifies that the edges of the destination image are set to 0. The EDGE_NO_OP constant specifies that source pixels along the edge of the image are copied to the destination without being modified. If you don't specify an edge behavior when constructing a ConvolveOp, EDGE_ZERO_FILL is used.

The following example shows how you could create a sharpening operator that uses the EDGE_NO_OP rule (NO_OP is passed as a ConvolveOp parameter in line 008):

001 float[] sharpKernel = { 002 0.0f, -1.0f, 0.0f, 003 -1.0f, 5.0f, -1.0f, 004 0.0f, -1.0f, 0.0f 005 }; 006 BufferedImageOp sharpen = new ConvolveOp( 007 new Kernel(3, 3, sharpKernel), 008 ConvolveOp.EDGE_NO_OP, null); 

Lookup tables

Another versatile image operation involves using a lookup table. For this operation, source pixel colors are translated into destination pixels colors through the use of a table. A color, remember, is composed of red, green, and blue components. Each component has a value from 0 to 255. Three tables with 256 entries are sufficient to translate any source color to a destination color.

The java.awt.image.LookupOp and java.awt.image.LookupTable classes encapsulate this operation. You can define separate tables for each color component, or use one table for all three. Let's look at a simple example that inverts the colors of every component. All we need to do is create an array that represents the table (lines 001-003). Then we create a LookupTable from the array and a LookupOp from the LookupTable (lines 004-005).

001 short[] invert = new short[256]; 002 for (int i = 0; i < 256; i++) 003 invert[i] = (short)(255 - i); 004 BufferedImageOp invertOp = new LookupOp( 005 new ShortLookupTable(0, invert), null); 

LookupTable has two subclasses, ByteLookupTable and ShortLookupTable, that encapsulate byte and short arrays. If you create a LookupTable that doesn't have an entry for any input value, an exception will be thrown.

This operation creates an effect that looks like a color negative in conventional film. Also note that applying this operation twice will restore the original image; you're basically taking a negative of the negative.

What if you only wanted to affect one of the color components? Easy. You construct a LookupTable with separate tables for each of the red, green, and blue components. The following example shows how to create a LookupOp that only inverts the blue component of the color. As with the previous inversion operator, applying this operator twice restores the original image.

001 short[] invert = new short[256]; 002 short[] straight = new short[256]; 003 for (int i = 0; i < 256; i++) { 004 invert[i] = (short)(255 - i); 005 straight[i] = (short)i; 006 } 007 short[][] blueInvert = new short[][] { straight, straight, invert }; 008 BufferedImageOp blueInvertOp = 009 new LookupOp(new ShortLookupTable(0, blueInvert), null); 

Posterizing is another nice effect you can apply using a LookupOp. Posterizing involves reducing the number of colors used to display an image.

A LookupOp can achieve this effect by using a table that maps input values to a small set of output values. The following example shows how input values can be mapped to eight specific values.

001 pendek [] posterize = pendek baru [256]; 002 untuk (int i = 0; i <256; i ++) 003 posterize [i] = (short) (i - (i% 32)); 004 BufferedImageOp posterizeOp = 005 LookupOp baru (ShortLookupTable baru (0, posterize), null);

Thresholding

Operasi gambar terakhir yang akan kita periksa adalah thresholding. Thresholding membuat perubahan warna melintasi "batas" yang ditentukan programmer, atau ambang batas, lebih jelas (mirip dengan bagaimana garis kontur pada peta membuat batas ketinggian lebih jelas). Teknik ini menggunakan nilai ambang batas, nilai minimum, dan nilai maksimum yang ditentukan untuk mengontrol nilai komponen warna untuk setiap piksel gambar. Nilai warna di bawah ambang batas diberi nilai minimum. Nilai di atas ambang diberi nilai maksimum.