10

I've tried multiple image resizing libraries posted here on SO, as well as with raw java using getScaledInstance. While everything works most of the time, there are two jpeg images for which the colors are always messed up whenever I resize them.

First image:

1

Result:

2

Second image:

3

Result:

4

I've tried these images with multiple libraries including Scalr, Thumbnailator, and raw java using image.getScaledInstance() (see here for code), but the result is the same.

Any ideas what the problem is?

Community
  • 1
  • 1
Ali
  • 261,656
  • 265
  • 575
  • 769
  • Are you saving them to disk? Or drawing on Graphics? Perhaps the problem lies during drawing? Some weird `add` hints or something? Or invalid color model on target buffered graphics? – Dariusz Oct 29 '13 at 13:17
  • @Dariusz I'm saving them to disk, I said I've tried with 2-3 libraries as well as just using raw java, please see http://stackoverflow.com/questions/19654017/resizing-bufferedimages-and-storing-them-to-file-results-in-black-background-for#19654452 for the code I used. – Ali Oct 29 '13 at 13:18
  • 1
    there was someone having similar issues: http://stackoverflow.com/questions/9340569/jpeg-image-with-wrong-colors – grexter89 Oct 29 '13 at 13:19
  • @grexter89 their solution is really hairy... and they seem to be getting the imageData / inputStream, while for me, I'd like to just use the File / BufferedImage. – Ali Oct 29 '13 at 13:23
  • Try storing the image without scaling. Or better, display them after loading. I suspect these images are YCbCr encoded, but not JFIF, and thus the default `JPEGImageReader` does not handle them correctly (assumes them to be RGB). In other words, it's unrelated to the resampling. I've created a [JPEGImageReader](https://github.com/haraldk/TwelveMonkeys/blob/master/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReader.java) that does read them correctly, feel free to try it out. – Harald K Oct 29 '13 at 13:53
  • 1
    Instead of Image.getScaledInstance, have you tried scaling yourself by using Graphics2D.scale and then drawing the image with drawImage? Although this sounds like an unusual color problem and not a scaling problem. Have you tried just reading the image and then saving it immediately without scaling it? That will tell you whether or not it is really the scaling that is the problem. – Radiodef Oct 29 '13 at 13:54
  • @Radiodef Reading it and saving it immediately works fine – Ali Oct 29 '13 at 14:20
  • It is probably what @haraldK is saying then about the encoding. I don't know what YCbCr is to offer you a technical explanation other than that the reader probably does not recognize it and is interpreting the pixel colors incorrectly. There could be another simpler workaround that doesn't use the linked reader but depends on how you are doing the scaling. If it were me I would not worry about it and move on unless reading any format was an intended and important feature of the program I was writing. – Radiodef Oct 29 '13 at 14:45
  • Viewing the info on these images with the Mac OS X "get info" option says they are RGB color space but this could be a red herring. OS X could also be reading the color space incorrectly. Depends on how a JPEG file indicates this information which I don't know, JPEG is not a file format I'm very familiar with. – Radiodef Oct 29 '13 at 14:54
  • @Radiodef I can't move on.. its an important feature of the program, it needs to resize the users' avatars and display them. – Ali Oct 29 '13 at 16:36
  • @haraldK I can't really display them or store them without resizing, this is for a website and they need to be resized to a given size so they wouldn't break the layout. – Ali Oct 29 '13 at 16:41
  • Well you basically can choose to use Harold's reader which provides licensing conditions in the source (and I assume he meant it is OK to use it) or you can try to write your own converter. Here is a Stack Overflow question that can point you in the right direction for this: http://stackoverflow.com/questions/2779951/how-to-work-with-bufferedimage-and-ycbcr-colorspace – Radiodef Oct 29 '13 at 17:37
  • @haraldK Could you give an example of how to use your JpegReader or the rest of your library to resize a given BufferedImage ? And will it work on these two example images? – Ali Oct 29 '13 at 17:43
  • @ClickUpvote I did a quick test, and yes, it seems to read your images fine. No changes in your code required, just put the classes on class path. For more help see [Using the JPEGImageReader](https://github.com/haraldk/TwelveMonkeys/issues/14) or file your own issue if you have a different problem. – Harald K Oct 30 '13 at 10:04

1 Answers1

6

I've found a solution, with a lot of help from this answer:

import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.MediaTracker;
import java.awt.RenderingHints;
import java.awt.Toolkit;
import java.awt.Transparency;
import java.awt.image.BufferedImage;
import java.io.File;
import javax.imageio.ImageIO;
import javax.swing.JPanel;

public class ImgUtility
{

    /**
     * Takes a file, and resizes it to the given width and height, while keeping
     * original proportions. Note: It resizes a new file rather than resizing 
     * the original one. Resulting file is always written as a png file due to issues
     * with resizing jpeg files which results in color loss. See:
     * https://stackoverflow.com/a/19654452/49153 
     * for details, including the comments.
     * 
     */    
    public static File resize(File file, int width, int height) throws Exception
    {
        Image img = Toolkit.getDefaultToolkit().getImage( file.getAbsolutePath() );
        loadCompletely(img);
        BufferedImage bm = toBufferedImage(img);
        bm = resize(bm, width, height);

        StringBuilder sb = new StringBuilder();
        sb.append( bm.hashCode() ).append(".png");
        String filename = sb.toString(); 

        File result = new File( filename );
        ImageIO.write(bm, "png", result);

        return result;
    }

    public static BufferedImage toBufferedImage(Image img)
    {
        if (img instanceof BufferedImage)
        {
            return (BufferedImage) img;
        }

        BufferedImage bimage = new BufferedImage(img.getWidth(null), img.getHeight(null), BufferedImage.TYPE_INT_ARGB);

        bimage.getGraphics().drawImage(img, 0, 0 , null);
        bimage.getGraphics().dispose();

        return bimage;
    }

    public static BufferedImage resize(BufferedImage image, int areaWidth, int areaHeight)
    {
        float scaleX = (float) areaWidth / image.getWidth();
        float scaleY = (float) areaHeight / image.getHeight();
        float scale = Math.min(scaleX, scaleY);
        int w = Math.round(image.getWidth() * scale);
        int h = Math.round(image.getHeight() * scale);

        int type = image.getTransparency() == Transparency.OPAQUE ? BufferedImage.TYPE_INT_RGB : BufferedImage.TYPE_INT_ARGB;

        boolean scaleDown = scale < 1;

        if (scaleDown) {
            // multi-pass bilinear div 2
            int currentW = image.getWidth();
            int currentH = image.getHeight();
            BufferedImage resized = image;
            while (currentW > w || currentH > h) {
                currentW = Math.max(w, currentW / 2);
                currentH = Math.max(h, currentH / 2);

                BufferedImage temp = new BufferedImage(currentW, currentH, type);
                Graphics2D g2 = temp.createGraphics();
                g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
                g2.drawImage(resized, 0, 0, currentW, currentH, null);
                g2.dispose();
                resized = temp;
            }
            return resized;
        } else {
            Object hint = scale > 2 ? RenderingHints.VALUE_INTERPOLATION_BICUBIC : RenderingHints.VALUE_INTERPOLATION_BILINEAR;

            BufferedImage resized = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
            Graphics2D g2 = resized.createGraphics();
            g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, hint);
            g2.drawImage(image, 0, 0, w, h, null);
            g2.dispose();
            return resized;
        }
    }


    /**
     * Since some methods like toolkit.getImage() are asynchronous, this
     * method should be called to load them completely.
     */
    public static void loadCompletely (Image img)
    {
        MediaTracker tracker = new MediaTracker(new JPanel());
        tracker.addImage(img, 0);
        try {
            tracker.waitForID(0);
        } catch (InterruptedException ex) {
            throw new RuntimeException(ex);
        }
    }
}
Community
  • 1
  • 1
Ali
  • 261,656
  • 265
  • 575
  • 769
  • The gist of this answer is to change the image type - cf the `toBufferedImage()` method. However, there is no reason to make your own resizing algorithm. Just use a library like ImgScalr for resizing, and change the color type as a post-processing step. – Valentin Waeselynck Feb 26 '16 at 14:23