
Perbaikan Deskewing Image – Grafika komputer yang sebelumnya pernah kita bahas yaitu Croping Image dengan Mouse Event yang masih ada pekerjaan utama untuk menganhandle objek/gambar yang terkena skewing. Skewing pernah juga kita bahas Corner Detection (python) serta sedikit menyinggung penerapannya dengan OpenCV Membuat Scanner Document Corner Detection OpenCV Java. Pada 2 judul postingan tersebut yaitu mencari 4 corner secara otomatis yaitu A, B, C, dan D sebagai titik referensi operasi deskewing.
Namun pada postingan yang kita bahas nanti, kita akan mengandalkan user untuk menentukan sendiri point A, B, C, dan D menggunakan mouse setelah sebelumnya dibahas pada grafika komputer untuk Croping Image dengan Mouse Event.
Tulisan ini dibagi menjadi beberapa serial yaitu
- Belajar OpenCV bagian 1 dan 2 Setting OpenCV di Java
- Grafika Komputer untuk Resize Object Secara Dinamis
- Croping Image dengan Mouse Event
Pada postingan tersebut akan kita perbaiki dengan drawPolyline()
dan bagaimana teknik yang kita akan kerjakan agar drawPolyline()
bersifat closed path secara otomatis. Fokus kita yaitu pada class Button yang telah kita buat pada Croping Image dengan Mouse Event dengan operasi drawPolyline()
Yuk kita mulai bahas mengenai drawPolyline()
yang merupakan operasi grafik vektor pada Java untuk membuat multi-line. Secara garis besar argument input pada function tersebut berupa array [] integer x, y. Misalkan saja kita akan membuat sebuah 3 garis yang saling terhubung dengan aturan koordinat layar x dan y sebagai berikut
int [] x = new int []{100,200,200}; int [] y = new int []{100,100,50}; g.drawPolyline(x, y,x.length);
Pada contoh diatas, polyline masih bersifat open path, agar membentuk closed path, kita harus menambahkan point ke 3 sama dengan posisi ke 0 (ingat bahwa array dimulai dari index 0) menjadi bentuk segitiga!
int [] x = new int []{100,200,200,100}; int [] y = new int []{100,100,50,100}; g.drawPolyline(x, y,x.length);
Sehingga dapat disimpulkan bahwa untuk membuat closed path maka posisi point x,y pertama dan yang paling akhir harus sama! Misalkan saja kita akan buat rectangle, maka butuh 5 point
g.setColor(Color.white); int [] x = new int []{100,200,200,100}; int [] y = new int []{100,100,50,100}; g.drawPolyline(x, y,x.length); x = new int[]{100,200,200,50,50,100}; y = new int[]{300,200,200,90,200,300}; g.drawPolyline(x, y,x.length);
Ok, sekarang sudah paham ya, yuk sekarang kita buat event mouse ketika moved maka akan diikuti oleh sebuah oval vektor
class Button2 extends JButton{ Image IMAGE; File FILE; Dimension DIMENSION; Point CURRENT; public Button2(){ super(); setSize(100,100); setText("Button"); FILE = new File("D:/corner_gambar3.jpg"); DIMENSION = new Dimension(100,100);//sebagai awalan saja this.addMouseListener(new MouseAdapter(){ @Override public void mousePressed(MouseEvent e){ } }); this.addMouseMotionListener(new MouseAdapter() { @Override public void mouseMoved(MouseEvent e){ CURRENT = new Point(e.getX(),e.getY()); repaint(); } }); } @Override public void paint(Graphics g){ IMAGE = new ImageIcon(FILE.getAbsolutePath()).getImage(); g.drawImage(IMAGE, 0, 0, DIMENSION.width,DIMENSION.height, null); int diameter = 10; if(CURRENT!=null){ g.setColor(Color.white); g.fillOval(CURRENT.x-diameter/2,CURRENT.y-diameter/2,diameter,diameter); } } };
Jadi setiap mouse digerakan, maka akan ada oval putih yang mengikuti, berikut wadah untuk menampung Button tersebut pada JFrame pada class Demo2
public class Demo2 { public static void main(String[] args) { // TODO code application logic here JFrame frame = new JFrame(); Button2 button = new Button2(); frame.setSize(new Dimension(500,500)); frame.add(button); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.setLocationRelativeTo(null); frame.addComponentListener(new ComponentAdapter(){ @Override public void componentResized(ComponentEvent evt) { Dimension actualSize = frame.getContentPane().getSize(); button.DIMENSION = actualSize; } }); frame.setVisible(true); } }
Sekarang kita tingkatkan lagi yaitu kita akan menerapkan polyline, untuk hal tersebut kita butuh arraylist untuk menampung setiap mouse clicked. Jadi akan dihitung! jika jumlah klik sudah mencapai 4 kali klik! maka tambahkan 1 lokasi terakhir sama dengan lokasi yang pertama. Perhatikan kode berikut jadi ketika ada klik mouse akan ditambahkan lokasi point nya tapi bila sudah 4 kali klik hentikan dulu tidak boleh nambah
@Override public void mousePressed(MouseEvent e){ if(e.getButton()==MouseEvent.BUTTON3){ //klik kanan point = new ArrayList(); repaint(); return; } //jangan nambah lagi kalau point sudah cukup if(point.size()>3){ System.out.println("sudah 4 titik"); return; } //setiap kali klik akan tambah point point.add(new Point(e.getX(),e.getY())); repaint(); }
kemudian untuk menggambar terlebih dahulu akan check array list point bila tidak null, maka buat array x,y. Ketika sudah 4 kali klik! maka buat 1 point untuk ditambahkan diakhir dengan lokasi yang sama dengan pertama
if(point!=null){ int [] x = new int [point.size()]; int [] y = new int [point.size()]; for(int i=0;i<point.size();i++){ x[i] = (int) point.get(i).getX(); y[i] = (int) point.get(i).getY(); g.setColor(Color.RED); g.fillOval(x[i]-diameter/2,y[i]-diameter/2,diameter,diameter); } g.setColor(Color.RED); g.drawPolyline(x, y,x.length); } //jika sudah 4 kali, maka //point ke 5 harus sama dengan point 1 biar closed if(point.size()==4){ //maka otomatis point.add(point.get(0)); }
Yuk kita lihat kode lengkapnya class Button2 berikut
class Button2 extends JButton{ Image IMAGE; File FILE; Dimension DIMENSION; Point CURRENT; ArrayList <Point> point; public Button2(){ super(); setSize(100,100); setText("Button"); FILE = new File("D:/corner_gambar3.jpg"); DIMENSION = new Dimension(100,100);//sebagai awalan saja point = new ArrayList(); this.addMouseListener(new MouseAdapter(){ @Override public void mousePressed(MouseEvent e){ if(e.getButton()==MouseEvent.BUTTON3){ //klik kanan point = new ArrayList(); repaint(); return; } //jangan nambah lagi kalau point sudah cukup if(point.size()>3){ System.out.println("sudah 4 titik"); return; } //setiap kali klik akan tambah point point.add(new Point(e.getX(),e.getY())); repaint(); } }); this.addMouseMotionListener(new MouseAdapter() { @Override public void mouseMoved(MouseEvent e){ CURRENT = new Point(e.getX(),e.getY()); repaint(); } }); } @Override public void paint(Graphics g){ IMAGE = new ImageIcon(FILE.getAbsolutePath()).getImage(); g.drawImage(IMAGE, 0, 0, DIMENSION.width,DIMENSION.height, null); int diameter = 10; if(CURRENT!=null){ g.setColor(Color.white); g.fillOval(CURRENT.x-diameter/2,CURRENT.y-diameter/2,diameter,diameter); } if(point!=null){ int [] x = new int [point.size()]; int [] y = new int [point.size()]; for(int i=0;i<point.size();i++){ x[i] = (int) point.get(i).getX(); y[i] = (int) point.get(i).getY(); g.setColor(Color.RED); g.fillOval(x[i]-diameter/2,y[i]-diameter/2,diameter,diameter); } g.setColor(Color.RED); g.drawPolyline(x, y,x.length); } //jika sudah 4 kali, maka //point ke 5 harus sama dengan point 1 biar closed if(point.size()==4){ //maka otomatis point.add(point.get(0)); } } };
ketika di run pada ketika setiap kali diklik akan membuat polyline secara otomatis
Yup pada tahap ini kalian sudah membuat corner detection secara manual sebagai dasar operasi deskewing, yuk kita tambahkan kode berikut agar tampil lokasi nya ketika klik kanan mouse
public void mousePressed(MouseEvent e){ if(e.getButton()==MouseEvent.BUTTON3){ for(int i = 0;i<point.size();i++){ System.out.println("lokasi : "+point.get(i)); } //klik kanan point = new ArrayList(); repaint(); return; } //jangan nambah lagi kalau point sudah cukup if(point.size()>3){ System.out.println("sudah 4 titik"); return; } //setiap kali klik akan tambah point point.add(new Point(e.getX(),e.getY())); repaint(); }
hasilnya kalian bisa melihat ada 5 point (dimana point 1 dan 5 mempunyai letak yang sama)
lokasi : java.awt.Point[x=105,y=144] lokasi : java.awt.Point[x=319,y=135] lokasi : java.awt.Point[x=371,y=361] lokasi : java.awt.Point[x=134,y=394] lokasi : java.awt.Point[x=105,y=144]
Tapi saya punya aturan tersendiri untuk mempermudah membuat polygon akan closed path yaitu dimulai dari searah jarum jam sebagai gambaran berikut yaitu searah jarum jam
Karena kalau tidak seperti itu nanti malah bikin tanda silang
Deskewing menggunakan OpenCV
Sekarang kita lupakan diatas dulu, mari kita bahas mengenai operasi deskewing yang telah dibahas https://softscients.com/2022/02/16/membuat-scanner-document-corner-detection-opencv-java/ namun kita permudah dengan cara meminta user untuk meng input sendiri lokasi corner nya – Memperbaiki Deskewing perlu kita pahami mengenai lokasi A, B, C, dan D sebagai titik perspektif
- A: atas kiri
- B: atas kanan
- C: bawah kiri
- D: bawah kiri
yang diproyeksikan ulang terhadap A’, B’, C’, dan D’
Pada kasus diatas sebelumnya didapatkan informasi berikut (searah jarum jam)
lokasi : java.awt.Point[x=105,y=144] lokasi : java.awt.Point[x=319,y=135] lokasi : java.awt.Point[x=371,y=361] lokasi : java.awt.Point[x=134,y=394] lokasi : java.awt.Point[x=105,y=144]
atau bila saya tulis lagi sebagai berikut
- A: lokasi : java.awt.Point[x=105,y=144]
- B: lokasi : java.awt.Point[x=319,y=135]
- C: lokasi : java.awt.Point[x=371,y=361]
- D: lokasi : java.awt.Point[x=134,y=394]
- E: lokasi : java.awt.Point[x=105,y=144]
kemudian untuk mengikuti aturan opencv kita akan ubah urutannya agar mengikuti kaidah yaitu mengubah lokasi C ditukar ke D
- A: lokasi : java.awt.Point[x=105,y=144]
- B: lokasi : java.awt.Point[x=319,y=135]
- C: lokasi : java.awt.Point[x=134,y=394] (tadinya lokasi D)
- D: lokasi : java.awt.Point[x=371,y=361] (tadinya lokasi C)
sedangkan lokasi E tidak perlu digunakan! perhatikan kode berikut untuk melakukan operasi deskewing gambar menggunakan OpenCV
//jika point nya sudah ada A,B,C, dan D maka bisa buat simpan gambar! if(point.size()==4){ Imgcodecs imageCodecs = new Imgcodecs(); //perspektif openCV org.opencv.core.Point[] pointSorted = new org.opencv.core.Point[4]; pointSorted[0] = new org.opencv.core.Point(point.get(0).getX(),point.get(0).getY()); //A pointSorted[1] = new org.opencv.core.Point(point.get(1).getX(),point.get(1).getY()); //B pointSorted[2] = new org.opencv.core.Point(point.get(3).getX(),point.get(3).getY()); //C penukaran lokasi pointSorted[3] = new org.opencv.core.Point(point.get(2).getX(),point.get(2).getY()); //D penukaran lokasi MatOfPoint2f pointSrc = new MatOfPoint2f( pointSorted[0], //atas kiri pointSorted[1], //atas kanan pointSorted[2], //bawah kiri pointSorted[3]); //bawah kanan double lebarObjekBagianAtas = pointSorted[1].x - pointSorted[0].x; double lebarObjekBagianBawah = pointSorted[3].x - pointSorted[2].x; //nanti dipilih yang lebar paling besar int lebar = 0; if(lebarObjekBagianAtas>lebarObjekBagianBawah){ lebar = (int)lebarObjekBagianAtas; }else{ lebar = (int)lebarObjekBagianBawah; } double tinggiObjekBagianKiri = pointSorted[2].y - pointSorted[0].y; double tinggiObjekBagianKanan = pointSorted[3].y - pointSorted[1].y; //pilih tinggi yang paling besar int tinggi = 0; if(tinggiObjekBagianKiri>tinggiObjekBagianKanan){ tinggi = (int) tinggiObjekBagianKiri; }else{ tinggi = (int) tinggiObjekBagianKanan; } int margin = 5; MatOfPoint2f pointDst = new MatOfPoint2f( new org.opencv.core.Point(margin, margin), //atas kiri new org.opencv.core.Point(lebar,margin), //atas kanan new org.opencv.core.Point(margin,tinggi-margin), // bawah kiri new org.opencv.core.Point(lebar-margin,tinggi-margin)// bawah kanan ); Mat matWrap = Imgproc.getPerspectiveTransform(pointSrc,pointDst); Mat matDeskewing = new Mat(); //baca file gambar Mat matSrc = Imgcodecs.imread(FILE.getAbsolutePath(),1); int tinggi_semula = matSrc.height(); int lebar_semula = matSrc.width(); //lakukan resize agar sama dengan ukuran window saat ini //berguna untuk menyesuaikan posisi corner Imgproc.resize(matSrc, matSrc, new Size(DIMENSION.width,DIMENSION.height),Imgproc.INTER_LINEAR); //lakukan deskewing Imgproc.warpPerspective(matSrc, matDeskewing, matWrap, new Size(lebar+margin,tinggi+margin)); //ubah lagi ke ukuran semula Imgproc.resize(matDeskewing, matDeskewing, new Size(lebar_semula,tinggi_semula)); //gambar akan disimpan imageCodecs.imwrite("D:/out.jpg",matDeskewing); }
Kode lengkap Perbaikan Deskewing Image bisa kalian pelajari dibawah ini
import java.awt.Color; import java.awt.Dimension; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Image; import java.awt.Point; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.ComponentAdapter; import java.awt.event.ComponentEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.geom.AffineTransform; import java.awt.image.AffineTransformOp; import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.logging.Level; import java.util.logging.Logger; import javax.imageio.ImageIO; import javax.swing.ImageIcon; import javax.swing.JButton; import javax.swing.JFileChooser; import javax.swing.JFrame; import javax.swing.JMenu; import javax.swing.JMenuBar; import javax.swing.JMenuItem; import org.opencv.core.Core; import org.opencv.core.Mat; import org.opencv.core.MatOfPoint2f; import org.opencv.core.Rect; import org.opencv.core.Size; import org.opencv.imgcodecs.Imgcodecs; import org.opencv.imgproc.Imgproc; /** * * @author User */ public class Demo2 { public static void main(String[] args) { // TODO code application logic here System.loadLibrary(Core.NATIVE_LIBRARY_NAME); JFrame frame = new JFrame(); Button2 button = new Button2(); frame.setSize(new Dimension(500,500)); frame.add(button); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.setLocationRelativeTo(null); frame.addComponentListener(new ComponentAdapter(){ @Override public void componentResized(ComponentEvent evt) { Dimension actualSize = frame.getContentPane().getSize(); button.DIMENSION = actualSize; } }); frame.setVisible(true); } } class Button2 extends JButton{ Image IMAGE; File FILE; Dimension DIMENSION; Point CURRENT; ArrayList <Point> point; public Button2(){ super(); setSize(100,100); setText("Button"); FILE = new File("D:\\Data Lama\\Project Java\\Deskewing dan Mouse Event\\corner_gambar3.jpg"); DIMENSION = new Dimension(100,100);//sebagai awalan saja point = new ArrayList(); this.addMouseListener(new MouseAdapter(){ @Override public void mousePressed(MouseEvent e){ if(e.getButton()==MouseEvent.BUTTON3){ //artinya klik kanan! for(int i = 0;i<point.size();i++){ System.out.println("lokasi : "+point.get(i)); } //jika point nya sudah ada A,B,C, dan D maka bisa buat simpan gambar! if(point.size()==4){ Imgcodecs imageCodecs = new Imgcodecs(); //perspektif openCV org.opencv.core.Point[] pointSorted = new org.opencv.core.Point[4]; pointSorted[0] = new org.opencv.core.Point(point.get(0).getX(),point.get(0).getY()); //A pointSorted[1] = new org.opencv.core.Point(point.get(1).getX(),point.get(1).getY()); //B pointSorted[2] = new org.opencv.core.Point(point.get(3).getX(),point.get(3).getY()); //C penukaran lokasi pointSorted[3] = new org.opencv.core.Point(point.get(2).getX(),point.get(2).getY()); //D penukaran lokasi MatOfPoint2f pointSrc = new MatOfPoint2f( pointSorted[0], //atas kiri pointSorted[1], //atas kanan pointSorted[2], //bawah kiri pointSorted[3]); //bawah kanan double lebarObjekBagianAtas = pointSorted[1].x - pointSorted[0].x; double lebarObjekBagianBawah = pointSorted[3].x - pointSorted[2].x; //nanti dipilih yang lebar paling besar int lebar = 0; if(lebarObjekBagianAtas>lebarObjekBagianBawah){ lebar = (int)lebarObjekBagianAtas; }else{ lebar = (int)lebarObjekBagianBawah; } double tinggiObjekBagianKiri = pointSorted[2].y - pointSorted[0].y; double tinggiObjekBagianKanan = pointSorted[3].y - pointSorted[1].y; //pilih tinggi yang paling besar int tinggi = 0; if(tinggiObjekBagianKiri>tinggiObjekBagianKanan){ tinggi = (int) tinggiObjekBagianKiri; }else{ tinggi = (int) tinggiObjekBagianKanan; } int margin = 5; MatOfPoint2f pointDst = new MatOfPoint2f( new org.opencv.core.Point(margin, margin), //atas kiri new org.opencv.core.Point(lebar,margin), //atas kanan new org.opencv.core.Point(margin,tinggi-margin), // bawah kiri new org.opencv.core.Point(lebar-margin,tinggi-margin)// bawah kanan ); Mat matWrap = Imgproc.getPerspectiveTransform(pointSrc,pointDst); Mat matDeskewing = new Mat(); //baca file gambar Mat matSrc = Imgcodecs.imread(FILE.getAbsolutePath(),1); int tinggi_semula = matSrc.height(); int lebar_semula = matSrc.width(); //lakukan resize agar sama dengan ukuran window saat ini //berguna untuk menyesuaikan posisi corner Imgproc.resize(matSrc, matSrc, new Size(DIMENSION.width,DIMENSION.height),Imgproc.INTER_LINEAR); //lakukan deskewing Imgproc.warpPerspective(matSrc, matDeskewing, matWrap, new Size(lebar+margin,tinggi+margin)); //ubah lagi ke ukuran semula Imgproc.resize(matDeskewing, matDeskewing, new Size(lebar_semula,tinggi_semula)); //gambar akan disimpan imageCodecs.imwrite("D:/out.jpg",matDeskewing); } //untuk menghapus titik yang ada! //mulai baru point = new ArrayList(); repaint(); return; } //jangan nambah lagi kalau point sudah cukup if(point.size()>3){ System.out.println("sudah 4 titik"); return; } //setiap kali klik akan tambah point point.add(new Point(e.getX(),e.getY())); repaint(); } }); this.addMouseMotionListener(new MouseAdapter() { @Override public void mouseMoved(MouseEvent e){ CURRENT = new Point(e.getX(),e.getY()); repaint(); } }); } @Override public void paint(Graphics g){ IMAGE = new ImageIcon(FILE.getAbsolutePath()).getImage(); g.drawImage(IMAGE, 0, 0, DIMENSION.width,DIMENSION.height, null); int diameter = 10; if(CURRENT!=null){ g.setColor(Color.white); g.fillOval(CURRENT.x-diameter/2,CURRENT.y-diameter/2,diameter,diameter); } if(point!=null){ int [] x = new int [point.size()]; int [] y = new int [point.size()]; for(int i=0;i<point.size();i++){ x[i] = (int) point.get(i).getX(); y[i] = (int) point.get(i).getY(); g.setColor(Color.RED); g.fillOval(x[i]-diameter/2,y[i]-diameter/2,diameter,diameter); } g.setColor(Color.RED); g.drawPolyline(x, y,x.length); } //jika sudah 4 kali, maka //point ke 5 harus sama dengan point 1 biar closed if(point.size()==4){ //maka otomatis point.add(point.get(0)); } } };