Cómo convertir fotos con etiquetas geográficas en PhotoOverlays de KML

Mano Marks, equipo de APIs de Google Maps
Enero de 2009

Objetivo

En este instructivo, se explica cómo usar fotos con etiquetas geográficas para crear un objeto PhotoOverlays de KML. Si bien el código de muestra está escrito en Python, existen muchas bibliotecas similares en otros lenguajes de programación, por lo que no debería haber problemas para traducir este código a otro lenguaje. El código de este artículo se basa en una biblioteca de Python de código abierto, EXIF.py.

Introducción

Las cámaras digitales son dispositivos increíbles. Muchos usuarios no se dan cuenta, pero hacen mucho más que solo tomar fotos y grabar videos. También etiquetan esos videos y fotos con metadatos sobre la cámara y su configuración. En los últimos años, las personas encontraron formas de agregar datos geográficos a esa información, ya sea incorporados por los fabricantes de cámaras, como algunas cámaras Ricoh y Nikon, o a través de dispositivos como los registradores de GPS y el EyeFi Explore. Los teléfonos con cámara, como el iPhone y los teléfonos que usan el sistema operativo Android, como el G1 de T-Mobile, incorporan esos datos automáticamente. Algunos sitios para subir fotos, como Panoramio, Álbumes web de Picasa y Flickr, analizarán automáticamente los datos de GPS y los usarán para geoetiquetar una foto. Luego, puedes recuperar esos datos en los feeds. Pero ¿dónde está la diversión en eso? En este artículo, se explica cómo acceder a esos datos por tu cuenta.

Encabezados EXIF

La forma más común de incorporar datos en un archivo de imagen es mediante el Formato de archivo de imagen intercambiable, o EXIF. Los datos se almacenan en formato binario en los encabezados EXIF de forma estándar. Si conoces la especificación de los encabezados EXIF, puedes analizarlos por tu cuenta. Afortunadamente, alguien ya hizo el trabajo difícil y escribió un módulo de Python para ti. La biblioteca de código abierto EXIF.py es una excelente herramienta para leer los encabezados de los archivos JPEG.

El código

El código de muestra de este artículo se encuentra en el archivo exif2kml.py. Si quieres comenzar a usarlo directamente, descarga ese módulo, así como EXIF.py, y colócalos en el mismo directorio. Ejecuta python exif2kml.py foo.jpg y reemplaza foo.jpg por la ruta de acceso a una foto con etiquetas geográficas. Se generará un archivo llamado test.kml.

Cómo analizar los encabezados Exif

EXIF.py proporciona una interfaz sencilla para extraer los encabezados Exif. Simplemente ejecuta la función process_file() y mostrará los encabezados como un objeto dict.

def GetHeaders(the_file):
  """Handles getting the Exif headers and returns them as a dict.

  Args:
    the_file: A file object

  Returns:
    a dict mapping keys corresponding to the Exif headers of a file.
  """

  data = EXIF.process_file(the_file, 'UNDEF', False, False, False)
  return data

Una vez que tengas los encabezados Exif, deberás extraer las coordenadas GPS. EXIF.py los trata como objetos Ratio, objetos para almacenar el numerador y el denominador de los valores. Esto establece una proporción precisa en lugar de depender de un número de punto flotante. Sin embargo, KML espera números decimales, no proporciones. Por lo tanto, extrae cada una de las coordenadas y convierte el numerador y el denominador en un solo número de punto flotante para los grados decimales:

def DmsToDecimal(degree_num, degree_den, minute_num, minute_den,
                 second_num, second_den):
  """Converts the Degree/Minute/Second formatted GPS data to decimal degrees.

  Args:
    degree_num: The numerator of the degree object.
    degree_den: The denominator of the degree object.
    minute_num: The numerator of the minute object.
    minute_den: The denominator of the minute object.
    second_num: The numerator of the second object.
    second_den: The denominator of the second object.

  Returns:
    A deciminal degree.
  """

  degree = float(degree_num)/float(degree_den)
  minute = float(minute_num)/float(minute_den)/60
  second = float(second_num)/float(second_den)/3600
  return degree + minute + second


