Java 2D and resize - java

I have some old Java 2D code I want to reuse, but was wondering, is this the best way to get the highest quality images?
public static BufferedImage getScaled(BufferedImage imgSrc, Dimension dim) {
// This code ensures that all the pixels in the image are loaded.
Image scaled = imgSrc.getScaledInstance(
dim.width, dim.height, Image.SCALE_SMOOTH);
// This code ensures that all the pixels in the image are loaded.
Image temp = new ImageIcon(scaled).getImage();
// Create the buffered image.
BufferedImage bufferedImage = new BufferedImage(temp.getWidth(null),
temp.getHeight(null), BufferedImage.TYPE_INT_RGB);
// Copy image to buffered image.
Graphics g = bufferedImage.createGraphics();
// Clear background and paint the image.
g.setColor(Color.white);
g.fillRect(0, 0, temp.getWidth(null),temp.getHeight(null));
g.drawImage(temp, 0, 0, null);
g.dispose();
// j2d's image scaling quality is rather poor, especially when
// scaling down an image to a much smaller size. We'll post filter
// our images using a trick found at
// http://blogs.cocoondev.org/mpo/archives/003584.html
// to increase the perceived quality....
float origArea = imgSrc.getWidth() * imgSrc.getHeight();
float newArea = dim.width * dim.height;
if (newArea <= (origArea / 2.)) {
bufferedImage = blurImg(bufferedImage);
}
return bufferedImage;
}
public static BufferedImage blurImg(BufferedImage src) {
// soften factor - increase to increase blur strength
float softenFactor = 0.010f;
// convolution kernel (blur)
float[] softenArray = {
0, softenFactor, 0,
softenFactor, 1-(softenFactor*4), softenFactor,
0, softenFactor, 0};
Kernel kernel = new Kernel(3, 3, softenArray);
ConvolveOp cOp = new ConvolveOp(kernel, ConvolveOp.EDGE_NO_OP, null);
return cOp.filter(src, null);
}

Chris Campbell has an excellent and detailed write-up on scaling images - see this article.
Chet Haase and Romain Guy also have a detailed and very informative write-up of image scaling in their book, Filthy Rich Clients.

Adding some clarifying information here.
No, that isn't the best way to get a good looking scaled image in Java. Use of getScaledInstance and the underlying AreaAveragingScaleFilter are deprecated by the Java2D team in favor of some more advanced methods.
If you are just trying to get a good-looking thumbnail, using Chris Campbell's method as suggested by David is the way to go. For what it's worth, I have implemented that algorithm along with 2 other faster methods in a Java image-scaling library called imgscalr (Apache 2 license). The point of the library was to specifically address this question in a highly tuned library that is easy to use:
BufferedImage thumbnail = Scalr.resize(srcImg, 150);
To get the best-looking scaled instance possible in Java, the method call would look something like this:
BufferedImage scaledImg = Scalr.resize(img, Method.QUALITY,
150, 100, Scalr.OP_ANTIALIAS);
The library will scale the original image using the incremental-scaling approach recommended by the Java2D team and then to make it look even nicer a very mild convolveop is applied to the image, effectively anti-aliasing it slightly. This is really nice for small thumbnails, not so important for huge images.
If you haven't worked with convolveops before, it's a LOT of work just to get the perfect looking kernel for the op to look good in all use-cases. The OP constant defined on the Scalr class is the result of a week of collaboration with a social networking site in Brazil that had rolled out imgscalr to process profile pictures for it's members. We went back and forth and tried something like 10 different kernels until we found one that was subtle enough not to make the image look soft or fuzzy but still smooth out the transitions between pixel values so the image didn't look "sharp" and noisey at small sizes.
If you want the best looking scaled image regardless of speed, go with Juha's suggestion of using the java-image-scaling library. It is a very comprehensive collection of Java2D Ops and includes support for the Lanczsos algorithm which will give you the best-looking result.
I would stay away from JAI, not because it's bad, but because it is just a different/broader tool than what you are trying to solve. Any of the previous 3 approaches mentioned will give you great looking thumbnails without needing to add a whole new imaging platform to your project in fewer lines of code.

You can use JAI (Java Advanced Imaging) to get more sophisticated image resizing options. See https://jai.dev.java.net/. These allow you much more flexibility than the java.awt.image package.

You could also look into java-image-scaling library.

