In the Java 2D™ API an image is typically a rectangular two-dimensional array of pixels, where each pixel represents the color at that position of the image and where the dimensions represent the horizontal extent (width) and vertical extent (height) of the image as it is displayed.
There are bitmap and vector images. Bitmap images (also known as raster images) are made up of pixels in a grid.  Vector images are made up of many individual, scalable objects such as points, lines, curves or polygons. These objects are defined by mathematical equations rather than pixels.

The most important image class for representing such images is the java.awt.image.BufferedImage class. The Java 2D API stores the contents of such images in memory so that they can be directly accessed.
Applications can directly create a BufferedImage object or obtain an image from an external image format such as PNG or GIF.
In either case, the application can then draw on to image by using Java 2D API graphics calls. So, images are not limited to displaying photographic type images. Different objects such as line art, text, and other graphics and even other images can be drawn onto an image (as shown on the following images).


The Java 2D API enables you to apply image filtering operations to BufferedImage and includes several built-in filters. For example, the ConvolveOp filter can be used to blur or sharpen images.The resulting image can then be drawn to a screen, sent to a printer, or saved in a graphics format such as PNG, GIF etc. To learn more about images see the Working with Images lesson.


With the Abstract Window Toolkit (AWT) alone, the only way to display an image is to use the java.awt.Image class. However, this class does not allow you to access the image data directly. In fact, the only methods that directly returned information about the image in java.awt.Image were getHeight() and getWidth(), but even then, there were limitations: If the system had not yet loaded the image data, the values would be erroneous, and you would have to use an instance of java.awt.ImageObserver to be notified when the data became available. If you wanted to manipulate the image data in other ways, you were forced to use the inconvenient producer-consumer model to inspect or manipulate the data as it was decoded from its source.

Buffered Images
The java.awt.image.BufferedImage class, introduced as part of the Java 2D API with the Java Development Kit (JDK) 1.2, affords the programmer much more freedom to directly manipulate the pixels inside an image. Compared to the producer-consumer model, this class uses an immediate-mode imaging model from which you can inspect and modify pixel data stored directly in memory. You can also access image data in a variety of formats and use several types of filtering operations to manipulate the data.
BufferedImage object -- specifically the image inside of it -- has two parts: a ColorModel object and a Raster object that represents the image data. See Figure 1.

Figure 1. The BufferedImage Class
The ColorModel object provides an interpretation of the image's pixel data within a color space. A color space is essentially a collection of all the colors that can be shown on a particular device. Computer monitors, for example, often define their color space using the red-green-blue (RGB) color space. A printer, on the other hand, may use a cyan-magenta-yellow-black (CMYK, using the letter K for "black" rather than B for "blue") color space. Images may use one of several subclasses of ColorModel in the Java 2D API libraries:
  • ComponentColorModel, in which a pixel is represented by several discrete values, typically bytes, each representing one component of color, such as the red component of an RGB representation
  • DirectColorModel, in which all components of a color are packed together in separate bits of the same single pixel value
  • An IndexColorModel, in which each pixel is a single value representing an index into a palette of colors
The Raster object, on the other hand, stores the actual pixel data for an image in a rectangular array addressed by x-axis and y-axis (x and y) coordinates. It also provides a mechanism for creating subimages from its image data buffer. The Raster itself is composed of two parts:
  • data buffer, which contains the raw image data
  • sample model, which describes how the data is organized in the buffer
Raster also provides methods for accessing specific pixels within the image.
Using a BufferedImage Object
To create a BufferedImage object, simply call one of its constructors with the width, height, and an image-type constant.
BufferedImage image =
        new BufferedImage(400, 400, BufferedImage.TYPE_INT_RGB);

