1. Introduction
In this tutorial, we’ll go over how to rescale an image using basic Java APIs. We’ll show how to load and save the image from/to a file and explain some technical aspects of the rescaling process.
2. Loading an Image in Java
For this tutorial, we’ll use a simple JPG image file. We’ll load it using the ImageIO API bundled with the basic Java SDK. This API has a few preset ImageReaders for formats such as JPEG and PNG. ImageReaders know how to read their respective image formats and get a bitmap from an image file.
The method we’ll be using is the read method from ImageIO. There are a few overloads of this method, but we’ll go with the simplest one:
BufferedImage srcImg = ImageIO.read(new File("src/main/resources/images/sampleImage.jpg"));
As we can see, the read() method provides a BufferedImage object, which is the primary Java representation of an image bitmap.
3. Rescaling the Image
Before we rescale the loaded image, we have to do some preparation.
3.1. Creating a Destination Image
First, we have to create a new BufferedImage object representing the scaled image in memory, also called the destination image. Since we’re looking to rescale, it means that the resulting image will have a different width and height from the original.
We must set the scaled sizes in the new BufferedImage:
float scaleW = 2.0f, scaleH = 2.0f;
int w = srcImg.getWidth() * (int) scaleW;
int h = srcImg.getHeight() * (int) scaleH;
BufferedImage dstImg = new BufferedImage(w, h, srcImg.getType());
As shown in the code, the scaling factor doesn’t need to be the same for width and height. However, they usually are since using different scaling factors gives us distorted results.
The BufferedImage constructor also requires an imageType parameter. This is not to be confused with the image file format (e.g., PNG or JPEG); the image type dictates the color space of the new BufferedImage. The class itself provides static int members for supported values such as BufferedImage.TYPE_INT_RGB and BufferedImage.TYPE_BYTE_GRAY for color and gray images, respectively. In our case, we’ll use the same type as the source image since we’re only changing the scale.
The next step is to apply a transformation that will bring the source image to our target size.
3.2. Applying an AffineTransform
We’ll scale the image by applying a scaling affine transformation. These linear transformations can map points from one 2D plane to another. Depending on the transformation, the destination plane can be a scaled-up and even rotated version of the original plane.
In our case, we’ll only apply scaling. The simplest way to think of this is to take all the points that make up the image and increase the distance between them by scaling the factor.
Let’s create an AffineTransform and its respective operation:
AffineTransform scalingTransform = new AffineTransform();
scalingTransform.scale(scaleW, scaleH);
AffineTransformOp scaleOp = new AffineTransformOp(scalingTransform, AffineTransformOp.TYPE_BILINEAR);
The AffineTransform defines what operation we’ll apply, while the AffineTransformOp defines how it’ll be applied. We create an operation that will use a scalingTransform and apply it using a bilinear interpolation.
The selected interpolation algorithm is determined on a case-by-case basis and dictates how the pixel values of the new image will be chosen. What these interpolation algorithms do and why they’re mandatory are beyond the scope of this article. Understanding them requires knowing why we use these linear transformations and how they apply to 2D images.
Once the scaleOp is ready, we can apply it to the srcImg and put the result in the dstImg:
dstImg = scaleOp.filter(srcImg, dstImg);
Finally, we can save the dstImg to a file so we can view the results:
ImageIO.write(dstImg, "jpg", new File("src/main/resources/images/resized.jpg"));
4. Conclusion
In this article, we learned how to scale an image by an arbitrary scale factor. We showed how to load/save images from/to the file system and how to use Java’s AffineTransform to apply the scaling operation.