Image resizing tips general and for python

Christian Harms's picture

Generating thumbnails from a collection of pictures is an easy task and there are many tools for the job. But I didn't find a tool who can clip out a centered box of my pictures to get quadratic thumbs of my originals. Because I am a developer and I dont want to try out many tools: I will hack a short python script (with help of the PIL) myself.

simple solution

The following python script can be find on many webpages. It will take all command line arguments as image filenames and convert it to 200x200 thumbnails with the default extension "_thumb.jpg" (build from the original filename):

  1. import Image, os, sys
  2.  
  3. for filename in sys.argv[1:]:
  4.     img = Image.open(filename).resize( (200,200) )
  5.     out = file(os.path.splitext(filename)[0]+"_thumb.jpg", "w")
  6.     try:
  7.         img.save(out, "JPEG")
  8.     finally:
  9.         out.close()


With the command line tool find all images and converted:

  1. find . -regex .*jpg | xargs python resize.py

But the result is not ok:

  • the only good part: the result image is 200x200 pixel
  • aspect ratio ignored
  • ugly quality
  • performance bad: 1.025 sec (example image on my eeeBox)
  • no value to choose the jpg quality for saving

For my web gallery I need identical thumbnails for a clean design and the aspect ratio should not be ignored!

Challenge: Find the biggest box with correct aspect ratio and clip the image, resize it fast and with the best quality to the given thumbnail size!

resize tips you should know

When choosing an image libary (with python is the build-in variant PIL good enough, but Samuel will add an article for java image libraries) for resizing images try to find out the following three parts:

choose the right algorythm

There should be a option to choose the image transforming algorithm. "Fast and ugly" or "slow and high quality".

With the python imaging library there are included algorithm for image translations (ordered from fastest to best quality): NEAREST, BILINEAR, BICUBIC, ANTIALIAS. If you resize an image with NEAREST the quality will be poor but the difference in calculation time to ANTIALIAS is factor 3 (depending on images). The first example has the NEAREST as default value.

  1. img = Image.open(filename).resize( (200,200), Image.ANTIALIAS)

Resizing the image with the best algorithm will produce a good quality, but needs more than 3 sec.

use two resize steps

The trick for resizing an 1600x1600 image to 200x200:

  • first resize it from 1600x1600 to double the target size (400x400) with the fastest allgorithm
  • resize the smaller image to the target resolution with the best algorithm

You get the fast transformation for the big amount and the last step to the target resolution with the best quality.

  1. img = Image.open(filename).resize( (400,400) )
  2. img = img.resize( (200,200), Image.ANTIALIAS)

This 2-pass resize part runs in exactly the same time (1.02 sec) like the first example with better quality.

resize vs. thumbnail

Because our picture collection contain 90% JPEG there is an other optimization step. To resize JPEGs you dont need all pixels for downsizing with factor 8. Its adequate to read only the starting point of an minimum coded Unit (MCU -> wikipedia). You have to try out if it is supported of your image lib - the transformation time is the answer.

The python image library offer this option with the Image.thumbnail-method. An other aspect is that the thumbnail parameter mean a target box size instead the target image size - so the aspect ratio will be saved.

  1. img = Image.open(filename)
  2. img.thumbnail( (200,200) )

The thumbnail method works on the image object and dont produce a new image object. Its faster: 0.25 sec.

  1. img = Image.open(filename)
  2. img.thumbnail( (400,400) )
  3. img.thumbnail( (200,200), Image.ANTIALIAS)

The two-pass variant is slower but has better quality on the edges. Its runs 0.42 sec.

other problems

You should be careful by loading every image. I can produce a pixel bomb by saving an 40000x40000 pixel black picture as a png. This image file is not expensive but your memory will not hold the complete bitmap. Check the resolution or have an idea to handle the OutOfMemory-exception - before loading!

There are many corrupt example images. Try these images to check if your image library is robust.

benchmark for the different steps

  • 1.02 sec: direct resize with bad quality
  • over 3 sec: direct resize with best quality
  • 1.02 sec: two resize steps with best quality
  • 0.25 sec: direct thumbnail call with bad quality
  • 0.42 sec: two thumbnail calls with best quality

This is factor 8 for different, best quality resize operation and worth for code-checking. If you plan to resize images on-the-fly for webpages there are other technics than these basic tips ...

Finding the center box

I will construct two extreme examples for defining the clipping algorithm. If the two examples works the quadratic thumbnail work too.

Get from a portait picture a landscape thumb

  • Original: bmw.jpg (2048x3072 pixel)
  • thumbnail-box: 200x100 pixel

