Trong phần này chúng ta sẽ làm việc với ảnh.
Xử lý ảnh là một lĩnh vực khó (ít nhất là đối với mình), mặc dù Java cung cấp rất nhiều các hàm API cấp cao để đơn giản hóa việc xử lý nhưng trong phạm vi bài này mình chỉ đề cập đến một số thao tác xử lý ảnh đơn giản.
Lớp BufferedImage
là lớp chuyên để làm việc với ảnh, lớp này lưu một mảng 2 chiều chứa thông tin của từng pixel trong ảnh.
Load ảnh
import java.awt.Dimension; import java.awt.EventQueue; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Image; import javax.swing.ImageIcon; import javax.swing.JFrame; import javax.swing.JPanel; class Surface extends JPanel { private Image mshi; public Surface() { loadImage(); setSurfaceSize(); } private void loadImage() { mshi = new ImageIcon("mushrooms.jpg").getImage(); } private void setSurfaceSize() { Dimension d = new Dimension(); d.width = mshi.getWidth(null); d.height = mshi.getHeight(null); setPreferredSize(d); } private void doDrawing(Graphics g) { Graphics2D g2d = (Graphics2D) g; g2d.drawImage(mshi, 0, 0, null); } @Override public void paintComponent(Graphics g) { super.paintComponent(g); doDrawing(g); } } public class DisplayImageEx extends JFrame { public DisplayImageEx() { initUI(); } private void initUI() { add(new Surface()); pack(); setTitle("Mushrooms"); setLocationRelativeTo(null); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); } public static void main(String[] args) { EventQueue.invokeLater(new Runnable() { @Override public void run() { DisplayImageEx ex = new DisplayImageEx(); ex.setVisible(true); } }); } }
Chúng ta load ảnh và chỉnh kích thước của cửa sổ bằng với kích thước của ảnh.
private void loadImage() { mshi = new ImageIcon("mushrooms.jpg").getImage(); }
Để load ảnh thì chúng ta dùng lớp ImageIcon
rồi dùng phương thức getImage()
để lấy về đối tượng Image
.
private void setSurfaceSize() { Dimension d = new Dimension(); d.width = mshi.getWidth(null); d.height = mshi.getHeight(null); setPreferredSize(d); }
Trong phương thức setSurfaceSize()
, chúng ta lấy kích thước của ảnh rồi dùng phương thức setPreferredSize()
để thiết lập kích thước cửa sổ.
private void doDrawing(Graphics g) { Graphics2D g2d = (Graphics2D) g; g2d.drawImage(mshi, 0, 0, null); }
Để vẽ ảnh lên JPanel
thì chúng ta dùng phương thức drawImage()
. Tham số thứ 4 trong phương thức là một một đối tượng ImageObserver,
đối tượng này thực hiện các thao tác đồng bộ các thay đổi của ảnh trước khi được vẽ lên màn hình, tuy nhiên ở đây chúng ta chưa cần đến nên để null
.
private void initUI() { ... pack(); ... }
Phương thức pack()
thay đổi kích thước của cửa sổ để phù hợp với kích thước của JPanel
.
Tạo ảnh đen trắng
Trong đồ họa máy tính thì ảnh đen trắng được biểu diễn bởi một kênh màu (khác với ảnh màu có 3 kênh là Red Green Blue) cùng với giá trị alpha để biểu diễn mức độ trong suốt của điểm ảnh.
Trong ví dụ dưới đây, chúng ta sẽ tạo ảnh đen trắng từ một ảnh có sẵn.
import java.awt.Dimension; import java.awt.EventQueue; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Image; import java.awt.image.BufferedImage; import javax.swing.ImageIcon; import javax.swing.JFrame; import javax.swing.JPanel; class Surface extends JPanel { private Image mshi; private BufferedImage bufimg; private Dimension d; public Surface() { loadImage(); determineAndSetSize(); createGrayImage(); } private void determineAndSetSize() { d = new Dimension(); d.width = mshi.getWidth(null); d.height = mshi.getHeight(null); setPreferredSize(d); } private void createGrayImage() { bufimg = new BufferedImage(d.width, d.height, BufferedImage.TYPE_BYTE_GRAY); Graphics2D g2d = bufimg.createGraphics(); g2d.drawImage(mshi, 0, 0, null); g2d.dispose(); } private void loadImage() { mshi = new ImageIcon("mushrooms.jpg").getImage(); } private void doDrawing(Graphics g) { Graphics2D g2d = (Graphics2D) g; g2d.drawImage(bufimg, null, 0, 0); } @Override public void paintComponent(Graphics g) { super.paintComponent(g); doDrawing(g); } } public class GrayScaleImageEx extends JFrame { public GrayScaleImageEx() { initUI(); } private void initUI() { Surface dpnl = new Surface(); add(dpnl); pack(); setTitle("GrayScale image"); setLocationRelativeTo(null); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); } public static void main(String[] args) { EventQueue.invokeLater(new Runnable() { @Override public void run() { GrayScaleImageEx ex = new GrayScaleImageEx(); ex.setVisible(true); } }); } }
Trong Java có rất nhiều cách để tạo một ảnh đen trắng. Trong ví dụ này thì chúng ta dùng thuộc tính BufferedImage.TYPE_BYTE_GRAY
.
bufimg = new BufferedImage(d.width, d.height, BufferedImage.TYPE_BYTE_GRAY);
Đầu tiên chúng ta tạo một đối tượng BufferedImage
với thuộc tính BufferedImage.TYPE_BYTE_GRAY
.
Graphics2D g2d = bufimg.createGraphics(); g2d.drawImage(mshi, 0, 0, null);
Tiếp theo chúng ta tạo một đối tượng Graphics2D
rồi gọi phương thức drawImage()
để vẽ ảnh vào BufferedImage
.
private void doDrawing(Graphics g) { Graphics2D g2d = (Graphics2D) g; g2d.drawImage(bufimg, null, 0, 0); }
Bên trong phương thức doDrawing()
chúng ta vẽ lại BufferedImage
lên JPanel
.
Lật ảnh
Trong ví dụ dưới đây, chúng ta sẽ thực hiện thao tác lật ảnh.
import java.awt.Dimension; import java.awt.EventQueue; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Image; import java.awt.geom.AffineTransform; import java.awt.image.AffineTransformOp; import java.awt.image.BufferedImage; import javax.swing.ImageIcon; import javax.swing.JFrame; import javax.swing.JPanel; class Surface extends JPanel { private Image mshi; private BufferedImage bufimg; private final int SPACE = 10; public Surface() { loadImage(); createFlippedImage(); setSurfaceSize(); } private void loadImage() { mshi = new ImageIcon("mushrooms.jpg").getImage(); } private void createFlippedImage() { bufimg = new BufferedImage(mshi.getWidth(null), mshi.getHeight(null), BufferedImage.TYPE_INT_RGB); Graphics gb = bufimg.getGraphics(); gb.drawImage(mshi, 0, 0, null); gb.dispose(); AffineTransform tx = AffineTransform.getScaleInstance(-1, 1); tx.translate(-mshi.getWidth(null), 0); AffineTransformOp op = new AffineTransformOp(tx, AffineTransformOp.TYPE_NEAREST_NEIGHBOR); bufimg = op.filter(bufimg, null); } private void setSurfaceSize() { int w = bufimg.getWidth(); int h = bufimg.getHeight(); Dimension d = new Dimension(3*SPACE+2*w, h+2*SPACE); setPreferredSize(d); } private void doDrawing(Graphics g) { Graphics2D g2d = (Graphics2D) g; g2d.drawImage(mshi, SPACE, SPACE, null); g2d.drawImage(bufimg, null, 2*SPACE + bufimg.getWidth(), SPACE); } @Override public void paintComponent(Graphics g) { super.paintComponent(g); doDrawing(g); } } public class FlippedImageEx extends JFrame { public FlippedImageEx() { initUI(); } private void initUI() { add(new Surface()); pack(); setTitle("Flipped image"); setLocationRelativeTo(null); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); } public static void main(String[] args) { EventQueue.invokeLater(new Runnable() { @Override public void run() { FlippedImageEx ex = new FlippedImageEx(); ex.setVisible(true); } }); } }
Chúng ta sẽ lật ảnh theo chiều ngang.
AffineTransform tx = AffineTransform.getScaleInstance(-1, 1); tx.translate(-castle.getWidth(null), 0);
Thao tác lật ảnh gồm có 2 bước, đầu tiên chúng ta scale nó theo chiều ngược lại tức là tỉ lệ -1 sau đó tịnh tiến tâm vẽ lùi về bên trái với khoảng cách bằng với kích thước của ảnh.
AffineTransformOp op = new AffineTransformOp(tx, AffineTransformOp.TYPE_NEAREST_NEIGHBOR); bufferedImage = op.filter(bufferedImage, null)
Sau khi đã định nghĩa thao tác lật ảnh trong đối tượng AffineTransform
, chúng ta copy từng pixel trong ảnh vào một BufferedImage,
trong Java có sẵn lớp AffineTransformOp
cho phép chúng ta làm điều này dễ dàng với phương thức filter()
.
private void doDrawing(Graphics g) { Graphics2D g2d = (Graphics2D) g; g2d.drawImage(mshi, SPACE, SPACE, null); g2d.drawImage(bufimg, null, 2*SPACE + bufimg.getWidth(), SPACE); }
Chúng ta vẽ cả ảnh gốc và ảnh đã được lật vào panel.
private void setSurfaceSize() { int w = bufimg.getWidth(); int h = bufimg.getHeight(); Dimension d = new Dimension(3*SPACE+2*w, h+2*SPACE); setPreferredSize(d); }
Chúng ta thiết lập kích thước panel dựa theo kích thước ảnh, kích thước của panel sẽ có chiều ngang bằng 2 lần kích thước ảnh vì chúng ta vẽ cả ảnh gốc và ảnh đã được lật, đồng thời chiều ngang sẽ rộng thêm vài pixel vì chúng ta muốn có một chút khoảng trống giữa 2 ảnh và viền panel.
Làm mờ ảnh
Trong ví dụ dưới đây chúng ta sẽ làm mờ một ảnh, ảnh mờ thường được thấy khi bạn cầm máy ảnh mà bị rung tay, hay trong các hiệu ứng tốc độ…
Trong đồ họa máy tính thì việc làm mờ một ảnh được thực hiện bằng cách thay thế từng pixel trên ảnh bằng trung bình cộng của các pixel xung quanh nó (từ 3 đến 8 điểm). Có rất nhiều kiểu làm mờ một ảnh, ở đây mình chỉ demo một kiểu đơn giản, các kiểu khác sẽ được đề cập trong một tương lai không ai biết 🙂
import java.awt.Dimension; import java.awt.EventQueue; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.image.BufferedImage; import java.awt.image.BufferedImageOp; import java.awt.image.ConvolveOp; import java.awt.image.Kernel; import java.io.File; import java.io.IOException; import java.util.logging.Level; import java.util.logging.Logger; import javax.imageio.ImageIO; import javax.swing.JFrame; import javax.swing.JPanel; class Surface extends JPanel { private BufferedImage mshi; private BufferedImage bluri; public Surface() { loadImage(); createBlurredImage(); setSurfaceSize(); } private void loadImage() { try { mshi = ImageIO.read(new File("mushrooms.jpg")); } catch (IOException ex) { Logger.getLogger(Surface.class.getName()).log( Level.WARNING, null, ex); } } private void createBlurredImage() { float[] blurKernel = { 1 / 9f, 1 / 9f, 1 / 9f, 1 / 9f, 1 / 9f, 1 / 9f, 1 / 9f, 1 / 9f, 1 / 9f }; BufferedImageOp blur = new ConvolveOp(new Kernel(3, 3, blurKernel)); bluri = blur.filter(mshi, new BufferedImage(mshi.getWidth(), mshi.getHeight(), mshi.getType())); } private void setSurfaceSize() { Dimension d = new Dimension(); d.width = mshi.getWidth(null); d.height = mshi.getHeight(null); setPreferredSize(d); } private void doDrawing(Graphics g) { Graphics2D g2d = (Graphics2D) g; g2d.drawImage(bluri, null, 0, 0); } @Override public void paintComponent(Graphics g) { super.paintComponent(g); doDrawing(g); } } public class BlurredImageEx extends JFrame { public BlurredImageEx() { setTitle("Blurred image"); add(new Surface()); pack(); setLocationRelativeTo(null); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); } public static void main(String[] args) { EventQueue.invokeLater(new Runnable() { @Override public void run() { BlurredImageEx ex = new BlurredImageEx(); ex.setVisible(true); } }); } }
Chúng ta load ảnh từ đĩa cứng sau đó làm mờ nó rồi hiển thị lên màn hình.
float[] blurKernel = { 1 / 9f, 1 / 9f, 1 / 9f, 1 / 9f, 1 / 9f, 1 / 9f, 1 / 9f, 1 / 9f, 1 / 9f };
Mảng blurKernel
được dùng cho việc tính toán các pixel.
BufferedImageOp blur = new ConvolveOp(new Kernel(3, 3, blurKernel)); bluri = blur.filter(mshi, new BufferedImage(mshi.getWidth(), mshi.getHeight(), mshi.getType()));
Ảnh sẽ được làm mờ bằng lớp ConvolveOp
.
Tạo ảnh phản chiếu
Trong ví dụ dưới đây chúng ta sẽ tạo hiệu ứng phản chiếu cho ảnh giống như hình ảnh phản chiếu của hồ nước.
import java.awt.AlphaComposite; import java.awt.Color; import java.awt.Dimension; import java.awt.EventQueue; import java.awt.GradientPaint; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.image.BufferedImage; import java.io.File; import java.util.logging.Level; import java.util.logging.Logger; import javax.imageio.ImageIO; import javax.swing.JFrame; import javax.swing.JPanel; class Surface extends JPanel { private BufferedImage image; private BufferedImage refImage; private int img_w; private int img_h; private final int SPACE = 30; public Surface() { loadImage(); getImageSize(); createReflectedImage(); } private void loadImage() { try { image = ImageIO.read(new File("rotunda.jpg")); } catch (Exception ex) { Logger.getLogger(Surface.class.getName()).log( Level.WARNING, null, ex); } } private void getImageSize() { img_w = image.getWidth(); img_h = image.getHeight(); } private void createReflectedImage() { float opacity = 0.4f; float fadeHeight = 0.3f; refImage = new BufferedImage(img_w, img_h, BufferedImage.TYPE_INT_ARGB); Graphics2D rg = refImage.createGraphics(); rg.drawImage(image, 0, 0, null); rg.setComposite(AlphaComposite.getInstance(AlphaComposite.DST_IN)); rg.setPaint(new GradientPaint(0, img_h * fadeHeight, new Color(0.0f, 0.0f, 0.0f, 0.0f), 0, img_h, new Color(0.0f, 0.0f, 0.0f, opacity))); rg.fillRect(0, 0, img_w, img_h); rg.dispose(); } private void doDrawing(Graphics g) { Graphics2D g2d = (Graphics2D) g.create(); int win_w = getWidth(); int win_h = getHeight(); int gap = 20; g2d.setPaint(new GradientPaint(0, 0, Color.black, 0, win_h, Color.darkGray)); g2d.fillRect(0, 0, win_w, win_h); g2d.translate((win_w - img_w) / 2, win_h / 2 - img_h); g2d.drawImage(image, 0, 0, null); g2d.translate(0, 2 * img_h + gap); g2d.scale(1, -1); g2d.drawImage(refImage, 0, 0, null); g2d.dispose(); } @Override public void paintComponent(Graphics g) { super.paintComponent(g); doDrawing(g); } @Override public Dimension getPreferredSize() { return new Dimension(img_w + 2 * SPACE, 2 * img_h + 3 * SPACE); } } public class ReflectionEx extends JFrame { public ReflectionEx() { initUI(); } private void initUI() { add(new Surface()); pack(); setTitle("Reflection"); setLocationRelativeTo(null); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); } public static void main(String[] args) { EventQueue.invokeLater(new Runnable() { @Override public void run() { ReflectionEx ex = new ReflectionEx(); ex.setVisible(true); } }); } }
Chúng ta load ảnh rồi tạo một ảnh khác làm phản chiếu từ ảnh gốc.
refImage = new BufferedImage(img_w, img_h, BufferedImage.TYPE_INT_ARGB); Graphics2D rg = refImage.createGraphics(); rg.drawImage(image, 0, 0, null);
Đầu tiên chúng ta tạo đối tượng BufferedImage
dùng làm ảnh phản chiếu.
rg.setComposite(AlphaComposite.getInstance(AlphaComposite.DST_IN)); rg.setPaint(new GradientPaint(0, img_h * fadeHeight, new Color(0.0f, 0.0f, 0.0f, 0.0f), 0, img_h, new Color(0.0f, 0.0f, 0.0f, opacity))); rg.fillRect(0, 0, img_w, img_h);
Tiếp theo chúng ta tạo phần mờ bằng lớp GradientPaint
, lớp này được dùng để vẽ các dải màu nhưng ở đây chúng ta vẽ ảnh nên phần màu không được hiển thị, tham số opacity
biểu diễn giá trị alpha sẽ làm cho ảnh mờ dần dần.
g2d.setPaint(new GradientPaint(0, 0, Color.black, 0, win_h, Color.darkGray)); g2d.fillRect(0, 0, win_w, win_h);
Phần màu nền của cửa sổ cũng được vẽ bằng gradient từ màu đen dần chuyển sang màu xám từ trên xuống.
g2d.translate(0, 2 * imageHeight + gap); g2d.scale(1, -1);
Phần ảnh được làm mờ sẽ được lật lại theo chiều dọc và được vẽ bên dưới ảnh gốc.
@Override public Dimension getPreferredSize() { return new Dimension(img_w + 2 * SPACE, 2 * img_h + 3 * SPACE); }
Khác với các ví dụ trên, ở đây chúng ta chỉnh kích thước cửa sổ bằng cách override lại phương thức getPreferredSize()
.