For the image-type parameter, use one of the BufferedImage constants shown in Table 1, which specifies how the image data is stored for each of its pixels.
Table 1. BufferedImage Color Models
TYPE_3BYTE_BGR
Blue, green, and red values stored, 1 byte each
TYPE_4BYTE_ABGR
Alpha, blue, green, and red values stored, 1 byte each
TYPE_4BYTE_ABGR_PRE
Alpha and premultiplied blue, green, and red values stored, 1 byte each
TYPE_BYTE_BINARY
1 bit per pixel, 8 pixels to a byte
TYPE_BYTE_INDEXED
8-bit pixel value that references a color index table
TYPE_BYTE_GRAY
8-bit gray value for each pixel
TYPE_USHORT_555_RGB
5-bit red, green, and blue values packed into 16 bits
TYPE_USHORT_565_RGB
5-bit red and blue values, 6-bit green values packed into 16 bits
TYPE_USHORT_GRAY
16-bit gray values for each pixel
TYPE_INT_RGB
8-bit red, green, and blue values stored in a 32-bit integer
TYPE_INT_BGR
8-bit blue, green, and red pixel values stored in a 32-bit integer
TYPE_INT_ARGB
8-bit alpha, red, green, and blue values stored in a 32-bit integer
TYPE_INT_ARGB_PRE
8-bit alpha and premultiplied red, green, and blue values stored in a 32-bit integer


To draw into a BufferedImage, call the createGraphics() method to obtain the Graphics2D object that renders into the BufferedImage, then just call the appropriate rendering methods on theGraphics2D object. Note that you can use all of the Java 2D API rendering features, including those discussed in the first article, when you're rendering to a BufferedImage.
BufferedImage image =
        new BufferedImage(400, 400, BufferedImage.TYPE_INT_RGB);
Graphics2D g2 = (Graphics2D)image.createGraphics();
g2.setFont(new Font("Serif", Font.PLAIN, 36));
g2.drawString("Hello BufferedImage", 50, 50);

Historically, you can create a BufferedImage from a jpeg file using the com.sun.image.codec.jpeg.JPEGImageDecoder class.
String filename = "myGraphic.jpg";
InputStream in = ClipImage.class.getResourceAsStream(filename);
JPEGImageDecoder decoder = JPEGDecoder.createJPEGDecoder(in);
final BufferedImage bufferedImage = decoder.decodeAsBufferedImage();
in.close();

However, if you're looking for a simpler route, you can use the Image I/O libraries in javax.imageio (JSR 15). The javax.imageio.ImageIO class provides a set of static convenience methods that perform most simple Image I/O operations. For example, to read an image that is in a standard format (gifpng, or jpeg), do the following:
File f = new File("c:\images\myimage.gif");
BufferedImage bufferedImage = ImageIO.read(f);

To write it back out, use the write() method of javax.imageio.ImageIO. With this method, you can convert one image type to another. In this case, we converted a gif to a png.
File f = new File("c:\images\myimage.png");
ImageIO.write(bufferedImage, "png", f);

BufferedImage can be rendered using the drawImage()method of any Graphics or Graphics2D objects. For example, you can render a BufferedImage into a Component using the Graphicsobject passed into its paint() method.
public void paint(Graphics g) {
        Graphics2D g2 = (Graphics2D)g;
        g2.drawImage(bufferedImage, 0, 0, null);
}

You may have noticed that there is an unnecessary line in the previous code snippet. Isn't there already a drawImage() method in the base Graphics class? Yes, the drawImage() method invoked above also exists on the base Graphics class, so it is really unnecessary to cast the incoming Graphics object to a Graphics2D. Because the object that is passed in is really a Graphics2D object, the proper Java 2D method will be called.
The easiest way to access specific pixel data of an image is to use the getRGB() and setRGB() methods of the BufferedImage class for the given x and y coordinates:
int rgb = 3096;
int oldRGB = image.getRGB(250, 180);
image.setRGB(250, 180, rgb);

The setRGB() and getRGB() methods accept and return a 32-bit color value in the same format and color space as a non-premultiplied INT_RGB image.
int rgb = image.getRGB(x, y);

int alpha = ((rgb >> 24) & 0xff); 
int red = ((rgb >> 16) & 0xff); 
int green = ((rgb >> 8) & 0xff); 
int blue = ((rgb ) & 0xff); 

// Manipulate the r, g, b, and a values.

rgb = (a << 24) | (r << 16) | (g << 8) | b; 
image.setRGB(x, y, rgb);