You can Resize Image using a Open Source Library
enter link description here
I have done with Large Image to Small and result excellent, keeping the aspect ratio fine.
import java.awt.image.BufferedImage;
import java.io.File;
import javax.imageio.ImageIO;
import javax.swing.JOptionPane;
import org.imgscalr.*;
import org.imgscalr.Scalr.Method;
public class ImageScaller {
static String SRC_FILES_PATH = "I:\\BigGreen\\";
static String IMAGE_FILE_PATH = "I:\\Resized\\";
public ImageScaller() {
}
public static void main(String[] args) {
// TODO Auto-generated method stub
try
{
ResizeLoad(SRC_FILES_PATH);
}
catch(Exception ex)
{
System.out.println(ex.toString());
}
}
public static int ResizeLoad(String path)
{
String file;
File folder ;
File[] listOfFiles = null;
listOfFiles = null;
try
{
folder = new File(path);
listOfFiles = folder.listFiles();
for (int i = 0; i < listOfFiles.length; i++)
{
if (listOfFiles[i].isFile())
{
file = listOfFiles[i].getName();
ScalledImageWrite(listOfFiles[i].getPath(),file);
//System.out.println(file);
}
}
System.out.println("All Resized");
}
catch (Exception e)
{
JOptionPane.showMessageDialog(null,e.toString(),"Resize & Load :Exception",JOptionPane.WARNING_MESSAGE);
}
return listOfFiles.length;
}
private static File ScalledImageWrite(String path,String fileName)
{
try
{
BufferedImage img = ImageIO.read(new File(path));
BufferedImage scaledImg = Scalr.resize(img, Method.AUTOMATIC, 24, 24);
File destFile = new File(IMAGE_FILE_PATH + fileName);
ImageIO.write(scaledImg, "png", destFile);
//System.out.println("Done resizing");
return destFile;
}
catch (Exception e)
{
JOptionPane.showMessageDialog(null,e.toString(),"Scalled Images Write: Exception",JOptionPane.WARNING_MESSAGE);
return null;
}
}
}
Here is the output in pictorial format of this code.

Related

Glide load into SimpleTarget<Bitmap> not honoring the specified width and height