def GetGps(data):
  """Parses out the GPS coordinates from the file.

  Args:
    data: A dict object representing the Exif headers of the photo.

  Returns:
    A tuple representing the latitude, longitude, and altitude of the photo.
  """

  lat_dms = data['GPS GPSLatitude'].values
  long_dms = data['GPS GPSLongitude'].values
  latitude = DmsToDecimal(lat_dms[0].num, lat_dms[0].den,
                          lat_dms[1].num, lat_dms[1].den,
                          lat_dms[2].num, lat_dms[2].den)
  longitude = DmsToDecimal(long_dms[0].num, long_dms[0].den,
                           long_dms[1].num, long_dms[1].den,
                           long_dms[2].num, long_dms[2].den)
  if data['GPS GPSLatitudeRef'].printable == 'S': latitude *= -1
  if data['GPS GPSLongitudeRef'].printable == 'W': longitude *= -1
  altitude = None

  try:
    alt = data['GPS GPSAltitude'].values[0]
    altitude = alt.num/alt.den
    if data['GPS GPSAltitudeRef'] == 1: altitude *= -1

  except KeyError:
    altitude = 0

  return latitude, longitude, altitude

Una vez que tengas las coordenadas, será fácil crear un objeto PhotoOverlay simple para cada foto:

def CreatePhotoOverlay(kml_doc, file_name, the_file, file_iterator):
  """Creates a PhotoOverlay element in the kml_doc element.

  Args:
    kml_doc: An XML document object.
    file_name: The name of the file.
    the_file: The file object.
    file_iterator: The file iterator, used to create the id.

  Returns:
    An XML element representing the PhotoOverlay.
  """

  photo_id = 'photo%s' % file_iterator
  data = GetHeaders(the_file)
  coords = GetGps(data)

  po = kml_doc.createElement('PhotoOverlay')
  po.setAttribute('id', photo_id)
  name = kml_doc.createElement('name')
  name.appendChild(kml_doc.createTextNode(file_name))
  description = kml_doc.createElement('description')
  description.appendChild(kml_doc.createCDATASection('<a href="#%s">'
                                                     'Click here to fly into '
                                                     'photo</a>' % photo_id))
  po.appendChild(name)
  po.appendChild(description)

  icon = kml_doc.createElement('icon')
  href = kml_doc.createElement('href')
  href.appendChild(kml_doc.createTextNode(file_name))

  camera = kml_doc.createElement('Camera')
  longitude = kml_doc.createElement('longitude')
  latitude = kml_doc.createElement('latitude')
  altitude = kml_doc.createElement('altitude')
  tilt = kml_doc.createElement('tilt')

  # Determines the proportions of the image and uses them to set FOV.
  width = float(data['EXIF ExifImageWidth'].printable)
  length = float(data['EXIF ExifImageLength'].printable)
  lf = str(width/length * -20.0)
  rf = str(width/length * 20.0)

  longitude.appendChild(kml_doc.createTextNode(str(coords[1])))
  latitude.appendChild(kml_doc.createTextNode(str(coords[0])))
  altitude.appendChild(kml_doc.createTextNode('10'))
  tilt.appendChild(kml_doc.createTextNode('90'))
  camera.appendChild(longitude)
  camera.appendChild(latitude)
  camera.appendChild(altitude)
  camera.appendChild(tilt)

  icon.appendChild(href)

  viewvolume = kml_doc.createElement('ViewVolume')
  leftfov = kml_doc.createElement('leftFov')
  rightfov = kml_doc.createElement('rightFov')
  bottomfov = kml_doc.createElement('bottomFov')
  topfov = kml_doc.createElement('topFov')
  near = kml_doc.createElement('near')
  leftfov.appendChild(kml_doc.createTextNode(lf))
  rightfov.appendChild(kml_doc.createTextNode(rf))
  bottomfov.appendChild(kml_doc.createTextNode('-20'))
  topfov.appendChild(kml_doc.createTextNode('20'))
  near.appendChild(kml_doc.createTextNode('10'))
  viewvolume.appendChild(leftfov)
  viewvolume.appendChild(rightfov)
  viewvolume.appendChild(bottomfov)
  viewvolume.appendChild(topfov)
  viewvolume.appendChild(near)

  po.appendChild(camera)
  po.appendChild(icon)
  po.appendChild(viewvolume)
  point = kml_doc.createElement('point')
  coordinates = kml_doc.createElement('coordinates')
  coordinates.appendChild(kml_doc.createTextNode('%s,%s,%s' %(coords[1],
                                                              coords[0],
                                                              coords[2])))
  point.appendChild(coordinates)

  po.appendChild(point)

  document = kml_doc.getElementsByTagName('Document')[0]
  document.appendChild(po)

