Chuyển đổi ảnh được gắn thẻ địa lý thành lớp phủ ảnh KML

Mano Marks, Nhóm API địa lý của Google
Tháng 1 năm 2009

Mục tiêu

Hướng dẫn này sẽ hướng dẫn bạn cách sử dụng ảnh được gắn thẻ địa lý để tạo tệp KML PhotoOverlays. Mặc dù mã mẫu được viết bằng Python, nhưng có nhiều thư viện tương tự trong các ngôn ngữ lập trình khác, nên việc dịch mã này sang một ngôn ngữ khác sẽ không gặp vấn đề gì. Mã trong bài viết này dựa trên một thư viện Python nguồn mở là EXIF.py.

Giới thiệu

Máy ảnh kỹ thuật số là một thiết bị khá tuyệt vời. Nhiều người dùng không nhận ra rằng họ làm nhiều việc hơn là chỉ chụp ảnh và quay video. Họ cũng gắn thẻ những video và ảnh đó bằng siêu dữ liệu về camera và chế độ cài đặt của camera. Trong vài năm qua, mọi người đã tìm ra cách thêm dữ liệu địa lý vào thông tin đó, có thể là do nhà sản xuất máy ảnh nhúng (chẳng hạn như một số máy ảnh Ricoh và Nikon) hoặc thông qua các thiết bị như thiết bị ghi nhật ký GPS và EyeFi Explore. Điện thoại có camera như iPhone và điện thoại sử dụng hệ điều hành Android (chẳng hạn như G1 của T-Mobile) sẽ tự động nhúng dữ liệu đó. Một số trang web tải ảnh lên, chẳng hạn như Panoramio, Album Web PicasaFlickr, sẽ tự động phân tích dữ liệu GPS và dùng dữ liệu đó để gắn thẻ địa lý cho ảnh. Sau đó, bạn có thể lấy lại dữ liệu đó trong nguồn cấp dữ liệu. Nhưng như vậy thì có gì vui? Bài viết này trình bày cách tự lấy dữ liệu đó.

Tiêu đề EXIF

Cách phổ biến nhất để nhúng dữ liệu vào tệp hình ảnh là sử dụng Định dạng tệp hình ảnh có thể trao đổi (EXIF). Dữ liệu được lưu trữ ở dạng nhị phân trong tiêu đề EXIF theo cách tiêu chuẩn. Nếu biết quy cách cho tiêu đề EXIF, bạn có thể tự phân tích các tiêu đề đó. Rất may là đã có người làm việc này và viết một mô-đun Python cho bạn. Thư viện nguồn mở EXIF.py là một công cụ tuyệt vời để đọc tiêu đề của tệp JPEG.

Mã mẫu cho bài viết này nằm trong tệp exif2kml.py. Nếu bạn muốn chuyển thẳng đến bước sử dụng, hãy tải mô-đun đó xuống, cũng như EXIF.py và đặt chúng vào cùng một thư mục. Chạy python exif2kml.py foo.jpg thay thế foo.jpg bằng đường dẫn đến một bức ảnh được gắn thẻ địa lý. Thao tác này sẽ tạo ra một tệp có tên là test.kml.

Phân tích cú pháp tiêu đề Exif

EXIF.py cung cấp một giao diện dễ dàng để trích xuất các tiêu đề Exif. Bạn chỉ cần chạy hàm process_file() và hàm này sẽ trả về các tiêu đề dưới dạng đối tượng 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

Sau khi có tiêu đề Exif, bạn cần trích xuất toạ độ GPS. EXIF.py coi những giá trị này là các đối tượng Ratio, tức là các đối tượng dùng để lưu trữ tử số và mẫu số của các giá trị. Thao tác này thiết lập một tỷ lệ chính xác thay vì dựa vào số có dấu phẩy động. Tuy nhiên, KML yêu cầu số thập phân, chứ không phải tỷ lệ. Vì vậy, bạn sẽ trích xuất từng toạ độ và chuyển đổi tử số và mẫu số thành một số thực duy nhất cho độ thập phân:

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

Sau khi có toạ độ, bạn có thể dễ dàng tạo một PhotoOverlay đơn giản cho từng bức ảnh:

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)

Bạn có thể thấy chúng ta chỉ đang sử dụng các phương thức W3C DOM tiêu chuẩn, vì đây là những phương thức có trong hầu hết các ngôn ngữ lập trình. Để xem cách mọi thứ kết hợp với nhau, hãy tải mã xuống tại đây.

Mẫu này không tận dụng hết sức mạnh của PhotoOverlays, cho phép bạn tạo các khám phá sâu về ảnh có độ phân giải cao. Tuy nhiên, video này minh hoạ cách treo ảnh theo kiểu biển quảng cáo trên Google Earth. Sau đây là ví dụ về một tệp KML được tạo bằng mã này:

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

Đây là hình ảnh minh hoạ trong Google Earth:


Những điều cần lưu ý

Tính năng gắn thẻ địa lý cho ảnh vẫn đang trong giai đoạn đầu phát triển.

Sau đây là một số điều bạn cần lưu ý:

  • Thiết bị GPS không phải lúc nào cũng chính xác 100%, đặc biệt là những thiết bị có trong camera. Vì vậy, bạn nên kiểm tra vị trí của ảnh.
  • Nhiều thiết bị không theo dõi độ cao mà thay vào đó đặt độ cao thành 0. Nếu độ cao là thông tin quan trọng đối với bạn, thì bạn nên tìm cách khác để ghi lại dữ liệu đó.
  • Vị trí GPS là vị trí của camera, chứ không phải vị trí của đối tượng trong ảnh. Đó là lý do khiến mẫu này đặt phần tử Camera ở vị trí GPS và ảnh thực tế ở vị trí khác.
  • Exif không ghi lại thông tin về hướng mà camera của bạn đang hướng đến, vì vậy, bạn có thể cần điều chỉnh PhotoOverlays vì lý do đó. Tin vui là một số thiết bị (chẳng hạn như điện thoại chạy hệ điều hành Android) cho phép bạn chụp trực tiếp các dữ liệu như hướng la bàn và độ nghiêng, chỉ là không có trong tiêu đề Exif.

Mặc dù vậy, đây vẫn là một cách hiệu quả để hình dung ảnh của bạn. Hy vọng rằng trong tương lai gần, chúng ta sẽ thấy ngày càng nhiều bức ảnh được gắn thẻ địa lý chính xác hơn.

Nội dung tiếp theo nên tìm hiểu

Giờ đây, khi đã bắt đầu sử dụng tiêu đề EXIF, bạn có thể khám phá thông số kỹ thuật EXIF. Có rất nhiều dữ liệu khác được lưu trữ ở đó và bạn có thể muốn thu thập dữ liệu đó, đưa vào một bong bóng mô tả. Bạn cũng có thể cân nhắc việc tạo PhotoOverlays phong phú hơn bằng cách sử dụng ImagePyramids. Bài viết trên PhotoOverlays trong Hướng dẫn dành cho nhà phát triển có phần tổng quan hữu ích về cách sử dụng các thành phần này.