You can also directly manipulate the image data of the Raster using its various accessor methods, but you must be familiar with the operation of the ColorModel that it is associated with since you are manipulating the pixel data directly. The lowest-level and potentially most efficient way to access the image data would be to use the methods on the DataBuffer of the Raster. However, that requires knowledge of both the ColorModel and SampleModel in use.
Filtering a BufferedImage Object
Often, a graphics programmer may wish to perform more complex operations on BufferedImage objects than individually manipulating pixel values. The Java 2D API defines several filtering operations for BufferedImage objects that manipulate large amounts of the image at the same time. Each of these image-processing operations is represented by a class that implements the BufferedImageOpinterface. The image manipulation itself is performed in this class's filter() method.
The Java 2D API supports the following implementations of the BufferedImageOp interface:
  • Affine transformation
  • Amplitude scaling
  • Modification of the look-up table
  • Linear combination of bands
  • Color conversion
  • Convolution
Filtering a BufferedImage object using one of the image operation classes is easy. First, construct an instance of one of the BufferedImageOp classes: AffineTransformOpBandCombineOp,ColorConvertOpConvolveOp,
LookupOp, or RescaleOp. Then, call the image operation's filter() method, passing in the BufferedImage object that you want to filter and the BufferedImagewhere you want to store the results.
The following applet, Code Example 1, based on an example in the Java 2D API documentation, illustrates the use of four image-filtering operations:
  • Convolution using a 3x3 blurring filter
  • Convolution using a 3x3 sharpen filter
  • A look-up operation
  • A rescale operation
Code Example 1
import java.awt.*;
import java.io.*;
import java.awt.event.*;
import java.awt.image.*;
import java.awt.geom.*;
import java.awt.font.*;

import javax.swing.*;
import javax.imageio.*;



public class ImageOps extends JApplet {

    private BufferedImage bi[];

    public static final float[] BLUR3x3 = {
        0.1f, 0.1f, 0.1f,
        0.1f, 0.2f, 0.1f,
        0.1f, 0.1f, 0.1f };

    public static final float[] SHARPEN3x3 = {
        0.f,  -1.f,  0.f,
        -1.f,  5.f, -1.f,
        0.f,  -1.f,  0.f};

    public void init() {

        setBackground(Color.white);

        //  Load two images that we can use as examples for the
        //  image operations.

        bi = new BufferedImage[4];
        String s[] = { "bld.jpg", "bld.jpg", "boat.gif", "boat.gif"};

        for ( int i = 0; i < bi.length; i++ ) {
            File f = new File("C:/" + s[i]);
            try {
                
                //  Read in a BufferedImage from a file. 
                BufferedImage bufferedImage = ImageIO.read(f);
                
                //  Convert the image to an RGB style normalized image.
                bi[i] = new BufferedImage(bufferedImage.getWidth(),
                    bufferedImage.getHeight(), BufferedImage.TYPE_INT_RGB);
                bi[i].getGraphics().drawImage(bufferedImage, 0, 0, this);
                
            } catch (IOException e) {
                System.err.println("Error reading file: " + f);
                System.exit(1);
            }
        }
    }



    public void paint(Graphics g) {

        Graphics2D g2 = (Graphics2D) g;
        g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
                            RenderingHints.VALUE_ANTIALIAS_ON);
        g2.setRenderingHint(RenderingHints.KEY_RENDERING,
                            RenderingHints.VALUE_RENDER_QUALITY);
        int w = getSize().width;
        int h = getSize().height;

        //  Set the color to black.

        g2.setColor(Color.black);


        //  Create a low-pass filter and a sharpen filter.

        float[][] data = {BLUR3x3, SHARPEN3x3};

        String theDesc[] = { "Convolve LowPass",
                             "Convolve Sharpen", 
                             "LookupOp",
                             "RescaleOp"};

        //  Cycle through each of the four BufferedImage objects.

