Thursday, July 10, 2014

48. Creating an Atlas


We can create an atlas with many images.




This way we don't have to load a lot of small images. An atlas image will have a text file indicating the names of the individual images and their positions and sizes.




In Python 2.7 Kivy, there is a file called atlas.py. It can be used to create atlas files. The reason it is version 2.7 and not Python 3 versions is because of an old library, PIL, which stands for Python Imaging Library. The Kivy developers are correcting this, for future versions, so it does not depend on PIL.




Assuming you have installed both Python 2.7 and also PIL, the slightly modified file atlas.py file can be used, and which is on blogspot. You can have different python versions installed in your system. When Python 2.7 is installed, it will be installed to C:\\Python27. During the PIL setup, make sure it indicates the correct directory.


# atlas.py

import os, json
from os.path import basename, dirname, join, splitext
import Image


def create(outname, filenames, size, padding=2):
    if isinstance(size, (tuple, list)):
        size_w, size_h = map(int, size)
    else:
        size_w = size_h = int(size)

    # open all of the images
    ims = [(f, Image.open(f)) for f in filenames]

    # sort by image area
    ims = sorted(ims, key=lambda im: im[1].size[0] * im[1].size[1],
                 reverse=True)

    # free boxes are empty space in our output image set
    # the freebox tuple format is: outidx, x, y, w, h
    freeboxes = [(0, 0, 0, size_w, size_h)]
    numoutimages = 1

    # full boxes are areas where we have placed images in the atlas
    # the full box tuple format is: image, outidx, x, y, w, h, filename
    fullboxes = []

    # do the actual atlasing by sticking the largest images we can
    # have into the smallest valid free boxes
    for imageinfo in ims:
        im = imageinfo[1]
        imw, imh = im.size
        imw += padding
        imh += padding
        if imw > size_w or imh > size_h:
            print(
                'Atlas: image %s is larger than the atlas size!' %
                imageinfo[0])
            return

        inserted = False
        while not inserted:
            for idx, fb in enumerate(freeboxes):
                # find the smallest free box that will contain this image
                if fb[3] >= imw and fb[4] >= imh:
                    # we found a valid spot! Remove the current
                    # freebox, and split the leftover space into (up to)
                    # two new freeboxes
                    del freeboxes[idx]
                    if fb[3] > imw:
                        freeboxes.append((
                            fb[0], fb[1] + imw, fb[2],
                            fb[3] - imw, imh))

                    if fb[4] > imh:
                        freeboxes.append((
                            fb[0], fb[1], fb[2] + imh,
                            fb[3], fb[4] - imh))

                    # keep this sorted!
                    freeboxes = sorted(freeboxes,
                                       key=lambda fb: fb[3] * fb[4])
                    fullboxes.append((im,
                                      fb[0], fb[1] + padding,
                                      fb[2] + padding, imw - padding,
                                      imh - padding, imageinfo[0]))
                    inserted = True
                    break

            if not inserted:
                # oh crap - there isn't room in any of our free
                # boxes, so we have to add a new output image
                freeboxes.append((numoutimages, 0, 0, size_w, size_h))
                numoutimages += 1

    # now that we've figured out where everything goes, make the output
    # images and blit the source images to the approriate locations
    print('Atlas: create an {0}x{1} rgba image'.format(size_w,
                                                             size_h))
    outimages = [Image.new('RGBA', (size_w, size_h))
                 for i in range(0, int(numoutimages))]
    for fb in fullboxes:
        x, y = fb[2], fb[3]
        out = outimages[fb[1]]
        out.paste(fb[0], (fb[2], fb[3]))
        w, h = fb[0].size
        if padding > 1:
            out.paste(fb[0].crop((0, 0, w, 1)), (x, y - 1))
            out.paste(fb[0].crop((0, h - 1, w, h)), (x, y + h))
            out.paste(fb[0].crop((0, 0, 1, h)), (x - 1, y))
            out.paste(fb[0].crop((w - 1, 0, w, h)), (x + w, y))

    # save the output images
    for idx, outimage in enumerate(outimages):
        outimage.save('%s-%d.png' % (outname, idx))

    # write out an json file that says where everything ended up
    meta = {}
    for fb in fullboxes:
        fn = '%s-%d.png' % (basename(outname), fb[1])
        if fn not in meta:
            d = meta[fn] = {}
        else:
            d = meta[fn]

        # fb[6] contain the filename
        # for example, '../data/tiles/green_grass.png'
        # just get only 'green_grass' as the uniq id.
        uid = splitext(basename(fb[6]))[0]

        x, y, w, h = fb[2:6]
        d[uid] = x, size_h - y - h, w, h

    outfn = '%s.atlas' % outname
    with open(outfn, 'w') as fd:
        json.dump(meta, fd)

    return outfn, meta




A file, create_atlas.py, uses the file atlas.py to create atlases. These are the imports. The __future__ import is only for the print statements. Beside atlas.py module, the sys and os modules are needed, for system and operating system functions.




We require 3 parameters, in this python program. The outname indicates the name, of the atlas file. The variable im_ext, indicates what kind of image files, we have. Finally the size, indicates the size, of the resulting image files. Here, you could also have put in 256, and that would be understood to be 256, 256.




In the list fils, we have the names of all the files in current directory. This will include both image and non-image files. Next in filenames, only the elements that are actually images, with correct extension, are retained.




The only function, in the modified atlas module, is create() and, which requires 3 parameters. The default padding, which is 2 pixels, between images was used.




Finally, there are printouts, indicating the name of the main atlas file, which is the text file in json format. It also indicates how many images are in the atlas. If the size is large enough, it should be only 1 file.


# create_atlas.py

from __future__ import print_function
import atlas
import sys, os

outname = 'abc'
im_ext = '.png'
size = 256, 256

fils = os.listdir(os.getcwd())
filenames = [fil for fil in fils
             if fil.endswith(im_ext)]

ret = atlas.create(outname, filenames, size)
if not ret:
    print('Error while creating atlas!')
    sys.exit(1)

fn, meta = ret
print('Atlas created at', fn)
print('%d image%s created' % (len(meta),
        's' if len(meta) > 1 else ''))



Here, we can see the text file, abc.atlas has been created, as well as the image file, abc-0.png. This image contains the original three images.




This shows a part of abc.atlas. First, we have the image filename, and then the images and bounding boxes. Note, this bounding box is 64 by 64, the size of the image.




4 comments:

  1. Python:

    Good Post! Thank you so much for sharing this pretty post, it was so good to read and useful to improve my knowledge as updated one, keep blogging.

    https://www.emexotechnologies.com/online-courses/python-training-in-electronic-city/

    ReplyDelete
  2. Good Post! Thank you so much for sharing this pretty post, it was so good to read and useful to improve my knowledge as updated one, keep blogging.

    Python Training in electronic city

    ReplyDelete