将地理标记的照片转换为 KML PhotoOverlay
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/degree_den)
minute = float(minute_num/minute_den)/60
second = float(second_num/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
的全部功能,通过 PhotoOverlay,您还可以对高分辨率的照片进行进一步的研究。但是,本例确实演示了如何在 Google 地球或 Google 地球 API 中以布告板的样式悬挂照片。下面是使用此代码创建的 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 设备并不总是完全精确的,尤其对于安装在相机中的设备,因此,请检查照片的位置。
- 许多设备不跟踪海拔高度,而会把它设置为 0。如果您非常需要了解海拔高度,请寻找其他方法获得该数据。
- GPS 定位是对相机的定位,而不是对照片本身的定位。这就是本例会在 GPS 定位中定位 Camera 元素的原因,实际的照片距离该位置很远。
- Exif 不会获取有关您的相机指向的方向的信息,因此您可能需要调整您的
PhotoOverlays
。还好,有些设备(例如基于 Android 操作系统构建的手机)可让您直接获得诸如罗盘方向和倾斜度等数据,只是这些数据不在 Exif 标头中。
综上所述,这种方式仍算得上一种强大的照片显示方式。希望在不久的将来,我们能对照片进行越来越精确的地理标记。
跟进工作
开始使用 EXIF 标头后,您可以研究一下 EXIF 规范。这些规范存储了大量的其他数据,您可能很想获得这些数据,并将它们写入说明气泡框。您还可以考虑使用 ImagePyramids
创建功能更多的 PhotoOverlays
。有关 PhotoOverlays
的开发人员指南文章详细介绍了如何使用这些功能。