위치정보가 태그된 사진을 KML PhotoOverlay로 변환

Mano Marks, Google Geo API팀
2009년 1월

목표

이 튜토리얼에서는 위치정보 태그가 지정된 사진을 사용하여 KML PhotoOverlays을 만드는 방법을 알아봅니다. 샘플 코드는 Python으로 작성되었지만 다른 프로그래밍 언어에도 비슷한 라이브러리가 많이 있으므로 이 코드를 다른 언어로 변환하는 데는 문제가 없을 것입니다. 이 도움말의 코드는 오픈소스 Python 라이브러리인 EXIF.py를 사용합니다.

소개

디지털 카메라는 정말 놀라운 물건입니다. 많은 사용자가 인식하지 못하지만 사진과 동영상만 찍는 것이 아닙니다. 또한 카메라 및 설정에 관한 메타데이터로 동영상과 사진에 태그를 지정합니다. 지난 몇 년 동안 사람들은 일부 Ricoh 및 Nikon 카메라와 같이 카메라 제조업체에서 삽입한 지리 데이터를 해당 정보에 추가하거나 GPS 로거 및 EyeFi Explore와 같은 기기를 통해 추가하는 방법을 찾아냈습니다. iPhone과 같은 카메라 휴대전화와 Android 운영체제를 사용하는 휴대전화(예: T-Mobile의 G1)는 이 데이터를 자동으로 삽입합니다. Panoramio, Picasa 웹 앨범, Flickr와 같은 일부 사진 업로드 사이트에서는 GPS 데이터를 자동으로 파싱하여 사진에 위치정보 태그를 지정합니다. 그런 다음 피드에서 해당 데이터를 다시 가져올 수 있습니다. 하지만 그러면 재미가 없잖아. 이 도움말에서는 이러한 데이터를 직접 확인하는 방법을 살펴봅니다.

Exif 헤더

이미지 파일에 데이터를 삽입하는 가장 일반적인 방법은 교환 가능한 이미지 파일 형식(EXIF)을 사용하는 것입니다. 데이터는 표준 방식으로 EXIF 헤더에 바이너리 형식으로 저장됩니다. EXIF 헤더의 사양을 알고 있다면 직접 파싱할 수 있습니다. 다행히도 누군가가 이미 힘든 작업을 수행하여 Python 모듈을 작성했습니다. EXIF.py 오픈소스 라이브러리는 JPEG 파일의 헤더를 읽는 데 유용한 도구입니다.

코드

이 도움말의 샘플 코드는 exif2kml.py 파일에 있습니다. 바로 사용하려면 해당 모듈과 EXIF.py를 다운로드하여 동일한 디렉터리에 배치하세요. python exif2kml.py foo.jpg를 실행하여 foo.jpg를 위치정보 태그가 지정된 사진의 경로로 바꿉니다. 그러면 test.kml이라는 파일이 생성됩니다.

Exif 헤더 파싱

EXIF.py는 Exif 헤더를 추출하기 위한 간편한 인터페이스를 제공합니다. process_file() 함수를 실행하면 헤더가 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

Exif 헤더가 있으면 GPS 좌표를 추출해야 합니다. EXIF.py는 이를 값의 분자와 분모를 저장하는 객체인 Ratio 객체로 처리합니다. 이렇게 하면 부동 소수점 수에 의존하지 않고 정확한 비율이 설정됩니다. 하지만 KML은 비율이 아닌 십진수를 예상합니다. 따라서 각 좌표를 추출하고 분자와 분모를 십진수 도의 단일 부동 소수점 숫자로 변환합니다.

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

좌표가 있으면 각 사진에 대해 간단한 PhotoOverlay를 쉽게 만들 수 있습니다.

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)

대부분의 프로그래밍 언어에서 사용할 수 있는 표준 W3C DOM 메서드만 사용하고 있습니다. 전체 코드를 확인하려면 여기에서 코드를 다운로드하세요.

이 샘플에서는 고해상도 사진을 심층적으로 탐색할 수 있는 PhotoOverlays의 모든 기능을 활용하지 않습니다. 하지만 Google 어스에 광고판 스타일로 사진을 걸어두는 방법을 보여줍니다. 다음은 이 코드를 사용하여 만든 KML 파일의 샘플입니다.

<?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>

Google 어스에서는 다음과 같이 표시됩니다.


주의점

사진에 위치정보를 태그하는 기능은 아직 초기 단계입니다.

다음은 유의해야 할 사항입니다.

  • GPS 기기는 항상 100% 정확하지는 않으며 특히 카메라에 내장된 GPS 기기는 더욱 그렇습니다. 따라서 사진의 위치를 확인해야 합니다.
  • 많은 기기에서 고도를 추적하지 않고 0으로 설정합니다. 고도가 중요한 경우 해당 데이터를 캡처할 다른 방법을 찾아야 합니다.
  • GPS 위치는 사진의 피사체가 아닌 카메라의 위치입니다. 따라서 이 샘플에서는 카메라 요소를 GPS 위치에 배치하고 실제 사진은 해당 위치에서 멀리 떨어져 있습니다.
  • Exif는 카메라가 향하는 방향에 관한 정보를 캡처하지 않으므로 이로 인해 PhotoOverlays를 조정해야 할 수 있습니다. 다행히 Android 운영체제를 기반으로 빌드된 휴대전화와 같은 일부 기기에서는 Exif 헤더가 아닌 나침반 방향 및 기울기와 같은 데이터를 직접 캡처할 수 있습니다.

하지만 여전히 사진을 시각화하는 강력한 방법입니다. 조만간 더 정확한 사진의 위치정보 태그가 제공되기를 바랍니다.

다음 단계

EXIF 헤더를 사용하기 시작했으므로 EXIF 사양을 살펴볼 수 있습니다. 여기에 저장된 다른 데이터도 많으므로 이를 캡처하여 설명 풍선에 넣는 데 관심이 있을 수 있습니다. ImagePyramids를 사용하여 더 풍부한 PhotoOverlays를 만드는 것도 고려해 볼 수 있습니다. PhotoOverlays에 관한 개발자 가이드 문서에 사용 방법이 잘 설명되어 있습니다.