        for ( int i = 0; i < bi.length; i++ ) {

            int iw = bi[i].getWidth(this);
            int ih = bi[i].getHeight(this);
            int x = 0, y = 0;

            //  Create a scaled transformation for the image.

            AffineTransform at = new AffineTransform();
            at.scale((w-14)/2.0/iw, (h-34)/2.0/ih);

            BufferedImageOp biop = null;
            BufferedImage bimg =
                new BufferedImage(iw, ih, BufferedImage.TYPE_INT_RGB);


            switch ( i ) {

            //  IMAGE 1 and 2: Create a convolution 
            //  kernel that consists of either the low-pass filter
            //  or the sharpen filter. Set the x and y of the image
            //  so that it appears in the correct quadrant and has
            //  enough room for the descriptive text above.

            case 0 : 
            case 1 : x = i==0?5:w/2+3; y = 15;

                Kernel kernel = new Kernel(3, 3, data[i]);
                ConvolveOp cop = new ConvolveOp(kernel,
                                                ConvolveOp.EDGE_NO_OP,
                                                null);


                //  Apply the convolution operation, placing the 
                //  result in bimg.

                cop.filter(bi[i], bimg);

                //  Create the appropriate AffineTransformation that
                //  will be used while drawing IMAGES 1 and 2

                biop = new AffineTransformOp(at,
                    AffineTransformOp.TYPE_NEAREST_NEIGHBOR);
                break;

            case 2 : x = 5; y = h/2+15;

                //  IMAGE 3:
                //  Create the parameters needed for a LookupOp, which
                //  process the color channels of an image using a
                //  look-up table. This will create a reverse brightness
                //  of the image, similar to a photographic negative.

                byte chlut[] = new byte[256]; 
                for ( int j=0;j<200 ;j++ )
                    chlut[j]=(byte)(256-j); 
                ByteLookupTable blut=new ByteLookupTable(0,chlut); 
                LookupOp lop = new LookupOp(blut, null);
 
                lop.filter(bi[i], bimg);

                //  Create the appropriate AffineTransformation, which 
                //  will be used while drawing the IMAGE 3.
 
                biop = new AffineTransformOp(at,
                    AffineTransformOp.TYPE_BILINEAR);
                break;

            case 3 : x = w/2+3; y = h/2+15;

                //  IMAGE 4:
                //  Perform a rescaling operation, multiplying each 
                //  pixel by a scaling factor (1.1), then adding an
                //  offset (20.0). Note that this has nothing to do
                //  with a geometric scaling of an image.

                RescaleOp rop = new RescaleOp(1.1f,20.0f, null);
                rop.filter(bi[i],bimg);
                biop = new AffineTransformOp(at,

                    AffineTransformOp.TYPE_BILINEAR);
            }

            //  Draw the image with the appropriate AffineTransform
            //  operation, as well as the text above it.

            g2.drawImage(bimg,biop,x,y); 
            TextLayout tl = new TextLayout(theDesc[i], 
                g2.getFont(),g2.getFontRenderContext());
            tl.draw(g2, (float) x, (float) y-4);
        }
    }

    public static void main(String s[]) {
        JFrame f = new JFrame("ImageOps");
        f.addWindowListener(new WindowAdapter() {
            public void windowClosing(WindowEvent e) {System.exit(0);}
        });
        JApplet applet = new ImageOps();
        f.getContentPane().add("Center", applet);
        applet.init();
        f.pack();
        f.setSize(new Dimension(550,550));
        f.setVisible(true);
    }

}


Figure 2. Image Operations
Note that both the blurring and sharpen filter operations are performed by using convolution. Convolution is the process of weighting or averaging the value of each pixel in an image with the values of neighboring pixels. Most spatial-filtering algorithms, including the 3x3 sharpening algorithm shown in Code Example 1, are based on convolution operations.
low-pass filter = blurring Làm mờ ảnh
sharpen filter = làm sắc nét ảnh
LookupOp trong Figure 2 đã chỉnh sửa ảnh bằng cách đảo màu sắc của mỗi pixel ảnh nguồn
Double Buffering
When a graphic is complex or is used repeatedly, you can reduce the time it takes to display it by first rendering the image to an offscreen buffer image and then copying the buffer image to the screen. This technique, called double buffering, is often used for animations. A BufferedImage can easily be used as an offscreen buffer image. To create a BufferedImage whose color space, depth, and pixel layout exactly match the window into which you're drawing, call the Component createImage() or the GraphicsConfiguration.createCompatibleImage() method. If you need control over the offscreen image's type or transparency, you can construct a BufferedImage object directly.
When you're ready to copy the BufferedImage to the screen, call the drawImage() method on your visible component's Graphics object and pass in the BufferedImage.
    public void paint(Graphics g) { 

        g.drawImage(offscreenBuffer, 0, 0, null); 

    }  


