10

When using javax.imageio.ImageIO to load a large-resolution (9000x9000) JPEG from disk, it takes more than 1 minute in my scala application. I tried creating a Java-only project, but it still takes too long - around 30 seconds.

This is how I load the image:

File file = new File("/Users/the21st/slow2.jpg");
BufferedImage image = ImageIO.read(file);

Is there any way to improve performance on reading progressively encoded large-res JPEGs in Java?

The image in question is this one (moderators, please don't reupload to other hosting site again so that encoding / quality doesn't change)

the21st
  • 1,012
  • 1
  • 10
  • 24
  • Loading that image (from a local file) using `ImageIO` in a simple java program takes about 1-2 seconds on my PC - where are you getting the image from? when you do it? – BretC Apr 17 '15 at 16:51
  • 3
    @Bret I didn't realize StackOverflow probably resized and/or re-encoded that image during upload to their server. This is the original image: https://dl.dropboxusercontent.com/u/73774/slow2.jpg (edited original question with this link) – the21st Apr 17 '15 at 16:54
  • Have you tried using `InputStreamReader` and/or `BufferedReader`? – Mr. Polywhirl Apr 17 '15 at 16:58
  • @Mr.Polywhirl Can you give an example? `ImageIO.read` does not take a Reader as a parameter. – the21st Apr 17 '15 at 17:06
  • 3
    Hmm, it seems that progressive encoding is the culprit. If i re-encode your sample as baseline, the reading time goes down from ~40s -> ~2s. – barti_ddu Apr 17 '15 at 17:47
  • Progessive coding should not slow down the decompression UNLESS the decoder is performing incremental updates of the images. There must be some setting in the encoder to tell it not to update the image under the final scan. – user3344003 Apr 17 '15 at 18:00
  • 1
    Perhaps you're GC churning, try increase heap memory – Steve Kuo Apr 17 '15 at 18:02
  • 1
    I have voted to reopen. "Are there any faster alternatives to ImageIO that can load JPEG images?" is not necessarily a recommendation for a tool. I had a number of programming suggestions. – user3344003 Apr 17 '15 at 22:18
  • 1
    a: What does your code look like? The way you read it, may make an impact. b: What is the source of your image? Whether you read from disk, network or memory may have a huge impact. c: Have you tried disable disk caching? `ImageIO.setUseCache(false)` (which is `true` by default) usually gives a speed increase (at the cost of using more memory). – Harald K Apr 18 '15 at 12:12
  • 2
    Due to using a different code path, my [JPEG plugin for ImageIO](https://github.com/haraldk/TwelveMonkeys#jpeg) does read the image about twice as fast (on my computer JRE: ~18 sec, TM: ~8 sec, ymmv). This means that if you obtain the JRE `ImageReader` for JPEG and use the `readRaster` method, you'll see the same speed up. Seems to be due to the progressive encoding, as noted by @barti_ddu PS: Voted for re-open, but can't answer yet... – Harald K Apr 18 '15 at 13:09
  • This only takes a second to load on my 'puter. I have a few theories and work arounds you might be able to try but I am space limited until there are enough votes to reopen. – user3344003 Apr 18 '15 at 20:23
  • @user3344003 Can you read the original progressive JPEG from the dropbox link in the comments (not the imgur one from the question, that has been re-encoded as baseline) in 1 sec? If so, what kind of computer do you have, and how do you do it? I'm very interested! – Harald K Apr 19 '15 at 17:40
  • I have Mac. It takes a second to load on the plain vanilla Preview app. I suspect that it is sampling the image for display. – user3344003 Apr 19 '15 at 19:19
  • How much RAM did you allocate to your app? By my estimates your preview image takes up roughly 950mb ram when loaded in memory. If you havent allocated enough RAM you might loose some overhead to the expansion of allocated ram. And if your RAM is nearly full it might start swapping to disk. – Tschallacka Apr 20 '15 at 06:58
  • @MichaelDibbets I allocated 3 gigs to my app, `Runtime.getRuntime().maxMemory()` returns 2667 MBs. The images now takes 27s to load (not a significant improvement) – the21st Apr 20 '15 at 07:10
  • @haraldK I tried `ImageIO.setUseCache(false)` but that actually caused a 1-2s slowdown. – the21st Apr 20 '15 at 07:11
  • Have you tried `BufferedImage img = ImageIO.read(new BufferedInputStream(new FileInputStream(file)));` ? I have noticed using a stream sometimes gives a speed increase. I dont know if the `read` utilises a stream internally. But I usually go with a buffered stream. – Tschallacka Apr 20 '15 at 07:22
  • @MichaelDibbets Tried that, no improvement. – the21st Apr 20 '15 at 07:36
  • If you load this image in an application like the GIMP or photoshop on your computer. how long does this application take to load it if you do file open? On my i5 it takes about 5 seconds to load it from disc with a graphics application. By testing this we can get a benchmark what speed it should achieve on your computer. – Tschallacka Apr 20 '15 at 07:47
  • @the21st Then you are likely to be very low on memory (or your RAM is slower than your disk, but that's unlikely). It's not always faster, but I've never experienced it to be slower. You should probably do some profiling, to figure out where the bottlenecks are. And say something about your system, use case and show more code. – Harald K Apr 20 '15 at 07:52
  • @user3344003 Oh, I thought you achieved that speed in Java... I think it should be doable to get close though, it's just a matter of how much time and energy you can spend. – Harald K Apr 20 '15 at 07:54
  • @the21st Using my [`BufferedImageFactory`](https://github.com/haraldk/TwelveMonkeys/blob/master/common/common-image/src/main/java/com/twelvemonkeys/image/BufferedImageFactory.java) class and the AWT `Toolkit`, like this `BufferedImage image = new BufferedImageFactory(Toolkit.getDefaultToolkit().createImage(file.getAbsolutePath())).getBufferedImage();` I could read the image in ~2.5 sec (in Java). – Harald K Apr 20 '15 at 13:15
  • Yeah, it's a real bummer. With the old com.sun.image.codec.jpeg.JPEGCodec that was removed in Java 7, it takes about 2 seconds. – Maurice Perry Apr 20 '15 at 13:51

2 Answers2

6

Ok, here's my findings so far (and to be honest, they are a little worrying...).

Using the standard JPEG plugin for ImageIO bundled with the Oracle JRE:

BufferedImage image = ImageIO.read(file); 

Reads the image in roughly 18 seconds on my computer (a MacBookPro/2.8GHz i7).

Using my JPEG plugin for ImageIO, which uses a slightly different code path (i.e., you can probably get the same results by obtaining the ImageReader and invoking the readRaster() method, then creating a BufferedImage from that. The code is non-trivial, so please refer tho the project page if you like to see the code):

BufferedImage image = ImageIO.read(file); 

Reads the image in roughly 8 seconds on my computer.

Using my BufferedImageFactory class and the AWT Toolkit:

BufferedImage image = new BufferedImageFactory(Toolkit.getDefaultToolkit().createImage(file.getAbsolutePat‌​h())).getBufferedImage();

Reads the image in ~2.5 seconds on my computer.

Using the deprecated JPEGImageDecoder class from sun.awt.codec:

BufferedImage image = new JPEGImageDecoderImpl(new FileInputStream(file)).decodeAsBufferedImage();

Reads the image in ~1.7 seconds on my computer.

So, this means that we should be able to read this image in less than 2 seconds, even in Java. The performance from the JPEGImageReader is just ridiculous in this case, and I really like to know why. As already mentioned, it seems to have to with the progressive decoding, but still, it should be better than this.

Update:

Just for the fun of it, I created a quick PoC ImageReader plugin backed by the LibJPEG-Turbo Java API. It's not very sophisticated yet, but it allows for code like:

BufferedImage image = ImageIO.read(file); 

To read the image in < 1.5 seconds on my computer.

PS: I used to maintain ImageIO wrappers for JMagick (similar to the code mentioned by @Jordan Doyle, but it would allow you to program against the ImageIO API), however I stopped as it was too much work. Maybe I have to reconsider... At least it's worth checking out his solution as well, if you don't mind on relying on JNI/native code installation.

Harald K
  • 26,314
  • 7
  • 65
  • 111
  • Is there a way to modify your `BufferedImageFactory` approach so that it can decode CMYK jpegs, too? – the21st Apr 27 '15 at 13:51
  • @the21st Sorry, it seems the `Toolkit.createImage` method doesn't support CMYK, so it won't work (and there's really not much we can do to fix it either). Both my original plugin and the LibJPEG-Turbo version does, though. ;-) – Harald K Apr 27 '15 at 15:41
  • 1
    Quite a late comment for this thread. I have quite a slow PC and use Windows 10 / JDK14, and tried loading the image: ImageIO.read(file) takes 50 seconds, but JavaFX new javafx.scene.image.Image() takes just ~3.5sec. – DuncG May 14 '20 at 18:01
  • @DuncG Interesting. The ImageIO plugin really should have some shortcut to avoid the progressive updates at least. But still, the difference you see is huge. – Harald K May 14 '20 at 19:56
2

One faster alternative to ImageIO is ImageMagick, there are various wrappers for interfacing ImageMagick via Java such as JMagick

To get a BufferedImage from JMagick you must first get an instance of MagickImage which can be done like so:

ImageInfo info = new ImageInfo(pathToImage);
MagickImage image = new MagickImage(info);

Now you can use the method provided by our very own Jacob Nordfalk 8 years ago to read the image into a BufferedImage here

public static BufferedImage magickImageToBufferedImage(MagickImage magickImage) throws Exception
{
    Dimension  dim = magickImage.getDimension();
    int size = dim.width * dim.height;
    byte[] pixels = new byte[size * 3];

    magickImage.dispatchImage(0, 0, dim.width, dim.height, "RGB", pixels);

    BufferedImage bimage = createInterleavedRGBImage(dim.width, dim.height, pixels);

    ColorModel cm = bimage.getColorModel();
    Raster raster = bimage.getData();
    WritableRaster writableRaster = null;

    writableRaster = (raster instanceof WritableRaster) ? (WritableRaster) raster : raster.createCompatibleWritableRaster();

    BufferedImage bufferedImage = new BufferedImage(cm, writableRaster, false, null);

    return bufferedImage;
}

Then the createInterleavedRGBImage method:

public static BufferedImage createInterleavedRGBImage(int imageWidth, int imageHeight, byte data[])
{
    int[] numBits = new int[3];
    int[] bandoffsets = new int[3];

    for (int i = 0; i < 3; i++) {
        numBits[i] = 8;
        bandoffsets[i] = i;
    }

    ComponentColorModel ccm = new ComponentColorModel(
        ColorSpace.getInstance(ColorSpace.CS_sRGB),
        numBits,
        false,
        false, //Alpha pre-multiplied
        Transparency.OPAQUE,
        DataBuffer.TYPE_BYTE
    );

    PixelInterleavedSampleModel csm = new PixelInterleavedSampleModel(
        DataBuffer.TYPE_BYTE,
        imageWidth,
        imageHeight,
        3, //Pixel stride
        imageWidth * 3, // Scanline stride
        bandoffsets
    );

    DataBuffer dataBuf = new DataBufferByte(data, imageWidth * imageHeight * 3);
    WritableRaster wr = Raster.createWritableRaster(csm, dataBuf, new Point(0, 0));
    return new BufferedImage(ccm, wr, false, null);
}
Community
  • 1
  • 1
Jordan Doyle
  • 2,976
  • 4
  • 22
  • 38