Como puedes ver, solo usamos métodos estándar del DOM de W3C, ya que son los que están disponibles en la mayoría de los lenguajes de programación. Para ver cómo funciona todo en conjunto, descarga el código aquí.

Este ejemplo no aprovecha toda la potencia de PhotoOverlays, que te permite crear exploraciones detalladas de fotos de alta resolución. Sin embargo, sí muestra cómo colgar una foto al estilo de una valla publicitaria sobre Google Earth. Este es un ejemplo de un archivo KML creado con este código:

<?xml version="1.0" encoding="utf-8"?>
<kml xmlns="http://www.opengis.net/kml/2.2">
  <Document>
    <PhotoOverlay id="photo0">
      <name>
        1228258523134.jpg
      </name>
      <description>
<![CDATA[<a href="#photo0">Click here to fly into photo</a>]]>      </description>
      <Camera>
      	<longitude>
          -122.3902159196034
        </longitude>
        <latitude>
           37.78961266330473
        </latitude>
        <altitude>
          10
        </altitude>
        <tilt>
          90
        </tilt>
      </Camera>
      <Icon>
        <href>
          1228258523134.jpg
        </href>
      </Icon>
      <ViewVolume>
        <leftFov>
          -26.6666666667
        </leftFov>
        <rightFov>
          26.6666666667
        </rightFov>
        <bottomFov>
          -20
        </bottomFov>
        <topFov>
          20
        </topFov>
        <near>
          10
        </near>
      </ViewVolume>
      <Point>
        <coordinates>
          -122.3902159196034,37.78961266330473,0
        </coordinates>
      </Point>
    </PhotoOverlay>
  </Document>
</kml>

Así se ve en Google Earth:


Precauciones

La geoetiquetación de fotos aún está en sus inicios.

Estos son algunos aspectos que debes tener en cuenta:

  • Los dispositivos GPS no siempre son 100% precisos, en especial los que vienen en las cámaras, por lo que debes verificar las posiciones de tus fotos.
  • Muchos dispositivos no registran la altitud y, en su lugar, la establecen en 0. Si la altitud es importante para ti, debes buscar otra forma de capturar esos datos.
  • La posición del GPS es la de la cámara, no la del sujeto de la foto. Por eso, este ejemplo posiciona el elemento Camera en la posición del GPS y la foto real lejos de esa posición.
  • Los datos Exif no capturan información sobre la dirección a la que apunta la cámara, por lo que es posible que debas ajustar tu PhotoOverlays por ese motivo. La buena noticia es que algunos dispositivos, como los teléfonos basados en el sistema operativo Android, te permiten capturar datos como la dirección de la brújula y la inclinación directamente, pero no en los encabezados Exif.

Dicho esto, sigue siendo una forma poderosa de visualizar tus fotos. Esperamos que, en el futuro cercano, veamos un etiquetado geográfico de fotos cada vez más preciso.

Próximos pasos

Ahora que comenzaste a usar los encabezados EXIF, puedes explorar la especificación EXIF. Allí se almacenan muchos otros datos que te pueden interesar para capturarlos y colocarlos en un globo de descripción. También puedes considerar la posibilidad de crear objetos PhotoOverlays más enriquecidos con ImagePyramids. El artículo de la Guía para desarrolladores sobre PhotoOverlays ofrece una buena descripción general de cómo usarlos.