The clipping algorithm should find the centered box:

  • (0, 1024, 2048, 2048).
  • This picture is ideal for a wide thumbnail because the dominant radiator grill of the old bmw is THE part of the picture!


    Get from a landscape picture a portait thumb

    • Second Example: ford.jpg (3072x2048 pixel)
    • thumbnail-box: 100x200 pixel

    The clipping algorithm should find the centered box:

  • (1024, 515, 2048, 2562).
  • This old ford T-Model image has to many people on the image - a portrail thumbnail crop some from the front.

    the complete function

    1. def resize(img, box, fit, out):
    2.     '''Downsample the image.
    3.    @param img: Image -  an Image-object
    4.    @param box: tuple(x, y) - the bounding box of the result image
    5.    @param fix: boolean - crop the image to fill the box
    6.    @param out: file-like-object - save the image into the output stream
    7.    '''
    8.     #preresize image with factor 2, 4, 8 and fast algorithm
    9.     factor = 1
    10.     while img.size[0]/factor > 2*box[0] and img.size[1]*2/factor > 2*box[1]:
    11.         factor *=2
    12.     if factor > 1:
    13.         img.thumbnail((img.size[0]/factor, img.size[1]/factor), Image.NEAREST)
    14.  
    15.     #calculate the cropping box and get the cropped part
    16.     if fit:
    17.         x1 = y1 = 0
    18.         x2, y2 = img.size
    19.         wRatio = 1.0 * x2/box[0]
    20.         hRatio = 1.0 * y2/box[1]
    21.         if hRatio > wRatio:
    22.             y1 = int(y2/2-box[1]*wRatio/2)
    23.             y2 = int(y2/2+box[1]*wRatio/2)
    24.         else:
    25.             x1 = int(x2/2-box[0]*hRatio/2)
    26.             x2 = int(x2/2+box[0]*hRatio/2)
    27.         img = img.crop((x1,y1,x2,y2))
    28.  
    29.     #Resize the image with best quality algorithm ANTI-ALIAS
    30.     img.thumbnail(box, Image.ANTIALIAS)
    31.  
    32.     #save it into a file-like object
    33.     img.save(out, "JPEG", quality=75)
    34. #resize
    Additional tip: Cropping from the smaller image will save memory and memcopy-operation.

    • Line 13: fast pre-resize
    • Line 27: cropping the pre-sized image with the calculcated cropping values
    • Line 30: resize the image to the target box (aspecti ratio with using of method thumbnail is considerd).
    • Line 33: Saving a thumbnail with 75% is acceptable for the quality

    Download area

    • the complete script with doctest and command line option is includet here in the article for free download.
    • the bmw and ford t model pictures are taken while the Leo Classic 2009 (Leonberg - Germany) and are for free use (download here).

    How to do this in java

    The questions for efficent and high quality resizing with java libs is open for the next article ... be free to give improvements in the comments or trackbacks!

    Comments

    Anonymous's picture

    Hi, Christian
    Which python image library did you use in your post? When I try to run your example and the following was got:
    ImportError: No module named Image

    Christian Harms's picture

    On ubuntu 10 and python 2.6 it's included: if not try to install the python image library http://www.pythonware.com/products/pil/

    emmi's picture

    Where can I download your complete script (mentioned as the first bullet point under the "Download area" section)?

    Thanks.

    KOS_MOS's picture

    use

    1.         if hRatio > wRatio:
    2.             y1 = int(y2/2-box[1]*wRatio/2)
    3.             y2 = int(y2/2+box[1]*wRatio/2)
    4.         else:
    5.             x1 = int(x2/2-box[0]*hRatio/2)
    6.             x2 = int(x2/2+box[0]*hRatio/2)

    because this variables can be float and this call exception

    1.         if hRatio > wRatio:
    2.             y1 = int(y2/2-box[1]*wRatio/2)
    3.             y2 = int(y2/2+box[1]*wRatio/2)
    4.         else:
    5.             x1 = int(x2/2-box[0]*hRatio/2)
    6.             x2 = int(x2/2+box[0]*hRatio/2)

    Sorry for bad English =)

    KOS_MOS's picture

    Sorry, source of exception is

    1. Traceback (most recent call last):
    2.   File "C:\Projects\beatimpulse\test.py", line 66, in <module>
    3.     img = resize(Image.open(path), (450, 450), True, None)
    4.   File "C:\Projects\beatimpulse\test.py", line 59, in resize
    5.     img.thumbnail(box, Image.ANTIALIAS)
    6.   File "C:\Python27\lib\site-packages\PIL\Image.py", line 1561, in thumbnail
    7.     self.load()
    8.   File "C:\Python27\lib\site-packages\PIL\Image.py", line 1707, in load
    9.     self.im = self.im.crop(self.__crop)
    10. TypeError: integer argument expected, got float

    Christian Harms's picture

    I think all my test images had even dimensions ... thank for the comment

    Nik's picture

    I solved this by changing only one string in code of resize() function. I converted all values to integer:
    img = img.crop((int(x1),int(y1),int(x2),int(y2)))

    TGM's picture

    I'm still a little new with this, and using this to make multiple thumbs

    1. #!/usr/bin/env python3
    2. from PIL import Image
    3.  
    4. class thumbnail(object):
    5.     '''Creates an object representative of an image,
    6.    allowing it to be saved out as a thumbnail of various
    7.    sizes.'''
    8.     def __init__(self, filename):
    9.         self.__init__
    10.         self._filename = filename
    11.        
    12.     def saveThumbnail(self, filename, size):          
    13.         i = Image.open(self._filename)
    14.         x, y = i.size
    15.         mx, my = (x / size[0], y / size[1])
    16.  
    17.         # Acknowledge aspect ratio for max size of 512
    18.         if (mx < my):
    19.             mx = my
    20.         else:
    21.             my = mx
    22.         _image = i.resize((int(x / mx), int(y / my)), Image.BICUBIC)
    23.         _image.save(filename)

    DirkR's picture

    Hi Christian,

    thanks for the great article! However, when using your method to generate images for a thumbnail composite page I got one pixel white lines around some images. I didn't fully investigate what was going on, as the fix was very simple: replace the second .thumbnail() with a .resize(). That fixed it for me, all thumbnails completely fill their images. Also, if you have very small images, thumbnail will not scale images up (it will leave them unchanged), so a resize fixes that one, too.

    Just for aesthetic reasons: if anybody needs smoother images or if your images have very fine structures you can play with the 2* in the factor determination. I ended up using 8*, which worked well enough to give me good results without being too expensive.

    Hope it helps!

    Dirk