Conversão de fotos com geo-tags em PhotoOverlays de KML

Mano Marks, equipe de APIs do Google Geo
Janeiro de 2009

Objetivo

Este tutorial ensina como usar fotos com geo-tags para criar KML PhotoOverlays. Embora o exemplo de código esteja escrito em Python, muitas bibliotecas semelhantes existem em outras linguagens de programação. Por isso, não é um problema traduzir esse código para outra linguagem. O código deste artigo depende de uma biblioteca Python de código aberto, EXIF.py.

Introdução

Câmeras digitais são coisas incríveis. Muitos usuários não percebem, mas fazem mais do que apenas tirar fotos e gravar vídeos. Eles também marcam esses vídeos e fotos com metadados sobre a câmera e suas configurações. Nos últimos anos, as pessoas descobriram maneiras de adicionar dados geográficos a essas informações, sejam incorporadas pelos fabricantes das câmeras, como algumas câmeras Ricoh e Nikon, ou por dispositivos como registradores de GPS e o EyeFi Explore. Smartphones com câmera, como o iPhone, e aqueles que usam o sistema operacional Android, como o G1 da T-Mobile, incorporam esses dados automaticamente. Alguns sites de upload de fotos, como o ChromeVox, os Álbuns do Picasa e o Flickr, analisam os dados do GPS automaticamente e os utilizam para aplicar uma geo-tag a uma foto. Você pode recuperar esses dados nos feeds. Mas como isso é divertido? Este artigo mostra como acessar esses dados por conta própria.

Cabeçalhos EXIF

A forma mais comum de incorporar dados a um arquivo de imagem é usar o Exchangeable Image File Format, ou EXIF. Os dados são armazenados de forma binária nos cabeçalhos EXIF de maneira padrão. Se você conhece a especificação para cabeçalhos EXIF, pode analisá-los por conta própria. Alguém já fez esse trabalho e escreveu um módulo em Python para você. A biblioteca de código aberto EXIF.py é uma ótima ferramenta para ler os cabeçalhos de um arquivo JPEG.

O código

O exemplo de código para este artigo está neste arquivo: exif2kml.py. Se você quiser pular diretamente para o uso, faça o download desse módulo, assim como EXIF.py, e coloque-os no mesmo diretório. Execute python exif2kml.py foo.jpg, substituindo foo.jpg pelo caminho para uma foto com geo-tag. Ele vai produzir um arquivo chamado test.kml.

Análise dos cabeçalhos Exif

O EXIF.py oferece uma interface fácil para extrair os cabeçalhos Exif. Basta executar a função process_file(), e ela retornará os cabeçalhos como um 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

Quando tiver os cabeçalhos Exif, será necessário extrair as coordenadas de GPS. O EXIF.py os trata como objetos Ratio, objetos para armazenar o numerador e o denominador dos valores. Isso configura uma proporção precisa em vez de depender de um número de ponto flutuante. No entanto, KML requer números decimais, não proporções. Então, você extrai cada uma das coordenadas e converte o numerador e o denominador em um único número de ponto flutuante para graus decimais:

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

Com as coordenadas, é fácil criar um PhotoOverlay simples 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)

Estamos usando o método padrão W3C DOM, que está disponível na maioria das linguagens de programação. Para ver como tudo se encaixa, faça o download do código aqui.

Esta amostra não aproveita toda a capacidade do PhotoOverlays, o que permite criar análises detalhadas de fotos em alta resolução. No entanto, ela mostra como pendurar uma foto em estilo outdoor no Google Earth. Veja um exemplo de um arquivo KML criado usando 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>

Veja como ele fica no Google Earth:


Cuidados

As fotos com geo-tags ainda estão na infância.

Há uma série de observações importantes:

  • Os dispositivos GPS nem sempre são 100% precisos, especialmente aqueles que vêm em câmeras. Por isso, verifique as posições das suas fotos.
  • Muitos dispositivos não monitoram a altitude, mas não são definidos como 0. Se a altitude for importante para você, encontre outra maneira de capturar esses dados.
  • A posição do GPS é a posição da câmera, não o objeto da foto. É por isso que esta amostra posiciona o elemento Camera na posição GPS e a foto real longe dessa posição.
  • O Exif não captura informações sobre a direção que sua câmera está apontando, então talvez seja necessário ajustar o PhotoOverlays por isso. A boa notícia é que alguns dispositivos, como smartphones criados no sistema operacional Android, permitem capturar dados como a direção e a inclinação da bússola diretamente, mas não nos cabeçalhos Exif.

No entanto, esta ainda é uma forma eficiente de visualizar suas fotos. Esperamos ver uma marcação geográfica cada vez mais precisa de fotos em breve.

O que fazer depois disso

Agora que você começou a usar cabeçalhos EXIF, é possível explorar as especificações EXIF (link em inglês). Há muitos outros dados armazenados lá, e você pode ter interesse em capturá-los em um balão de descrição. Você também pode criar uma PhotoOverlays mais rica usando ImagePyramids. O artigo do Guia do desenvolvedor sobre PhotoOverlays tem uma boa visão geral sobre como usá-los.