Jan 15, 2009


A Python Script to Make a Google Earth KMZ


This script makes thumbnails of jpg images using Python PIL, then writes a KML file based on data provided to group photos by location, finally zipping it all up into a self-contained KMZ file.


Taking photographs while traveling is a very common activity. I do it for both research and pleasure. Google Earth is a great way to showcase photographs for locations you visit, or locations in a series along a path. In Google Earth, a discrete location can be set with a placemark, for which you can provide a description. Using well-known HTML methods in the description, you can add links to photographs. Placemarks are described in a KML file. The KML file, along with the images, is zipped up into a KMZ file to complete the job, and the KMZ file can be distributed to others who simply open it to view the locations and photographs.


Here is what Google Earth looks like for a placemark made by this script:



































For this project I exported selected photos from iPhoto to a trip images folder at full image size. I used photographs from a trip to Seattle and the Olympic Peninsula of Washington in 2007. I renamed the files with a prefix olympic-001, so that filenames became olympic-003-DSC_0056.jpg, olympic-003-DSC_0057.jpg, etc. for a given location.


In the python file, I typed the prefix (e.g., olympic-003) for each location, the latitude and longitude, and a description into a series of text strings, delimited by the vertical bar (|), which works well as a data delimiter. The script first parses this data into a Python dictionary of location codes and the images for each location.


Then a 'kmz' directory is created.


And into this 'kmz' directory, into a subdirectory called 'files', are written thumbnail images for the photographs, appending '-thumb' to the file basenames.


The KML file is written to contain the placemarks for the locations. The main trick here is to embed HTML tags for the description and thumbnail image links within a CDATA-bounded description section. The thumbnail image links have the form 'files/olympic-003-DSC_0057-thumb.jpg'.


Finally, an os.system() call is made to zip up the KML file and the thumbnail images in the files directory.


Here's the script, pykmz.py:


# Version 0.1


#  pykmz is copyright 2008-2009 Jeffrey G. Pittman.


#  This file is part of pykmz.


#  pykmz is free software: you can redistribute it and/or modify

#  it under the terms of the GNU General Public License as published by

#  the Free Software Foundation, either version 3 of the License, or

#  (at your option) any later version.


#  pykmz is distributed in the hope that it will be useful,

#  but WITHOUT ANY WARRANTY; without even the implied warranty of


#  GNU General Public License for more details.


#  You should have received a copy of the GNU General Public License

#  along with pykmz.  If not, see <http://www.gnu.org/licenses/>.


import glob

import os

from PIL import Image


codes_and_photo_fns = {}

for photo_fn in glob.glob('images/*.jpg'):

    basedir,basename = os.path.split(photo_fn)

    fn_root,fn_ext = os.path.splitext(basename)

    code = '-'.join(fn_root.split('-')[:-1])

    if code not in codes_and_photo_fns:

        codes_and_photo_fns[code] = [basename]




csv_data = """olympic-001 | Port of Seattle| Washington|  47 | 36 | 12 | 122 | 20 | 56 | Leaving the Port of Seattle

olympic-002| Lake Cresent | Washington |  48 | 3 | 8.5| 123 | 50 | 2.4 | At a roadside park

olympic-003| Mora Beach | Washington|   47 | 55 | 9 | 124 | 38 | 7 | On Mora Beach

olympic-004| Mora Beach Road | Washington|  47 | 55 | 19 | 124 | 38 | 21 | Eagles on Mora Road"""


kml = []

kml.append('<?xml version=\"1.0\" encoding=\"UTF-8\"?>')

kml.append('<kml xmlns=\"http://www.opengis.net/kml/2.2\">')