Volatile Images
Starting with Java 2 Platform, Standard Edition 1.4, the Java 2D API provides access to hardware acceleration for offscreen images, resulting in better performance when rendering to and copying from these images. However, one problem with hardware-accelerated images is that their contents can be lost at any time, often due to circumstances beyond the application's control. Thejava.awt.image.VolatileImage class helps to correct that by allowing you to create a hardware-accelerated offscreen image and to manage the contents of that image. For example, in many operating systems, a VolatileImage object can be stored in VRAM and can benefit from hardware acceleration.
Note that the memory where the image contents actually reside can be lost or invalidated. Hence, the drawing surface needs to be restored or recreated, and the contents of that surface need to be rerendered. VolatileImage provides
an interface for allowing the user to detect these problems and fix them when they occur.
Code Example 2 shows how to use a VolatileImage object.
Code Example 2
VolatileImage vImg = GraphicsConfiguration.
        createCompatibleVolatileImage(w, h);

    public void paint(Graphics gScreen) {

        do {
            int returnCode = vImg.validate(getGraphicsConfiguration());

            if (returnCode == VolatileImage.IMAGE_RESTORED) {
                // Contents need to be restored.
                reRender();
            } else if (returnCode==VolatileImage.IMAGE_INCOMPATIBLE) {
                vImg = GraphicsConfiguration.
              createCompatibleVolatileImage(w, h);
                reRender();
            }

            gScreen.drawImage(vImg, 0, 0, this);

        } while (vImg.contentsLost());

    }


    public void reRender() {

        Graphics2D g2 = vImg.createGraphics();

        // Miscellaneous rendering commands to restore
        // the image

        g2.dispose();

    }

If you would like more information about using the VolatileImage class, check out Chet Haase's blog for an in-depth question-and-answer session.
Creating a Custom Button
At this point, we can apply what we've learned to create a user interface (UI) button that blurs itself when it is not enabled. The following code demonstrates how to override the BasicButtonUI class to do just that. Note that you can also override the component's paintComponent() method by subclassing a custom JButton class to do the same thing. However, this example also shows how to use the pluggable look and feel of Java Foundation Classes/Swing (JFC/Swing), which is useful if you would like to create more advanced effects, such as displaying semitransparent menus and pop-ups.
First, create a class that overrides the BasicButtonUI class in javax.swing.plaf.basic, which we will call CustomButtonUI. The source code for this class appears in Code Example 3. Note that it reuses the blurring convolution filter from Code Example 1. We want to override two methods: createUI(), which tells Swing to use our custom button UI, and the paint() method, which Swing calls upon to actually render our button.
Code Example 3
public class CustomButtonUI extends BasicButtonUI {

    public static final float[] BLUR3x3 = {
        0.1f, 0.1f, 0.1f,
        0.1f, 0.2f, 0.1f,
        0.1f, 0.1f, 0.1f };
            
    public static ComponentUI createUI(JComponent c) {
        return new CustomButtonUI();
    }
    
    public void paint(Graphics g, JComponent comp) {

        Graphics2D panelG2 = (Graphics2D)g;

        //  Create a buffered image to hold the rendering
        //  of the component that is passed in.

        BufferedImage image = new BufferedImage(
            comp.getWidth(),
            comp.getHeight(),
            BufferedImage.TYPE_INT_ARGB);

        //  Draw the component onto the buffered image.
        Graphics2D g2 = image.createGraphics();
        g2.setColor(g.getColor());
        super.paint(g2, comp);

        
        //  Draw the resulting buffered image onto the current 
        //  Graphics context with the same blurring convolution 
        //  kernel as in Code Example 1.
        
        if (!comp.isEnabled()) {
            Kernel kernel = new Kernel(3, 3, BLUR3x3);
            ConvolveOp cop = new ConvolveOp(kernel,
                                            ConvolveOp.EDGE_NO_OP,
                                            null);
            Image newImage = cop.filter(image, null);
            panelG2.drawImage(newImage, 0, 0, null);
        } else {
            panelG2.drawImage(image, 0, 0, null);
        }

    }
}

Next, use the static UIManager.put() method in your source code to indicate which class should be used for the button's UI.
public class BlurredButton {