I'm using Glide to load an image, resize it and save it to a file by means of a SimpleTarget<Bitmap>. These images will be uploaded to Amazon S3, but that's besides the point. I'm resizing the images prior to uploading as to save as much user's bandwidth as possible. For my app needs a 1024 pixel wide image is more than enough, so I'm using the following code to accomplish that:
final String to = getMyImageUrl();
final Context appCtx = context.getApplicationContext();
Glide.with(appCtx)
.load(sourceImageUri)
.asBitmap()
.into(new SimpleTarget<Bitmap>(1024, 768) {
#Override
public void onResourceReady(Bitmap resource, GlideAnimation<? super Bitmap> glideAnimation) {
try {
FileOutputStream out = new FileOutputStream(to);
resource.compress(Bitmap.CompressFormat.JPEG, 70, out);
out.flush();
out.close();
MediaScannerConnection.scanFile(appCtx, new String[]{to}, null, null);
} catch (IOException e) {
e.printStackTrace();
}
}
});
It works almost perfectly, but the size of the resulting image is not 1024 pixels wide. Testing it with a source image with dimensions of 4160 x 2340 pixels the dimensions of the resulting saved image is 2080 x 1170 pixels.
I've tried playing with the width and height parameters passed to new SimpleTarget<Bitmap>(350, 350) and with these parameters the resulting image dimensions are 1040 x 585 pixels.
I really don't know what to do to make Glide respect the passed dimensions. In fact I'd like to resize the image proportionally so that the bigger dimension (either width or height) will be restricted to 1024 pixels and the smaller one resized accordingly (I believe I'll have to find a way to get the original image dimensions and then pass the width and height to SimpleTarget, but to do that I need Glide to respect the passed width and height!).
Does anyone have a clue what's going on? I'm using Glide 3.7.0.
Since this question itself may be useful for people trying to use Glide to resize and save images I believe it is in everyone's interest to provide my actual "solution" which relies on a new SimpleTargetimplementation that automatically saves the resized image:
import android.graphics.Bitmap;
import com.bumptech.glide.request.animation.GlideAnimation;
import com.bumptech.glide.request.target.SimpleTarget;
import java.io.FileOutputStream;
import java.io.IOException;
public class FileTarget extends SimpleTarget<Bitmap> {
public FileTarget(String fileName, int width, int height) {
this(fileName, width, height, Bitmap.CompressFormat.JPEG, 70);
}
public FileTarget(String fileName, int width, int height, Bitmap.CompressFormat format, int quality) {
super(width, height);
this.fileName = fileName;
this.format = format;
this.quality = quality;
}
String fileName;
Bitmap.CompressFormat format;
int quality;
public void onResourceReady(Bitmap bitmap, GlideAnimation anim) {
try {
FileOutputStream out = new FileOutputStream(fileName);
bitmap.compress(format, quality, out);
out.flush();
out.close();
onFileSaved();
} catch (IOException e) {
e.printStackTrace();
onSaveException(e);
}
}
public void onFileSaved() {
// do nothing, should be overriden (optional)
}
public void onSaveException(Exception e) {
// do nothing, should be overriden (optional)
}
}
Using it is as simple as:
Glide.with(appCtx)
.load(sourceImageUri)
.asBitmap()
.into(new FileTarget(to, 1024, 768) {
#Override
public void onFileSaved() {
// do anything, or omit this override if you want
}
});
After a good night's sleep I just figured it out! I had stumbled on an issue in Glide's github page which had the answer but I didn't realize it: I missed something in the explanation, which I now fully understood after resting for 10 hours. You should never underestimate the power of sleeping! But I digress. Here is the answer found on Glide's Github issue tracker:
Sizing the image usually has two phases:
Decoding/Downsampler read image from stream with inSampleSize
Transforming/BitmapTransformation take the Bitmap and match the exact
target size
The decoding is always needed and is included in the flow,
the default case is to match the target size with the "at least"
downsampler, so when it comes to the transformation the image can be
downsized more without quality loss (each pixel in the source will
match at least 1.0 pixels and at most ~1.999 pixels) this can be
controlled by asBitmap().at least|atMost|asIs|decoder(with
downsampler)
The transformation and target size is automatic by default, but only
when using a ViewTarget. When you load into an ImageView the size of
that will be detected even when it has match_parent. Also if there's
no explicit transformation there'll be one applied from scaleType.
Thus results in a pixel perfect Bitmap for that image, meaning 1 pixel
in Bitmap = 1 pixel on screen resulting in the best possible quality
with the best memory usage and fast rendering (because there's no
pixel mapping needed when drawing the image).
With a SimpleTarget you take on these responsibilities by providing a
size on the constructor or via override() or implementing getSize if
the sizing info is async-ly available only.
To fix your load add a transformation: .fitCenter|centerCrop(), your
current applied transformation is .dontTransform()
(Answer by Róbert Papp)
I got confused by this answer because of this:
With a SimpleTarget you take on these responsibilities by providing a
size on the constructor or via override() or implementing getSize if
the sizing info is async-ly available only.
Since I was passing the size I thought I got this covered already and such size should have been respected. I missed this important concept:
Decoding/Downsampler read image from stream with inSampleSize
Transforming/BitmapTransformation take the Bitmap and match the exact
target size
And this:
To fix your load add a transformation: .fitCenter|centerCrop(), your
current applied transformation is .dontTransform()
Now that I pieced it together it makes sense. Glide was only downsampling the image (first step in the sizing image flow as explained by Róbert) which gives an image with approximated dimensions. Let me say that Glide is very smart in this aspect. By using the downsampling method prior to resizing it avoids working with unnecessarily large bitmaps in memory and improves the resizing quality because downsampling to the exact size would compromise too many "important" pixels!
Since I didn't have any transformation applied to this loading pipeline, the sizing flow stopped in this first step (downsampling), and the resulting image had only approximate dimensions to my expected target size.
To solve it I just applied a .fitCenter() transform as shown bellow:
Glide.with(appCtx)
.load(sourceImageUri)
.asBitmap()
.fitCenter()
.into(new FileTarget(to, 1024, 768) {
#Override
public void onFileSaved() {
// do anything, or omit this override if you want
}
});
The resulting image now have dimensions of 1024 x 576 pixels, which is exactly what I expected.
Glide is a very cool library!

Why is JPG altered on retrieval? - Java

I am trying to retrieve an jpg image as a BufferedImage then decompose it into a 3D array [RED][GREEN][BLUE] and then turn it back into a BufferedImage and store it under a different file-name. All looks fine to me BUT, when I am trying to reload the 3D array using the new file created I get different values for RGB although the new image looks fine to the naked eye. I did the following.
BufferedImage bi = ImageIO.read(new File("old.jpg"));
int[][][] one = getArray(bi);
save("kite.jpg", one);
BufferedImage bi2 = ImageIO.read(new File("new.jpg"));
int[][][] two = getArray(bi2);
private void save(String destination, int[][][] in) {
try {
BufferedImage out = new BufferedImage(in.length, in[0].length, BufferedImage.TYPE_3BYTE_BGR);
for (int x=0; x<out.getWidth(); x++) {
for (int y = 0; y < out.getHeight(); y++) {
out.setRGB(x, y, new Color(in[x][y][0], in[x][y][1], in[x][y][2]).getRGB());
}
}
File f = new File("name");
ImageIO.write(out, "JPEG", f);
} catch (IOException e) {
System.out.println(e.getMessage());
}
}
so in the example above the values that arrays one and two are holding are different.
I am guessing there is something to do with the different types of retrieving and restoring images? I am trying to figure out what is going on all day but with no luck. Any help appreciated.
Pretty simple:
JPEG is a commonly used method of lossy compression for digital images
(from wikipedia).
Each time the image is compressed, the image is altered to reduce file-size. In fact repeating the steps of decompression and compression for several hundred times alters the image to the point where the entire image turns into a plain gray area in most cases. There are a few compressions that work lossless, but most operation-modes will alter the image.

How to solve java.lang.IllegalArgumentException: Width (-1) and height (-1) cannot be <= 0?

I've been spending the last hours trying to solve the stack trace below. With major research on here SO and also through Google, I understand the exception can mean several things:
the program can't find the requested images with the provided path;
the images are being rendered after the width and the height are generated, reason why it equals 0...
Am I missing something? I can't figure out how to solve this...
Stack
Exception in thread "main" java.lang.IllegalArgumentException: Width
(-1) and height (-1) cannot be <= 0 at
java.awt.image.DirectColorModel.createCompatibleWritableRaster(DirectColorModel.java:1016)
at java.awt.image.BufferedImage.(BufferedImage.java:331) at
tp6.Interface.toBufferedImage(Interface.java:157) at
tp6.Interface.(Interface.java:36) at
tp6.Interface.main(Interface.java:171)
tp6.Interface.toBufferedImage(Interface.java:157):
public BufferedImage toBufferedImage(Image image) {
if( image instanceof BufferedImage ) {
return( (BufferedImage)image );
} else {
image = new ImageIcon(image).getImage();
BufferedImage bufferedImage = new BufferedImage(
image.getWidth(null),
image.getHeight(null),
BufferedImage.TYPE_INT_RGB );
Graphics g = bufferedImage.createGraphics();
g.drawImage(image,0,0,null);
g.dispose();
return( bufferedImage );
}
}
tp6.Interface.(Interface.java:36)
//IMAGE JPANEL
Image map=new ImageIcon("images/main.gif").getImage();
Image digi=new ImageIcon("images/digits.gif").getImage();
BufferedImage mapmodifiable= toBufferedImage(map);
BufferedImage digits= toBufferedImage(digi);
tp6.Interface.main(Interface.java:171)
public static void main(String[] args)
{
Window windowintro = new Window( 440, 400, 1);
//INTERFACE GRAPHIC
Interface graphic=new Interface();
graphic.addWindowListener(new WindowAdapter() {
#Override
public void windowClosing(WindowEvent evt) {
System.exit(0);
}
});
}
The reason for the exception has already been explained, Image methods getWidth(null) and getHeight(null) both return -1 when the image dimensions are not (yet) known. This is implemented so, because the old Image API in Java is asynchronous and loads image resources off the current thread. As you write, it could also happen because the image is not found.
However, as you want to use your images as BufferedImages (presumably because you want to modify them at some stage), it's better and easier to just load them using the more recent synchronous ImageIO API. In most cases, the code will be clearer and easier to understand, and more importantly; you'll get error messages right away if the image can't be found/loaded.
So, instead of:
Image map = new ImageIcon("images/main.gif").getImage();
BufferedImage mapmodifiable = toBufferedImage(map);
You can simply do:
BufferedImage mapmodifiable = ImageIO.read(new File("images/main.gif"));
PS: It is possible to convert an Image to a BufferedImage like you do in your toBufferedImage method, and using ImageIcon.getImage(..) should ensure the image was preloaded (ImageIcon internally uses a MediaTracker for preloading). However, as I say above, the old Image API is not very good at error feedback, so most likely the problem is that your image isn't found.
I was having this problem too. I solved it by adding one line of code. In your in the first line of your toBufferedImage() method you can put
while(image.getWidth() == -1);
This line will just keep looping until there is a value in getWidth() besides -1.
I found this question concerning your problem. Maybe you are using an asynchronous way to load the images. This mean the image may not be loaded yet, when you are calling getWidth(null) or getHeight(null). Since the image may not be loaded at this time, the width and height may not be known yet. This is the reason why -1 is returned.
Maybe you will get the right result if you add some delay with Thread.sleep(1000). I did not investigate it but it is definitively not a good solution. You may sleep not long enough on some systems. On other systems you may sleep unnecessary long. And since I do not know the specification very well, it may even be a valid implementation of Java if Thread.sleep blocks the process from reading the image (Does someone know it?).
I would see two ways which could be used to solve the problem:
First solution
One solution would be to load the image with blocking IO. Just like descripted in the answers of the linked question.
Second solution
Another solution would be to use an ImageObserver:
int width = getWidth(new ImageObserver() {
#Override
public boolean imageUpdate(Image img, int infoflags, int x, int y, int width, int height) {
// TODO use the width when the image is loaded and the size is known
}
});
if (width != -1) {
// TODO use the width, it is already known
}

Java - resize image without losing quality

I have 10,000 photos that need to be resized so I have a Java program to do that. Unfortunately, the quality of the image is poorly lost and I don't have access to the uncompressed images.
import java.awt.Graphics;
import java.awt.AlphaComposite;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import javax.imageio.ImageIO;
/**
* This class will resize all the images in a given folder
* #author
*
*/
public class JavaImageResizer {
public static void main(String[] args) throws IOException {
File folder = new File("/Users/me/Desktop/images/");
File[] listOfFiles = folder.listFiles();
System.out.println("Total No of Files:"+listOfFiles.length);
BufferedImage img = null;
BufferedImage tempPNG = null;
BufferedImage tempJPG = null;
File newFilePNG = null;
File newFileJPG = null;
for (int i = 0; i < listOfFiles.length; i++) {
if (listOfFiles[i].isFile()) {
System.out.println("File " + listOfFiles[i].getName());
img = ImageIO.read(new File("/Users/me/Desktop/images/"+listOfFiles[i].getName()));
tempJPG = resizeImage(img, img.getWidth(), img.getHeight());
newFileJPG = new File("/Users/me/Desktop/images/"+listOfFiles[i].getName()+"_New");
ImageIO.write(tempJPG, "jpg", newFileJPG);
}
}
System.out.println("DONE");
}
/**
* This function resize the image file and returns the BufferedImage object that can be saved to file system.
*/
public static BufferedImage resizeImage(final Image image, int width, int height) {
int targetw = 0;
int targeth = 75;
if (width > height)targetw = 112;
else targetw = 50;
do {
if (width > targetw) {
width /= 2;
if (width < targetw) width = targetw;
}
if (height > targeth) {
height /= 2;
if (height < targeth) height = targeth;
}
} while (width != targetw || height != targeth);
final BufferedImage bufferedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
final Graphics2D graphics2D = bufferedImage.createGraphics();
graphics2D.setComposite(AlphaComposite.Src);
graphics2D.setRenderingHint(RenderingHints.KEY_INTERPOLATION,RenderingHints.VALUE_INTERPOLATION_BILINEAR);
graphics2D.setRenderingHint(RenderingHints.KEY_RENDERING,RenderingHints.VALUE_RENDER_QUALITY);
graphics2D.setRenderingHint(RenderingHints.KEY_ANTIALIASING,RenderingHints.VALUE_ANTIALIAS_ON);
graphics2D.drawImage(image, 0, 0, width, height, null);
graphics2D.dispose();
return bufferedImage;
}
An image I am working with is this:
This is the manual resizing I've done in Microsoft Paint:
and this is the output from my program [bilinear]:
UPDATE: No significant difference using BICUBIC
and this is the output from my program [bicubic]:
is there anyway to increase the quality of the program output so I don't have to manually resize all photos?
Thank you in advance!
Unfortunately, there is no recommended out-of-the-box scaling in Java that provides visually good results. Among others, here are the methods I recommend for scaling:
Lanczos3 Resampling (usually visually better, but slower)
Progressive Down Scaling (usually visually fine, can be quite fast)
One-Step scaling for up scaling (with Graphics2d bicubic fast and good results, usually not as good as Lanczos3)
Examples for every method can be found in this answer.
Visual Comparison
Here is your image scaled to 96x140 with different methods/libs. Click on the image to get the full size:
Morten Nobel's lib Lanczos3
Thumbnailator Bilinear Progressive Scaling
Imgscalr ULTRA_QUALTY (1/7 step Bicubic Progressive Scaling)
Imgscalr QUALTY (1/2 step Bicubic Progressive Scaling)
Morten Nobel's lib Bilinear Progressive Scaling
Graphics2d Bicubic interpolation
Graphics2d Nearest Neighbor interpolation
Photoshop CS5 bicubic as reference
Unfortunately a single image is not enough to judge a scaling algorithm, you should test icons with sharp edges, photos with text, etc.
Lanczos Resampling
Is said to be good for up- and especially downscaling. Unfortunately there is no native implementation in current JDK so you either implement it yourself and use a lib like Morten Nobel's lib. A simple example using said lib:
ResampleOp resizeOp = new ResampleOp(dWidth, dHeight);
resizeOp.setFilter(ResampleFilters.getLanczos3Filter());
BufferedImage scaledImage = resizeOp.filter(imageToScale, null);
The lib is published on maven-central which is not mentioned unfortunately. The downside is that it usually is very slow without any highly optimized or hardware accelerated implementations known to me. Nobel's implementation is about 8 times slower than a 1/2 step progressive scaling algorithm with Graphics2d. Read more about this lib on his blog.
Progressive Scaling
Mentioned in Chris Campbell's blog about scaling in Java, progressive scaling is basically incrementally scaling an image in smaller steps until the final dimensions are reached. Campbell describes it as halving width/height until you reach target. This produces good results and can be used with Graphics2D which can be hardware accelerated, therefore usually having very good performance with acceptable results in most cases. The major downside of this is if downscaled less than half using Graphics2D provides the same mediocre results since it is only scaled once.
Here is a simple example on how it works:
The following libs incorporate forms of progressive scaling based on Graphics2d:
Thumbnailator v0.4.8
Uses the progressive bilinear algorithm if the target is at least half of every dimension, otherwise it uses simple Graphics2d bilinear scaling and bicubic for upscaling.
Resizer resizer = DefaultResizerFactory.getInstance().getResizer(
new Dimension(imageToScale.getWidth(), imageToScale.getHeight()),
new Dimension(dWidth, dHeight))
BufferedImage scaledImage = new FixedSizeThumbnailMaker(
dWidth, dHeight, false, true).resizer(resizer).make(imageToScale);
It is as fast or slightly faster than one-step scaling with Graphics2d scoring an average of 6.9 sec in my benchmark.
Imgscalr v4.2
Uses progressive bicubic scaling. In the QUALITY setting it uses Campbell style algorithm with halving the dimensions every step while the ULTRA_QUALITY has finer steps, reducing the size every increment by 1/7 which generates generally softer images but minimizes the instances where only 1 iteration is used.
BufferedImage scaledImage = Scalr.resize(imageToScale, Scalr.Method.ULTRA_QUALITY, Scalr.Mode.FIT_EXACT, dWidth, dHeight, bufferedImageOpArray);
The major downside is performance. ULTRA_QUALITY is considerably slower than the other libs. Even QUALITY a bit slower than Thumbnailator's implementation. My simple benchmark resulted in 26.2 sec and 11.1 sec average respectively.
Morten Nobel's lib v0.8.6
Has also implementations for progressive scaling for all basic Graphics2d (bilinear, bicubic & nearest neighbor)
BufferedImage scaledImage = new MultiStepRescaleOp(dWidth, dHeight, RenderingHints.VALUE_INTERPOLATION_BILINEAR).filter(imageToScale, null);
A word on JDK Scaling Methods
Current jdk way to scale an image would be something like this
scaledImage = new BufferedImage(dWidth, dHeight, imageType);
Graphics2D graphics2D = scaledImage.createGraphics();
graphics2D.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
graphics2D.drawImage(imageToScale, 0, 0, dWidth, dHeight, null);
graphics2D.dispose();
but most are very disappointed with the result of downscaling no matter what interpolation or other RenderHints are used. On the other hand upscaling seems to produce acceptable images (best would be bicubic). In previous JDK version (we talking 90s v1.1) Image.getScaledInstance() was introduced which provided good visual results with parameter SCALE_AREA_AVERAGING but you are discouraged to use it - read the full explanation here.
Thumbnailator is a library that was written to create high-quality thumbnails in a simple manner, and doing a batch conversion of existing images is one of its use cases.
Performing batch resizing
For example, to adapt your example using Thumbnailator, you should be able to achieve similar results with the following code:
File folder = new File("/Users/me/Desktop/images/");
Thumbnails.of(folder.listFiles())
.size(112, 75)
.outputFormat("jpg")
.toFiles(Rename.PREFIX_DOT_THUMBNAIL);
This will go ahead and takes all files in your images directory and proceed to process them one by one, try to resize them to fit in the dimensions of 112 x 75, and it will attempt to preserve the aspect ratio of the original image to prevent "warping" of the image.
Thumbnailator will go ahead and read all files, regardless of image types (as long as the Java Image IO supports the format, Thumbnailator will process it), perform the resizing operation and output the thumbnails as JPEG files, while tacking on a thumbnail. to the beginning of the file name.
The following is an illustration of how the file name of the original will be used in the file name of the thumbnail if the above code is executed.
images/fireworks.jpg -> images/thumbnail.fireworks.jpg
images/illustration.png -> images/thumbnail.illustration.png
images/mountains.jpg -> images/thumbnail.mountains.jpg
Generating high-quality thumbnails
In terms of image quality, as mentioned in Marco13's answer, the technique described by Chris Campbell in his The Perils of Image.getScaledInstance() is implemented in Thumbnailator, resulting in high-quality thumbnails without requiring any complicated processing.
The following is the thumbnail generated when resizing the fireworks image shown in the original question using Thumbnailator:
The above image was created with the following code:
BufferedImage thumbnail =
Thumbnails.of(new URL("http://i.stack.imgur.com/X0aPT.jpg"))
.height(75)
.asBufferedImage();
ImageIO.write(thumbnail, "png", new File("24745147.png"));
The code shows that it can also accept URLs as input, and that Thumbnailator is also capable of creating BufferedImages as well.
Disclaimer: I am the maintainer of the Thumbnailator library.
Given your input image, the method from the answer in the first link in the comments (kudos to Chris Campbell) produces one of the following thumbnails:
(The other one is the thumbnail that you created with MS Paint. It's hard to call one of them "better" than the other...)
EDIT: Just to point this out as well: The main problem with your original code was that you did not really scale the image in multiple steps. You just used a strange loop to "compute" the target size. The key point is that you actually perform the scaling in multiple steps.
Just for completeness, the MVCE
(Edit: I mentioned Chris Campbell and referred to the source via the comments, but to make this more clear here: The following is based on the article The Perils of Image.getScaledInstance() )
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.Transparency;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Iterator;
import javax.imageio.IIOImage;
import javax.imageio.ImageIO;
import javax.imageio.ImageWriteParam;
import javax.imageio.ImageWriter;
import javax.imageio.stream.ImageOutputStream;
import javax.imageio.stream.MemoryCacheImageOutputStream;
public class ResizeQuality
{
public static void main(String[] args) throws IOException
{
BufferedImage image = ImageIO.read(new File("X0aPT.jpg"));
BufferedImage scaled = getScaledInstance(
image, 51, 75, RenderingHints.VALUE_INTERPOLATION_BILINEAR, true);
writeJPG(scaled, new FileOutputStream("X0aPT_tn.jpg"), 0.85f);
}
public static BufferedImage getScaledInstance(
BufferedImage img, int targetWidth,
int targetHeight, Object hint,
boolean higherQuality)
{
int type =
(img.getTransparency() == Transparency.OPAQUE)
? BufferedImage.TYPE_INT_RGB : BufferedImage.TYPE_INT_ARGB;
BufferedImage ret = (BufferedImage) img;
int w, h;
if (higherQuality)
{
// Use multi-step technique: start with original size, then
// scale down in multiple passes with drawImage()
// until the target size is reached
w = img.getWidth();
h = img.getHeight();
}
else
{
// Use one-step technique: scale directly from original
// size to target size with a single drawImage() call
w = targetWidth;
h = targetHeight;
}
do
{
if (higherQuality && w > targetWidth)
{
w /= 2;
if (w < targetWidth)
{
w = targetWidth;
}
}
if (higherQuality && h > targetHeight)
{
h /= 2;
if (h < targetHeight)
{
h = targetHeight;
}
}
BufferedImage tmp = new BufferedImage(w, h, type);
Graphics2D g2 = tmp.createGraphics();
g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, hint);
g2.drawImage(ret, 0, 0, w, h, null);
g2.dispose();
ret = tmp;
} while (w != targetWidth || h != targetHeight);
return ret;
}
public static void writeJPG(
BufferedImage bufferedImage,
OutputStream outputStream,
float quality) throws IOException
{
Iterator<ImageWriter> iterator =
ImageIO.getImageWritersByFormatName("jpg");
ImageWriter imageWriter = iterator.next();
ImageWriteParam imageWriteParam = imageWriter.getDefaultWriteParam();
imageWriteParam.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
imageWriteParam.setCompressionQuality(quality);
ImageOutputStream imageOutputStream =
new MemoryCacheImageOutputStream(outputStream);
imageWriter.setOutput(imageOutputStream);
IIOImage iioimage = new IIOImage(bufferedImage, null, null);
imageWriter.write(null, iioimage, imageWriteParam);
imageOutputStream.flush();
}
}
After days of research i would prefer javaxt.
use Thejavaxt.io.Image class has a constructor like:
public Image(java.awt.image.BufferedImage bufferedImage)
so you can do (another example):
javaxt.io.Image image = new javaxt.io.Image(bufferedImage);
image.setWidth(50);
image.setOutputQuality(1);
Here's the output:
We should not forget a TwelveMonkeys Library
It contains a really impressive filter collection.
Usage example:
BufferedImage input = ...; // Image to resample
int width, height = ...; // new width/height
BufferedImageOp resampler = new ResampleOp(width, height, ResampleOp.FILTER_LANCZOS);
BufferedImage output = resampler.filter(input, null);
Below are my own implementation of Progressive Scaling, without using any external library. Hope this help.
private static BufferedImage progressiveScaling(BufferedImage before, Integer longestSideLength) {
if (before != null) {
Integer w = before.getWidth();
Integer h = before.getHeight();
Double ratio = h > w ? longestSideLength.doubleValue() / h : longestSideLength.doubleValue() / w;
//Multi Step Rescale operation
//This technique is describen in Chris Campbell’s blog The Perils of Image.getScaledInstance(). As Chris mentions, when downscaling to something less than factor 0.5, you get the best result by doing multiple downscaling with a minimum factor of 0.5 (in other words: each scaling operation should scale to maximum half the size).
while (ratio < 0.5) {
BufferedImage tmp = scale(before, 0.5);
before = tmp;
w = before.getWidth();
h = before.getHeight();
ratio = h > w ? longestSideLength.doubleValue() / h : longestSideLength.doubleValue() / w;
}
BufferedImage after = scale(before, ratio);
return after;
}
return null;
}
private static BufferedImage scale(BufferedImage imageToScale, Double ratio) {
Integer dWidth = ((Double) (imageToScale.getWidth() * ratio)).intValue();
Integer dHeight = ((Double) (imageToScale.getHeight() * ratio)).intValue();
BufferedImage scaledImage = new BufferedImage(dWidth, dHeight, BufferedImage.TYPE_INT_RGB);
Graphics2D graphics2D = scaledImage.createGraphics();
graphics2D.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
graphics2D.drawImage(imageToScale, 0, 0, dWidth, dHeight, null);
graphics2D.dispose();
return scaledImage;
}
The result seems to be better (than the result of your program), if you apply Gaussian blur before resizing:
This is the result I get, with sigma * (scale factor) = 0.3:
With ImageJ the code to do this is quite short:
import ij.IJ;
import ij.ImagePlus;
import ij.io.Opener;
import ij.process.ImageProcessor;
public class Resizer {
public static void main(String[] args) {
processPicture("X0aPT.jpg", "output.jpg", 0.0198, ImageProcessor.NONE, 0.3);
}
public static void processPicture(String inputFile, String outputFilePath, double scaleFactor, int interpolationMethod, double sigmaFactor) {
Opener opener = new Opener();
ImageProcessor ip = opener.openImage(inputFile).getProcessor();
ip.blurGaussian(sigmaFactor / scaleFactor);
ip.setInterpolationMethod(interpolationMethod);
ImageProcessor outputProcessor = ip.resize((int)(ip.getWidth() * scaleFactor), (int)(ip.getHeight()*scaleFactor));
IJ.saveAs(new ImagePlus("", outputProcessor), outputFilePath.substring(outputFilePath.lastIndexOf('.')+1), outputFilePath);
}
}
BTW: You only need ij-1.49d.jar (or equivalent for other version); there's no need to install ImageJ.

Adjust brightness and contrast of BufferedImage in Java

I'm processing a bunch of images with some framework, and all I'm given is a bunch of BufferedImage objects. Unfortunately, these images are really dim, and I'd like to brighten them up and adjust the contrast a little.
Something like:
BufferedImage image = something.getImage();
image = new Brighten(image).brighten(0.3); // for 30%
image = new Contrast(image).contrast(0.3);
// ...
Any ideas?
That was easy, actually.
RescaleOp rescaleOp = new RescaleOp(1.2f, 15, null);
rescaleOp.filter(image, image); // Source and destination are the same.
A scaleFactor of 1.2 and offset of 15 seems to make the image about a stop brighter.
Yay!
Read more in the docs for RescaleOp.

Categories

Resources