Wherever you go, there you are.

Rotation Tribulation


I love spending hours tracking down obscure API issues that turn my carefully crafted code into a crap machine. It's always exciting, and in the end I feel a healthy sense of accomplishment for having exhausted hours of intense study in order to make something work. The way it was supposed to work in the first place. If you can't guess already, I just had another one of those wonderful sessions with PIL -- the Python Imaging Library.

Now, in it's defence, this is pretty much the first issue I've run into with PIL. Both functionally and documentation-wise, it has been exceptional all around. So I can't really complain TOO much. That won't stop me from complaining just a little, though.

All I wanted to do was rotate an image. And while PIL happily obliged with a handy "rotate" function all rearing to go, the result was... well, it was crap. It looked like something you might expect from a CS student who had just learnt matrices and was busy rotating pixels gleefully, completely oblivious to the swath of destructive artifacting in their wake.

Here are the important bits of the rotate API documentation in the PIL handbook:

im.rotate(angle, filter=NEAREST, expand=0) => image

Returns a copy of an image rotated the given number of degrees counter clockwise around its centre.

The filter argument can be one of NEAREST (use nearest neighbour), BILINEAR (linear interpolation in a 2x2 environment), or BICUBIC (cubic spline interpolation in a 4x4 environment). If omitted, or if the image has mode "1" or "P", it is set to NEAREST.

The expand argument, if true, indicates that the output image should be made large enough to hold the rotated image. If omitted or false, the output image has the same size as the input image.

Here's the tricky part it doesn't mention: if you actually use the expand option, it will completely ignore any filter you provide, and simply use the NEAREST method. Which might as well be named UGLY, because that's what you get. Oh, and nevermind the fact that the actual argument isn't even filter at all, but resample (and yes, that matters when trying to pass a named argument in Python).

The fix? Trivial, once you've spent a few hours wondering why none of the filters make any difference and poked your way through the code to find out it's happily ignoring your settings entirely (no... I'm not bitter... why do you ask?). Simply pretend the "expand" option doesn't exist, and instead manually expand the image size prior to rotating so that it can hold the rotated image. A little bit of trig and you're good as rain. Then you can happily rotate away with expand turned off, using the BICUBIC filter, which looks oh so pretty.