The Minecraft Overviewer

a high-resolution Minecraft world renderer with a LeafletJS interface


On the New optimizeimg

Hi, I'm CounterPillow. If you've ever been in our IRC channel, you've probably seen me. I have been an irregular contributor to Overviewer for some time now, and recently, I've rewritten the way optimizeimg works. I thought that this would be a good opportunity to explain what it is, and how to best use it.

First, let's explain what this is all about. In essence, this increases the time it takes to render a map in favor of smaller images. But how does it do that? optimizeimg allows you to run various image optimizers against each tile image the Overviewer outputs. As of the time of writing, we support optipng, pngcrush and pngnq.

Lossless vs. Lossy Compression

One of the key things to understand is what lossless and lossy compression mean. In lossless compression, the file size is reduced, but the decompressed data is exactly the same. No data is lost in the compression step, hence the name "lossless". Lossy compression, on the other hand, does not preserve all information. An uncompressed output of a lossy compression algorithm is not the same as the input.

As you may already know, PNG uses lossless compression. However, there are a lot of different knobs to tweak when it comes to how the image is compressed, thus reducing file size without altering the image data, and we can even reduce the amount of colors in the image, essentially making the process lossy! I'm not going to go into detail as to how this all works in practice, but I will showcase the tools we currently support to achieve this.

Lossless Image Optimization, and "crushing"

First off, I'll have to admit that I've lied in the previous section. Some of the lossless image recompression methods are not lossless in the sense of "data in is the same as data out decompressed". Some tools, like optipng, discard unused alpha channels (where images store their transparency), to reduce file size. I said unused, which means that the visible output is still pixel-per-pixel the same, thus lossless, but the image data is obviously different. However, for all intents and purposes, we can call this process "lossless", because no visible information has been changed.

Discarding unused alpha channels is one way of making the image smaller, but there are other ways which are truly lossless. Both pngcrush and optipng tweak the previously mentioned knobs of the compression, trying to find the smallest possible output. The two tools do this a bit differently, but the principle is the same: Tweak the encoding parameters. This is what the Overviewer code refers to as a "crusher". If you wish to learn more on how this is done internally, you can read about it on the optipng site, but beware: It's quite technical.

Lossy Image Optimization

With pngnq, we also offer a way of lossy image optimization. You may be wondering how a PNG can be lossily optimized, as the compression method is lossless. The answer is simple: The amount of colors.

PNG supports a ton of different color modes, amongst them is an indexed color mode, or also referred to as "Palette PNG" or "PNG8". In images with indexed color modes, the colors of each pixel aren't determined individually using red, green and blue channels. Instead, it has a palette of colors, and then specifies for each pixel the color for it by assigning it the a color of the palette. This means less colors, in our case 256 at most, and because we can represent those with just one byte, it means less data that needs to be compressed. If we find the 256 colors that can best represent the image, we get pretty good results that are just a fraction of the original size; and since Minecraft doesn't use a lot of colors in the default texture pack, it's barely noticeable. agrif briefly mentioned this in the "Introducing OIL" blog post. In fact, if our input image has less than or exactly 256 colors anyway, the process is completely lossless!

Instead of waiting for OIL, you can use palette PNGs right now, with the pngnq tool. However, please be aware that due to multiple bugs in the imaging library we currently use, PIL (or Pillow, both are buggy), this will break transparency. As stated in the docs, this is not pngnq's fault.

Using Image Optimization In Practice

But how is this used in practice, you ask? It's not that hard. The first step is to install the tools you wish to use and make sure they can be called by Overviewer (i.e., are in your PATH environment variable). Then, add them to your config.

Please be aware that this will increase render times.

An example for everyone

The following example should be satisfactory for most people who wish to use image optimization. Please also be aware that when I say "most people", I always actually mean "most people". If you think you are not "most people", then you're doing so carrying the risk that you may be wasting your time.

from .optimizeimages import optipng

renders["foo"] = {
    # ... other stuff here
    "optimizeimg":[optipng()],
}

This will run optipng with the default number of trials (which is the way to go for most people).

Playing around with optimization levels

We can, of course, also specify some parameters for optipng. Please note that you're dangerously venturing out of most people-territory here.

from .optimizeimages import optipng

renders["foo"] = {
    # ... other stuff here
    "optimizeimg":[optipng(olevel=3)],
}

This will essentially execute optipng -o3 (instead of -o2), you can see what the exact meaning of that is in the optipng manual, or by typing optipng -h. Please note that anything above, or most likely even including the fifth optimization level, is pointless. The OptiPNG developers said so themselves:

It is important to mention that the achieved compression ratio is less and less likely to improve when higher-level presets (trigerring more trials) are being used. Even if the program is capable of searching automatically over more than 200 configurations (and the advanced users have access to more than 1000 configurations!), a preset that selects around 10 trials should be satisfactory for most users. Furthermore, a preset that selects between 30-40 trials should be satisfactory for all users, for it is very, very unlikely to be beaten significantly by any wider search. The rest of the trial configurations are offered rather as a curiosity (but they were used in the experimentation from which we concluded they are indeed useless!)

Getting crazy

Let's move on to something more hardcore, then. Let's say you don't care if your map looks a bit weird thanks to PIL. Let's say you want to use pngnq.

from .optimizeimages import optipng, pngnq

renders["foo"] = {
    # ... other stuff here
    "optimizeimg":[pngnq(), optipng()],
}

Note the order in which the two optimizers are specified. Here, pngnq is run first, then optipng is run on it. This is important: If you feed crushed output into something that is not a crusher (i.e. does not do lossless compression tweaking), all benefits of the crusher will be lost; essentially, you'd be wasting your time. If you choose to use more than one optimization tool, make sure the crusher is at the end. Overviewer will warn you if it isn't.

Don't be silly

Speaking of more than one optimization tool, let's answer a very important question: Will the image be smaller by using multiple crushers chained together? (i.e. optipng after pngcrush)

The short answer: No.

The long answer: Most likely not.

Crushers, by default, exit if they fail to make the image smaller, and just admit that they have not been able to crush it. Crushers may perform differently in different situations, so using two crushers chained after each other gives you essentially the best output of either; however, for most images (and I say "most images" like I say "most people"), this will be optipng in our example. Every optimizeimg configuration that uses both pngcrush and optipng can most likely be reduced to just optipng, with exactly the same results but much faster renders.

Conclusion

As you may have noticed, I often used "most likely" and "most people" in this post. This is because we can't make certain statements without large-scale tests, which I have not done, but other people did. If you're interested in utilizing image optimization, what you should take away from this blog post is that [optipng()] is the way to go. If you're not sure, do tests yourself. We may add support for better image optimizers in the future, including some for JPEG, so frequently checking the documentation to see which are available is a good idea.

(permalink)