    public static void main(String[] args) {

        UIManager.put("ButtonUI", "CustomButtonUI");

        JFrame frame = new JFrame("Button test");
        frame.getContentPane().setBackground(Color.black);
        
        JButton button = new JButton("Test Enabled");
        frame.add(button, BorderLayout.NORTH);

        JButton button2 = new JButton("Test Disabled");
        button2.setEnabled(false);
        frame.add(button2, BorderLayout.SOUTH);
        
        frame.pack();
        frame.setSize(200, 100);
        frame.setVisible(true);
    }
}

Có thể sửa một chút lớp BlurredButton.java như sau:
package java2dtest;

import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

public class BlurredButton {

    public static void main(String[] args) {

        UIManager.put("ButtonUI", "java2dtest.CustomButtonUI");

        JFrame frame = new JFrame("Button test");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.getContentPane().setBackground(Color.black);

        final JButton button = new JButton("Test Enabled");
        frame.add(button, BorderLayout.NORTH);

        final JButton button2 = new JButton("Test Disabled");
        button2.setEnabled(false);
        frame.add(button2, BorderLayout.SOUTH);

        button2.addActionListener(new ActionListener() {

            public void actionPerformed(ActionEvent evt) {
                button2.setEnabled(!button2.isEnabled());
                button.setEnabled(!button2.isEnabled());
            }
        });

        button.addActionListener(new ActionListener() {

            @Override
            public void actionPerformed(ActionEvent e) {
                button.setEnabled(!button.isEnabled());
                button2.setEnabled(!button.isEnabled());
            }
        });

        frame.pack();
        frame.setSize(200, 100);
        frame.setVisible(true);
    }
}

Figure 3 shows the result. Button đã bị làm mờ - blurring.

Figure 3. The Blurring of a Disabled Button



Summary
This article helped you learn a little about how the Java 2D API works with images using the BufferedImage class. You learned how the BufferedImage class stores images and how to manipulate images at both the pixel level and using filter operations. You also learned how to use the VolatileImage class to take advantage of hardware acceleration. Finally, we discussed how to use these classes in a custom Swing component. In the next article in this series, we will discuss how the Java 2D APIs manipulate and render text.

Convolution Từ điển Anh-Việt dịch là sự quấn lại/cuộn lại, is the process of weighting or averaging the value of each pixel in an image with the values of neighboring pixels. Most spatial-filtering algorithms, including the 3x3 sharpening algorithm shown in Code Example 1, are based on convolution operations.
ConvolveOp Implements: BufferedImageOp, RasterOp. Uses a Kernel to perform a convolution on the source image. A convolution is a spatial operation where the pixels surrounding the input pixel are multiplied by a kernel value to generate the value of the output pixel. The Kernel mathematically defines the relationship between the pixels in the immediate neighborhood of the input pixel and the output pixel.
AffineTransformOp Implements: BufferedImageOp, RasterOp. A class that defines an affine transform to perform a linear mapping from 2D coordinates in a source Image or Raster to 2D coordinates in the destination image or Raster. This class can perform either bilinear or nearest neighbor affine transform operations.
LookupOp Implements of BufferedImageOp, RasterOp. Performs a lookup operation from the source to the destination. For Rasters, the lookup operates on sample values. For BufferedImages, the lookup operates on color and alpha components. The filter method that is later invoked on an object of the LookupOp class to modify the image uses a color value from a pixel as an ordinal index into a lookup table.  It replaces the color value in the pixel with the value stored in the lookup table at that index.  Thus, you can modify the color values in the pixels using just about any substitution algorithm that you can devise.
RescaleOp Implements BufferedImageOp, RasterOp. Performs a pixel-by-pixel rescaling of the data in the source image by multiplying each pixel value by a scale factor and then adding an offset.
RasterOp Defines single-input/single-output operations performed on Raster objects. Implemented by AffineTransformOp, BandCombineOp, ColorConvertOp, ConvolveOp, LookupOp, and RescaleOp
BufferedImageOp Describes single-input/single-output operations performed on BufferedImage objects. Implemented by AffineTransformOp, ColorConvertOp, ConvolveOp, LookupOp, and RescaleOp.
volatile Bay hơi, hay fix lỗi mất dữ liệu ảnh.
<< Signed left shift (dịch trái số học)
>> Signed right shift (dịch phải số học)
>>> Unsigned right shift (dịch phải logic)
discrete Rời rạc
blurring Làm mờ nhạt
interpolation nội suy