for line in csv_data.split('\n'):

    line = line.strip()


    items = line.split('|')


    code = items[0].strip()

    locality = items[1].strip()

    area = items[2].strip()

    lat_deg = float(items[3])

    lat_min = float(items[4])

    lat_sec = float(items[5])

    lat = lat_deg + (lat_min/60.0) + (lat_sec/3600.0)

    lon_deg = float(items[6])

    lon_min = float(items[7])

    lon_sec = float(items[8])

    # longitude values are negative (West) for the U.S:

    lon = -(lon_deg + (lon_min/60.0) + (lon_sec/3600.0))

    description = items[9].strip()


    kml.append('  <Placemark>')

    kml.append('    <name>%s</name>' % code)

    kml.append('    <description><![CDATA[')

    kml.append('        <h1>%s</h1>' % locality)

    kml.append('        <p>%s</p>' % description)

    for photo_fn in codes_and_photo_fns[code]:

        basedir,basename = os.path.split(photo_fn)

        fn_root,fn_ext = os.path.splitext(basename)

        thumbnail_fn = fn_root + '_thumb' + fn_ext

        kml.append('        <p><img src=\"files/%s\" ></p>' % thumbnail_fn)

    kml.append('      ]]></description>')

    kml.append('    <Point>')

    kml.append('      <coordinates>%f,%f,0</coordinates>' % (lon, lat))

    kml.append('    </Point>')

    kml.append('  </Placemark>')





kml_string = '\n'.join(kml)


def check_dir(dest_dir):

    if os.path.isdir(dest_dir):

        print 'Aborting!'

        print '  (Directory ' + dest_dir + ' already exists.)'

        return False



        return True


def make_thumbnails(size, kmz_dirname, image_dirs):


    print '----------------'

    print 'Thumbnails...'


    os.mkdir(os.path.join(kmz_dirname, 'files'))

    for image_dir in image_dirs:

        for fn in glob.glob(os.path.join(image_dir, '*.jpg')):

            if 'DSC' in fn:

                basedir,basename = os.path.split(fn)

                fn_root,fn_ext = os.path.splitext(basename)

                outfile = os.path.join(kmz_dirname, 'files', fn_root + '_thumb' + fn_ext)

                print 'Wrote:', outfile

                im = Image.open(fn)

                im.thumbnail(size, Image.ANTIALIAS)



kmz_dirname = 'kmz'


project = 'olympic'


thumbnail_size = (320, 320)


if check_dir(kmz_dirname):

    make_thumbnails(thumbnail_size, kmz_dirname, ['images',])


    kmz_file = os.path.join(kmz_dirname, '%s.kmz' % project)

    kml_file = os.path.join(kmz_dirname, '%s.kml' % project)



    print '----------------'

    print 'KML file...'


    fileObj = open(kml_file, 'w' )



    print 'Wrote:', kml_file



    print '----------------'

    print 'Zipping KMZ...'



    os.system('zip -r %s.kmz *' % project)



    print 'Zipped:', kmz_file



UPDATE: Changed the name from kmzmaker to pykmz, because there is already a Java-based kmzmaker.


And here is the screen output from the script, for this example:


jeffs-macbook:pykmz jeff$ python pykmz.py





Wrote: kmz/files/olympic-001-DSC_0003_thumb.jpg

Wrote: kmz/files/olympic-002-DSC_0013_thumb.jpg

Wrote: kmz/files/olympic-003-DSC_0055_thumb.jpg

Wrote: kmz/files/olympic-003-DSC_0056_thumb.jpg

Wrote: kmz/files/olympic-003-DSC_0057_thumb.jpg

Wrote: kmz/files/olympic-003-DSC_0060_thumb.jpg

Wrote: kmz/files/olympic-004-DSC_0070_thumb.jpg

Wrote: kmz/files/olympic-004-DSC_0075_thumb.jpg



KML file...


Wrote: kmz/olympic.kml



Zipping KMZ...


  adding: files/ (stored 0%)

  adding: files/olympic-001-DSC_0003_thumb.jpg (deflated 1%)

  adding: files/olympic-002-DSC_0013_thumb.jpg (deflated 3%)

  adding: files/olympic-003-DSC_0055_thumb.jpg (deflated 1%)

  adding: files/olympic-003-DSC_0056_thumb.jpg (deflated 3%)

  adding: files/olympic-003-DSC_0057_thumb.jpg (deflated 6%)

  adding: files/olympic-003-DSC_0060_thumb.jpg (deflated 1%)

  adding: files/olympic-004-DSC_0070_thumb.jpg (deflated 5%)

  adding: files/olympic-004-DSC_0075_thumb.jpg (deflated 2%)

  adding: olympic.kml (deflated 73%)


Zipped: kmz/olympic.kmz