Python-地理空间分析学习指南-全-
Python 地理空间分析学习指南(全)
原文:
zh.annas-archive.org/md5/f684832d38cabcaf11bd908726afa9c8译者:飞龙
前言
本书首先为您介绍地理空间分析背景,然后介绍所使用的技术和科技流程,并将该领域划分为其组成部分的专业领域,例如 地理信息系统 (GIS)、遥感、高程数据、高级建模和实时数据。本书的重点是利用强大的 Python 语言和框架有效地进行地理空间分析,我们将专注于使用纯 Python 以及某些 Python 工具和 API,并使用通用算法。读者将能够分析各种形式的地理空间数据,了解实时数据跟踪,并了解如何将所学知识应用于有趣的场景。
尽管在示例中使用了多个第三方地理空间库,但我们将尽最大努力在可能的情况下使用纯 Python,不依赖任何库。这种专注于纯 Python 3 示例的做法将使本书区别于该领域的几乎所有其他资源。我们还将介绍一些在本书前版中未提及的流行库。
本书面向对象
本书面向任何希望了解数字制图和分析,并使用 Python 或任何其他脚本语言进行数据自动化或手动处理的人。本书主要针对希望使用 Python 进行地理空间建模和 GIS 分析的 Python 开发者、研究人员和分析人员。
要充分利用本书
本书假设您具备 Python 编程语言的基本知识。您将需要 Python(3.7 或更高版本),最低硬件要求为 300-MHz 处理器,128 MB 的 RAM,1.5 GB 的可用硬盘空间,以及 Windows、Linux 或 macOS X 操作系统。
下载示例代码文件
您可以从 www.packt.com 账户下载本书的示例代码文件。如果您在其他地方购买了此书,您可以访问 www.packtpub.com/support 并注册,以便将文件直接通过电子邮件发送给您。
您可以通过以下步骤下载代码文件:
-
在 www.packt.com 登录或注册。
-
选择“支持”标签。
-
点击“代码下载”。
-
在搜索框中输入书名,并遵循屏幕上的说明。
文件下载完成后,请确保使用最新版本的以下软件解压缩或提取文件夹:
-
Windows 版本的 WinRAR/7-Zip
-
Mac 版本的 Zipeg/iZip/UnRarX
-
Linux 版本的 7-Zip/PeaZip
本书代码包也托管在 GitHub 上,地址为 github.com/PacktPublishing/Learning-Geospatial-Analysis-with-Python-Third-Edition。如果代码有更新,它将在现有的 GitHub 仓库中更新。
我们还提供其他代码包,这些代码包来自我们丰富的书籍和视频目录,可在 github.com/PacktPublishing/ 上找到。查看它们吧!
下载彩色图像
我们还提供包含本书中使用的截图/图表的彩色图像的 PDF 文件。您可以从这里下载:static.packt-cdn.com/downloads/9781789959277_ColorImages.pdf。
使用的约定
本书使用了多种文本约定。
CodeInText:表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称。以下是一个示例:“为了演示这一点,以下示例访问了我们刚刚看到的相同文件,但使用urllib而不是ftplib。”
代码块以如下方式设置:
import ftplib
server = "ftp.ngdc.noaa.gov"
dir = "hazards/DART/20070815_peru"
fileName = "21415_from_20070727_08_55_15_tides.txt"
当我们希望您注意代码块中的特定部分时,相关的行或项目将以粗体显示:
if (sinSigma == 0):
distance = 0 # coincident points
break
cosSigma = sinU1*sinU2 + cosU1*cosU2*cosLam
sigma = math.atan2(sinSigma, cosSigma)
sinAlpha = cosU1 * cosU2 * sinLam / sinSigma
cosSqAlpha = 1 - sinAlpha**2
任何命令行输入或输出都应如下编写:
pip install virtualenv
粗体:表示新术语、重要单词或您在屏幕上看到的单词。例如,菜单或对话框中的单词在文本中显示如下。以下是一个示例:“从管理面板中选择系统信息。”
警告或重要提示显示如下。
小贴士和技巧显示如下。
联系我们
我们始终欢迎读者的反馈。
一般反馈:如果您对本书的任何方面有疑问,请在邮件主题中提及书名,并 通过 customercare@packtpub.com 发送邮件给我们。
勘误:尽管我们已经尽最大努力确保内容的准确性,但错误仍然可能发生。如果您在这本书中发现了错误,我们将不胜感激,如果您能向我们报告这一点。请访问 www.packtpub.com/support/errata,选择您的书籍,点击勘误提交表单链接,并输入详细信息。
盗版:如果您在互联网上以任何形式发现我们作品的非法副本,我们将不胜感激,如果您能提供位置地址或网站名称,请通过 copyright@packt.com 联系我们,并提供材料链接。
如果您想成为一名作者:如果您在某个领域有专业知识,并且您有兴趣撰写或为书籍做出贡献,请访问 authors.packtpub.com。
评论
请留下评论。一旦您阅读并使用过这本书,为何不在您购买它的网站上留下评论?潜在读者可以查看并使用您的客观意见来做出购买决定,我们 Packt 可以了解您对我们产品的看法,我们的作者也可以看到他们对书籍的反馈。谢谢!
如需了解 Packt 的更多信息,请访问 packt.com。
第一部分:行业的历史与现状
本节首先通过插图、基本公式、简单代码和 Python 演示常见的地理空间分析过程。在此基础上,你将学习如何处理地理空间数据——获取数据并为各种分析做准备。之后,你将了解地理空间技术生态系统中使用的各种软件包和库。本节结束时,你将学习如何评估任何地理空间工具。
本节包括以下章节:
-
第一章,使用 Python 学习地理空间分析
-
第二章,学习地理空间数据
-
第三章,地理空间技术概览
第一章:使用 Python 学习地理空间分析
地理空间技术目前正在影响我们的世界,因为它正在改变我们对人类历史的认识。在这本书中,我们将回顾地理空间分析的历史,这一历史早于计算机甚至纸质地图。然后,我们将探讨为什么你可能想要学习并使用编程语言作为地理空间分析师,而不是仅仅使用地理信息系统(GIS)应用。这将帮助我们理解将地理空间分析尽可能广泛地提供给尽可能多的人的重要性。
在本章中,我们将涵盖以下主题:
-
地理空间分析与我们的世界
-
Sarah Parcak 博士与考古学
-
地理信息系统
-
遥感概念
-
高程数据
-
计算机辅助绘图
-
地理空间分析与计算机编程
-
地理空间分析的重要性
-
地理信息系统概念
-
常见 GIS 流程
-
常见遥感流程
-
常见栅格数据概念
-
创建最简单的 Python GIS
是的,你没有听错!我们将从头开始使用 Python 构建最简单的 GIS。
技术要求
本书假设您对 Python 编程语言有一些基本知识,基本的计算机素养,以及至少对地理空间分析有所了解。本章为地理空间分析提供了一个基础,这对于攻击遥感和 GIS 领域的任何主题都是必要的,包括本书所有其他章节中的材料。
本书中的示例基于 Python 3.4.3,您可以从这里下载:www.python.org/downloads/release/python-343/.
地理空间分析与我们的世界
在 1880 年代,英国探险家开始将科学严谨性应用于挖掘古代文化遗址。考古学是一个令人沮丧、低效、昂贵且常常危险的行业,需要耐心和相当一部分运气。地球在保守秘密和抹去人类努力的故事方面做得非常出色。河流改道、洪水、火山、沙尘暴、飓风、地震、火灾和其他事件将整个城市吞噬进周围景观,而我们则随着时间流逝而失去它们。
我们对人类历史的了解基于通过考古挖掘和对通过有意识的猜测或试错而幸运地发现的遗址的研究,对古代文化的瞥见。过去,除非一个团队挖掘了一个遗址,找到了一些东西,并且正确地识别了它,否则在考古学上就不会有任何成功。对寻找地点的预测基于一些主要因素,如靠近支持农业所需的水源、先前发现的遗址、早期探险者的描述以及其他广泛的线索。
2007 年,阿拉巴马大学伯明翰分校的考古学家帕卡克博士开始诱导我们固执的地球揭示关于人类在哪里以及他们做了什么的一些秘密。从那时起,她的方法彻底改变了考古学领域。
在短短几年内,帕卡克博士及其团队在埃及发现了 17 座金字塔、1000 多座坟墓以及 3000 个古代定居点的足迹,包括著名失落城市塔尼斯的城市网格。她在罗马尼亚、纳巴泰王国和突尼斯确定了重要的考古遗址。她在经过充分挖掘的古代罗马港口波图斯找到了一个竞技场,以及通往台伯河附近的罗马的灯塔和运河。
她是如何发现那些几乎逃避了两个世纪检测的隐藏宝藏的呢?她着眼于更大的图景。帕卡克博士精通利用卫星图像从地球上方约 400 英里处定位古代遗址的艺术。她的职业生涯恰好与高分辨率卫星图像的出现相吻合,这些图像具有 10 英寸像素分辨率或更低,从而提供了检测景观微妙变化所需的细节,从而指示古代遗址。
尽管她的发现数量和重要性很大,但要从太空中定位文化遗产遗址需要做大量的工作。太空考古学家首先研究旧地图和历史记载。然后,他们查看现有遗址的现代数字地图。他们还查看数字地形模型,以定位古代人为了避免洪水而建造的微妙地面隆起。然后,他们使用多光谱图像,包括红外线,这些图像在处理过程中可以揭示植被或土壤的变化,这是由于地下埋藏的石头和其他材料引起的,这些材料在地表冒出。这种变色,通过伪彩色表示,使我们能够区分从地面上或甚至从空中肉眼几乎看不见的遗址反射的阳光频带,这些遗址突然以鲜明的对比脱颖而出,在卫星图像上显示出精确的位置。
古代文化遗产通常从地面上肉眼难以看到。例如,以下照片显示了美国伊利诺伊州利文斯顿附近一个保存完好的美洲原住民墓地,由于它的位置,它已经存在了数千年,并且很容易看到:

然而,在恶劣天气条件下,遗址可能会部分被破坏,因此它们很难找到。以下照片显示了路易斯安那州的一个沼泽地区,这里充满了几个世纪以来侵蚀的古代美洲原住民墓地,现在没有卫星图像几乎无法检测到:

以下由 NASA 科学家 Dr. Marco Giardino 处理过的卫星图像,与之前的照片在同一片湿地区域,显示了四个不同的墓葬堆的遗迹,这些遗迹从地面上是看不到的。尽管这个遗址已有数百年历史,但植被种类及其健康状况与周围湿地相比有所不同。尽管考古学家在该地区研究了数十个类似遗址,但该项目是第一个确定墓葬建造者通常使用将墓葬堆放置在四个主要方向(北、南、西、东)的模式的,这在太空中非常明显,但在地面上却难以实现:

空间考古学家在定位古代遗址方面速度很快,但他们现在发现自己不仅要与地质和气象因素作斗争。盗掘一直是考古学的威胁,但由于战争和黑市文物,这个问题变得更加严重。现代建设也可能破坏有价值的地标。然而,坚定的考古学家正在使用他们用来寻找上述遗址的相同技术来检测盗掘或建设威胁。一旦他们发现威胁的证据,他们就会通知政府,以便他们可以介入。以下图像显示了叙利亚东部杜拉欧罗普斯罗马遗址的盗掘证据。圆形区域包含盗掘者挖掘的洞:

除了卫星图像处理和视觉解释之外,空间考古学家还使用地理信息系统制图技术来标记或数字化遗址,叠加现代道路和城市足迹,创建标注地图等。令人兴奋的新兴领域空间考古学是我们将在本书中涵盖的许多地理空间分析应用中最新的一个。
超越考古学:地理空间分析几乎可以在每个行业中找到,包括房地产、石油和天然气、农业、国防、灾害管理、健康、交通和海洋学等。要了解地理空间分析在数十个不同行业中的应用概述,请访问 www.esri.com/what-is-gis/who-uses-gis。
地理空间分析的历史
地理空间分析可以追溯到 15000 年前,法国西南部的拉斯科洞穴。在这个洞穴中,旧石器时代的艺术家画了常见的猎物和许多专家认为的用于宗教仪式或甚至猎物迁移模式的星图。虽然这些绘画很粗糙,但它们展示了人类创造周围世界抽象模型并将时空特征相关联以寻找关系的古老例子。以下照片显示了其中一幅绘画,上面覆盖着星图:

几个世纪以来,制图艺术和土地测量科学得到了发展,但直到 19 世纪,地理分析的重大进展才出现。在 1830 年至 1860 年之间,欧洲爆发的致命霍乱疫情促使巴黎和伦敦的地理学家开始使用地理分析进行流行病学研究。
1832 年,查尔斯·皮凯特使用不同的半色调灰色阴影来表示巴黎 48 个区每千名公民的死亡人数,作为关于霍乱疫情报告的一部分。1854 年,约翰·斯诺博士通过追踪伦敦发生的霍乱疫情来扩展这种方法。通过在每次诊断出死亡病例时在城市的地图上放置一个点,他能够分析霍乱病例的聚集情况。斯诺追踪疾病到单一的水泵,并防止了进一步的病例。以下地图有三个层次,包括街道、每个水泵的X标记和每个霍乱死亡病例的点:

地理空间分析不仅仅用于疾病战争。几个世纪以来,将军和历史学家一直使用地图来理解人类战争。一位名叫查尔斯·米纳德的退休法国工程师在 1850 年至 1870 年期间制作了一些最复杂的信息图表。术语信息图表过于笼统,无法描述这些绘图,因为它们具有强烈的地理成分。这些地图的质量和细节使它们成为地理信息分析的绝佳例子,即使按照今天的标准也是如此。米纳德在 1869 年发布了他的杰作:
“La carte figurative des pertes successives en hommes de l'Armée Française dans la campagne de Russie 1812-1813,”翻译为“法国军队在 1812-13 年俄国战役中人员连续损失的图示地图。”
这描绘了 1812 年拿破仑军队在俄国战役中的毁灭。地图显示了军队随时间的变化规模和位置,以及当时的天气条件。以下图形包含关于单一主题的四个不同信息系列。这是使用笔和纸进行地理分析的绝佳例子。军队的大小由棕色和黑色条纹的宽度表示,比例为每毫米代表 10,000 人。数字也沿着条纹书写。棕色路径显示进入俄国的士兵,而黑色路径代表成功返回的士兵。地图比例尺显示在中心右侧,为一个法国里(2.75 英里或 4.4 公里)。底部的图表从右向左运行,描绘了士兵在从俄国返回的行军途中经历的残酷寒冷温度:

虽然远不如战争战役那样引人注目,但米纳德还发布了一份引人入胜的地图,记录了从法国各地运往巴黎的牲畜数量。米纳德在法国的不同地区使用了不同大小的饼图来展示每个地区的牲畜种类和数量:

在 20 世纪初,大规模印刷推动了地图层概念的发展——这是地理空间分析的一个关键特性。制图员在玻璃板上绘制不同的地图元素(植被、道路和等高线),然后可以将这些玻璃板堆叠并拍照,以打印成单一图像。如果制图员犯了错误,只需更换一块玻璃板,而不是整个地图。后来,塑料板的开发使得以这种方式创建、编辑和存储地图变得更加容易。然而,地图分层概念作为分析的有益特性,直到现代计算机时代才发挥作用。
GIS
计算机制图在 20 世纪 60 年代随着计算机本身的发展而演变。然而,GIS 这一术语的起源始于加拿大林业和农村发展部。罗杰·汤姆林森博士领导了一个由 40 名开发者组成的团队,与 IBM 签订协议,建立加拿大地理信息系统(CGIS)。CGIS 追踪加拿大的自然资源,并允许对这些特征进行详细分析,以便进一步研究。CGIS 将每种土地覆盖存储为不同的图层。
它还存储了适合整个国家的加拿大特定坐标系中的数据,该坐标系是为最佳面积计算而设计的。虽然按照今天的标准,所使用的技术是原始的,但当时该系统具有非凡的能力。CGIS 包括了在当时看起来相当现代的软件功能:
-
地图投影切换
-
扫描图像的橡皮膜处理
-
地图比例尺变更
-
线条平滑和泛化以减少特征中的点数
-
多边形的自动缝隙闭合
-
面积测量
-
多边形的溶解和合并
-
几何缓冲
-
创建新的多边形
-
扫描
-
从参考数据中数字化新特征
加拿大国家电影局在 1967 年制作了一部关于 CGIS 的纪录片,您可以通过以下网址观看:youtu.be/3VLGvWEuZxI。
汤姆林森常被称为 GIS 之父。在启动 CGIS 之后,他于 1974 年从伦敦大学获得博士学位,其论文题目为《电子计算方法和技术在存储、编制和评估地图数据中的应用》,该论文描述了 GIS 和地理空间分析。汤姆林森现在经营着自己的全球咨询公司——汤姆林森联合有限公司,并且他仍然是行业中的活跃参与者。他经常在地理空间会议上发表主题演讲。
CGIS 是地理空间分析的开端,如本书所定义。然而,如果没有 Howard Fisher 和哈佛大学设计研究生院计算机图形和空间分析实验室的工作,这本书就不会被写出来。他对 SYMAP GIS 软件的工作,该软件将地图输出到行式打印机,在实验室开启了一个发展时代,产生了另外两个重要的软件包,整体上永久性地定义了地理空间行业。SYMAP 导致了其他软件包的产生,包括 GRID 和 Odyssey 项目,这些项目都来自同一个实验室:
-
GRID 是一个基于栅格的 GIS 系统,它使用单元格来表示地理特征而不是几何形状。GRID 是由 Carl Steinitz 和 David Sinton 编写的。该系统后来成为 IMGRID。
-
Odyssey 是一个由 Nick Chrisman 和 Denis White 领导的团队项目。它是一个包含许多现代地理数据库系统典型地理空间数据管理功能的程序系统。哈佛试图将这些软件包商业化,但成功有限。然而,它们的影响至今仍可见。
几乎所有现有的商业和开源软件包都从这些代码库中汲取了某些东西。
Howard Fisher 制作了一部 1967 年的电影,使用 SYMAP 的输出结果展示了密歇根州兰辛市从 1850 年到 1965 年的城市扩张,通过手动将数十年的财产信息编码到系统中。这项分析耗时数月,但现在由于现代工具和数据,只需几分钟就能重新创建它们。
您可以在www.youtube.com/watch?v=xj8DQ7IQ8_o上观看这部电影。
现在已有来自 Esri、ERDAS、Intergraph、ENVI 等公司的数十种图形用户界面(GUI)地理空间桌面应用程序。Esri 是最早且持续运营的 GIS 软件公司,其始于 20 世纪 60 年代末。在开源领域,包括Quantum GIS(QGIS)和地理资源分析支持系统(GRASS)在内的软件包被广泛使用。除了全面的桌面软件包外,用于构建新软件的软件库有成千上万。
GIS 可以提供有关地球的详细信息,但它仍然只是一个模型。有时,我们需要直接的表现形式来了解我们星球上当前或最近的变化。在这种情况下,我们需要遥感。
遥感
遥感是指在不与该物体进行物理接触的情况下收集有关该物体的信息。在地理空间分析的情况下,该物体通常是地球。遥感还包括处理收集到的信息。地理信息系统(GIS)的潜力仅限于可用的地理数据。即使使用现代 GPS 来填充 GIS,土地测量的成本也一直是资源密集型的。
遥感技术的出现不仅极大地降低了地理空间分析的成本,而且将这一领域引向了全新的方向。除了为 GIS 系统提供强大的参考数据外,遥感还通过从图像和地理数据中提取特征,使得 GIS 数据的自动和半自动生成成为可能。这位古怪的法国摄影师加斯帕德-费利克斯·图尔纳尚,也被称为纳达尔,于 1858 年从巴黎的热气球上拍摄了第一张航空照片:

真正的全景视角对世界的价值立即显现出来。早在 1920 年,关于航空照片解读的书籍就开始出现。
二战后,当美国与苏联进入冷战时,为了监控军事能力,航空摄影随着美国 U-2 侦察机的发明而变得非常普遍。U-2 侦察机可以飞行至 75,000 英尺的高度,超出了仅能到达 50,000 英尺的现有防空武器范围。当苏联最终击落一架 U-2 并捕获飞行员时,美国 U-2 在俄罗斯上空的飞行结束了。
然而,航空摄影对现代地理空间分析的影响很小。飞机只能捕捉到区域的小面积。照片被钉在墙上或在灯箱上查看,而不是与其他信息结合。尽管非常实用,但航空照片解读仅仅是另一种视觉视角。
重大变革发生在 1957 年 10 月 4 日,当时苏联发射了 Sputnik 1 卫星。苏联因为制造困难而放弃了更复杂、更先进的卫星原型,这个原型后来成为 Sputnik 3。相反,他们选择了一个简单的金属球体,带有四个天线和一个简单的无线电发射器。包括美国在内的其他国家也在研制卫星。这些卫星计划并非完全保密。它们是作为国际地球物理年(IGY)科学动机的一部分而推动的。
火箭技术的进步使得人造卫星成为地球科学的一个自然演变。然而,在几乎所有情况下,每个国家的国防机构都深度参与其中。与苏联类似,其他国家也在努力应对复杂的卫星设计,这些卫星装备了大量的科学仪器。苏联决定转向最简单的设备,仅仅是为了在美国人有效之前发射卫星。当它经过天空时,Sputnik 是可见的,业余无线电操作员可以听到它的无线电脉冲。尽管 Sputnik 很简单,但它提供了从其轨道力学和无线电频率物理学中可以得出的宝贵科学信息。
斯普特尼克计划对美国太空计划的影响最大。美国的主要对手在太空竞赛中获得了巨大的优势。美国最终以阿波罗登月计划做出了回应。然而,在此之前,美国启动了一个直到 1995 年才公开的国家机密计划。保密的 CORONA 计划产生了第一张太空照片。美国和苏联签署了一项结束间谍飞机飞行的协议,但卫星在谈判中却明显缺席。
下面的地图显示了 CORONA 流程。虚线是卫星飞行路径,长长的白色管子是卫星,较小的白色圆锥体是胶卷盒,黑色块状物是触发胶卷弹射的控制站,以便飞机可以在空中接住:

第一颗 CORONA 卫星是一个历时 4 年的项目,遇到了许多挫折。然而,该计划最终取得了成功。即使在今天,卫星成像的困难之处在于从太空中获取图像。CORONA 卫星使用了黑白胶卷的罐子,一旦曝光就会从车辆中弹出。当胶卷盒从空中降落时,美国军用飞机会在空中接住包裹。如果飞机错过了罐子,它会在水中漂浮一段时间,然后沉入海底,以保护敏感信息。
美国继续发展 CORONA 卫星,直到它们的分辨率和摄影质量与 U-2 间谍飞机照片相当。CORONA 仪器的缺点主要是可重复使用性和及时性。一旦用完胶片,卫星就不再提供服务。此外,胶片的回收有一个固定的日程安排,这使得该系统不适合监控实时情况。然而,CORONA 计划的整体成功为下一波卫星的发展铺平了道路,迎来了遥感技术的现代时代。
由于 CORONA 计划的保密状态,其对遥感的影响是间接的。在载人美国太空任务中拍摄的地球照片启发了民用遥感卫星的想法。这种卫星的好处是显而易见的,但这个想法仍然存在争议。政府官员质疑卫星是否像航空摄影那样具有成本效益。军方担心公众卫星可能会危及 CORONA 计划的保密性。其他官员担心未经许可对其他国家进行成像的政治后果。然而,内政部(DOI)最终获得了 NASA 创建用于监测地球表面资源的卫星的许可。
1972 年 7 月 23 日,NASA 发射了地球资源技术卫星(ERTS)。ERTS 很快被更名为Landsat 1。该平台包含两个传感器。第一个是回波束显像管(RBV)传感器,本质上是一个视频摄像机。它由无线电和电视巨头美国无线电公司(RCA)建造。RBV 立即出现了问题,包括使卫星的 altitude guidance system 失效。第二次卫星尝试是高度实验性的多光谱扫描仪(MSS)。MSS 表现完美,并产生了比 RBV 更优越的结果。MSS 捕捉了地球表面反射光线的四个不同波长的四个单独图像。
这个传感器具有几个革命性的功能。第一个也是最重要的功能是首次对地球进行全球成像,每 16 天扫描地球上的每一个地点。以下由 NASA 提供的图像展示了这次飞行和收集模式,即传感器围绕地球轨道运行时的一系列重叠带,每次传感器对地球上的某个地点进行成像时,都会捕获数据块:

它还记录了可见光谱之外的光线。虽然它捕捉到了人眼可见的绿色和红色光线,但它还扫描了人眼看不见的两种不同波长的近红外光。图像被存储并数字化传输到马里兰州、加利福尼亚州和阿拉斯加的三个不同的地面站。其多光谱能力和数字格式意味着 Landsat 提供的天空视角不仅仅是另一张天空照片。它是在向下传输数据。这些数据可以通过计算机处理,以输出关于地球的派生信息,就像 GIS 通过分析另一个地理特征中的另一个地理特征来提供关于地球的派生信息一样。NASA 推广了 Landsat 在全球的使用,并将数据以非常低廉的价格提供给任何请求的人。
这种全球成像能力导致了许多科学突破,包括直到 1976 年才发现的未知地理发现。例如,利用 Landsat 图像,加拿大政府找到了一个由北极熊居住的微小未绘制的岛屿。他们把新的陆地命名为 Landsat 岛。
Landsat 1 之后,还有六个其他任务,这些任务被转交给国家海洋和大气管理局(NOAA)作为责任机构。Landsat 6 由于破裂的歧管而未能进入轨道,这使其机动引擎失效。在这些任务中,卫星由地球观测卫星公司(EOSAT)管理,现在称为Space Imaging,但在 Landsat 7 任务中又回归政府管理。以下由 NASA 提供的图像是 Landsat 7 产品的样本:

陆地卫星数据连续性任务(LDCM)于 2013 年 2 月 13 日发射,并于 2013 年 4 月 27 日开始收集图像,作为其成为陆地卫星 8 的校准周期的一部分。LDCM 是 NASA 和美国地质调查局(USGS)之间的联合任务。
高程数据
遥感数据可以从两个维度测量地球。但我们可以利用数字高程数据,将其包含在数字高程模型中,从而使用遥感技术从三个维度测量地球。数字高程模型(DEM)是行星地形的立体表示。在本书中,这个行星指的是地球。数字高程模型的历史比遥感影像要简单得多,但同样重要。在计算机出现之前,高程数据的表示仅限于通过传统地面测量创建的地形图。虽然存在从立体图像或如粘土或木材等材料制作三维模型的技术,但这些方法在地理学中并未得到广泛应用。
数字高程模型的概念于 1986 年出现,当时法国空间机构国家空间研究局(CNES)或国家空间研究中心,发射了其 SPOT-1 卫星,该卫星包含一个立体雷达。这个系统创建了第一个可用的 DEM。随后,几个美国和欧洲的卫星也采用了类似的模式,执行了类似的任务。
2000 年 2 月,航天飞机奋进号执行了航天飞机雷达地形测量任务(SRTM),使用特殊的雷达天线配置,单次飞行就收集了地球表面超过 80%的高程数据。这个模型在 2009 年被美国和日本联合任务超越,该任务使用搭载在 NASA 的 Terra 卫星上的高级机载热辐射和反射辐射计(ASTER)传感器。这个系统捕捉了地球表面的 99%,但已经证明存在一些数据问题。由于航天飞机的轨道没有穿越地球的极地,因此没有捕捉到整个表面。SRTM 仍然是黄金标准。以下来自美国地质调查局(USGS)的图片(www.usgs.gov/media/images/national-elevation-dataset)显示了一个被称为阴影图的彩色 DEM。绿色区域代表低海拔,而黄色和棕色区域代表中等到高海拔:

最近,全球高程数据集的更宏伟尝试正在进行中,形式为德国于 2007 年和 2010 年分别发射的 TerraSAR-X 和 TanDEM-X 卫星。这两颗雷达高程卫星共同工作,于 2014 年 4 月 15 日发布了名为 WorldDEM 的全局 DEM。这个数据集的相对精度为 2 米,绝对精度为 4 米。
计算机辅助绘图
计算机辅助设计(CAD)值得提及,尽管它与地理空间分析没有直接关系。CAD 系统的发展历史与地理空间分析的历史平行且交织在一起。CAD 是一种工程工具,用于建模二维和三维对象,通常用于工程和制造。地理空间模型与 CAD 模型的主要区别在于,地理空间模型以地球为参考,而 CAD 模型可能存在于抽象空间中。
例如,在 CAD 系统中,一个建筑的三个维度的蓝图不会有纬度或经度,但在 GIS 中,同一个建筑模型将在地球上有一个位置。然而,多年来,CAD 系统已经采用了许多 GIS 系统的功能,并且通常用于较小的 GIS 项目。同样,许多 GIS 程序可以导入已经地理参考的 CAD 数据。传统上,CAD 工具主要是为了设计非地理空间的数据。
然而,参与城市公用事业电力系统设计等地理空间工程项目的工程师会使用他们熟悉的 CAD 工具来创建地图。随着时间的推移,GIS 软件演变为导入工程师生产的面向地理空间的 CAD 数据,CAD 工具也演变为支持地理空间数据创建并与 GIS 软件更好地兼容。Autodesk 的 AutoCAD 和 Esri 的 ArcGIS 是开发这种功能的领先商业软件包,地理空间数据抽象库(GDAL)OGR 库的开发者也增加了 CAD 支持。
地理空间分析和计算机编程
现代地理空间分析可以通过在任何易于使用的商业或开源地理空间软件包中点击按钮来完成。那么,为什么你想要使用编程语言来学习这个领域呢?最重要的原因如下:
-
你希望完全控制底层算法、数据和执行
-
你希望用最小的开销从大型多用途地理空间框架中自动化特定的重复分析任务
-
你希望创建一个易于分享的程序
-
你希望学习地理空间分析,而不仅仅是软件中的按钮操作
地理空间行业正逐渐摆脱传统的流程,即分析团队使用昂贵的桌面软件来制作地理空间产品。地理空间分析正在被推向居住在云中的自动化流程。终端用户软件正朝着特定任务的工具发展,其中许多工具可以通过移动设备访问。了解地理空间概念和数据,以及构建自定义地理空间流程的能力,是未来地理空间工作的关键所在。
面向对象编程用于地理空间分析
面向对象编程是一种软件开发范式,其中概念被建模为具有属性的对象,这些属性以属性和方法的形式表示行为。这种范式的目标是创建更模块化的软件,其中一个对象可以继承自一个或多个其他对象,以鼓励软件重用。
Python 编程语言以其作为设计良好的面向对象语言、过程式脚本语言,甚至函数式编程语言的多重角色而闻名。然而,你永远不会完全放弃面向对象编程,因为即使是 Python 的本地数据类型也是对象,所有 Python 库,称为模块,都遵循基本的对象结构和行为。
地理空间分析是面向对象编程的完美活动。在大多数面向对象编程项目中,对象是抽象概念,例如没有现实世界对应物的数据库连接。然而,在地理空间分析中,所建模的概念实际上是现实世界中的对象!地理空间分析的应用领域是地球及其上的所有事物。树木、建筑物、河流和人类都是地理空间系统中的对象示例。
在面向对象编程的新手文献中,常见的例子是具体的猫类比。面向对象编程的书籍经常使用以下形式的例子。
想象一下你正在看一只猫。我们了解一些关于这只猫的信息,比如它的名字、年龄、颜色和大小。这些特征是猫的属性。猫还表现出诸如进食、睡觉、跳跃和咕噜咕噜叫等行为。在面向对象编程中,对象也有属性和行为。你可以模拟现实世界中的对象,例如我们例子中的猫,或者更抽象的对象,如银行账户。
面向对象编程中的大多数概念比简单的猫范式或甚至银行账户都要抽象得多。然而,在地理空间分析中,所建模的对象保持具体,例如简单的猫类比,在许多情况下就是猫。地理空间分析允许你继续使用简单的猫类比,甚至可视化它。以下地图展示了由澳大利亚生活地图集(ALA)提供的数据所表示的澳大利亚野猫种群:

因此,我们可以使用计算机来分析地球上特征之间的关系,但我们为什么要这样做呢?在下一节中,我们将探讨为什么地理空间分析是一项值得努力的事业。
地理空间分析的重要性
地理空间分析帮助人们做出更好的决策。它不会为你做决定,但它可以回答那些决定核心的、通常无法以其他方式回答的关键问题。直到最近,地理空间技术和数据只是政府和资金充足的研究人员可用的工具。然而,在过去十年中,数据变得更加广泛可用,软件也变得更加容易获得。
除了免费提供的政府卫星图像外,许多地方政府现在进行航空摄影调查,并将数据在线提供。无处不在的谷歌地球提供了一个跨平台的旋转地球视图,包含卫星和航空数据、街道、兴趣点、照片等等。谷歌地球用户可以创建定制的Keyhole Markup Language(KML)文件,这些是用于将数据和样式加载到地球上的 XML 文件。这个程序和类似工具通常被称为地理探索工具,因为它们是优秀的数据查看器,但提供的数据分析能力非常有限。
野心勃勃的 OpenStreetMap 项目(www.openstreetmap.org/#map=5/51.500/-0.100)是一个众包的、全球性的地理基础地图,包含大多数在 GIS 中常见的层。现在几乎每部手机都内置了 GPS,以及用于收集 GPS 轨迹作为点、线或多边形的移动应用程序。大多数手机还会将用手机相机拍摄的照片标记上 GPS 坐标。简而言之,任何人都可以成为地理空间分析师。
全球人口已达到 70 亿人。世界的变化比以往任何时候都要快。地球正在经历着历史上从未见过的环境变化。更快的通讯和交通增加了我们与生活环境之间的互动。安全、负责任地管理人和资源比以往任何时候都更具挑战性。地理空间分析是更高效、更深入地理解我们世界的最佳方法。越多地利用地理空间分析能力的政治家、活动家、救援工作者、父母、教师、第一响应者、医疗专业人士和小型企业,我们就有更多潜力创造一个更美好、更健康、更安全、更公平的世界。
GIS 概念
为了开始地理空间分析,我们需要了解一些独特的核心概念。这个列表并不长,但分析的几乎每个方面都可以追溯到这些想法之一。
主题地图
如其名称所示,主题地图描绘了一个特定的主题。一般参考地图以与导航或规划相关的地理位置来直观地表示特征。主题地图超越了位置,为围绕中心思想的信息提供地理背景。通常,主题地图是为特定受众设计的,以回答具体问题。主题地图的价值在于它们没有展示的内容。主题地图将使用最少的地理特征来避免分散读者的注意力。大多数主题地图包括政治边界,如国家或州边界,但省略了导航特征,如街道名称或位于主要地标之外的景点,这些地标有助于读者定位。
本章前面提到的约翰·斯诺博士的霍乱地图是主题地图的一个完美例子。主题地图的常见用途是可视化健康问题,如疾病、选举结果以及环境现象,如降雨。这些地图也是地理空间分析的最常见输出。以下是美国人口普查局的地图,显示了各州的癌症死亡率:

主题地图讲述故事并且非常有用。然而,重要的是要记住,尽管主题地图和其他地图一样,是现实的模型,但它们也是信息的概括。两位不同的分析师使用相同的信息来源,通常会得出非常不同的主题地图,这取决于他们如何分析和总结数据。他们也可能选择关注数据集的不同方面。主题地图的技术性质往往导致人们将它们视为科学证据。然而,地理空间分析往往没有结论。虽然分析可能基于科学数据,但分析师并不总是遵循科学方法的严谨性。
在他的经典著作《如何用地图说谎》中,马克·莫蒙尼尔(Mark Monmonier),芝加哥大学出版社,详细展示了地图是如何轻易地被操纵成现实的模型,这些模型通常被滥用。这一事实并不降低这些工具的价值。传奇统计学家乔治·博克斯(George Box)在他的 1987 年著作《经验模型构建和响应面》中写道:
“本质上,所有模型都是错误的,但有些是有用的。”
主题地图已被用作指导开始(和结束)战争、阻止致命疾病、赢得选举、养活国家、对抗贫困、保护濒危物种以及救助受灾害影响的人的指南。主题地图可能是创建的最有用的模型。
在其最纯粹的形式中,数据库只是一个组织的信息集合。数据库管理系统(DBMS)是一个可以与数据库交互的交互式软件套件。人们经常使用“数据库”这个词作为一个总称,指代 DBMS 和底层数据结构。数据库通常包含字母数字数据,在某些情况下,还包含可以存储二进制数据(如图像)的二元大对象或 BLOB。大多数数据库还允许使用关系数据库结构,其中规范化表中的条目可以相互引用,以在数据之间创建多对一和一对多关系。
空间数据库,也称为地理数据库,使用专用软件将传统的关系数据库管理系统(RDBMS)扩展到存储和查询在二维或三维空间中定义的数据。一些系统还考虑了一系列随时间变化的数据。在空间数据库中,关于地理特征的属性以传统的数据库结构存储和查询。这些空间扩展允许您使用结构化查询语言(SQL)以类似于传统数据库查询的方式查询几何形状。空间查询和属性查询也可以结合使用,以基于位置和属性选择结果。
空间索引
空间索引是一个组织地理空间矢量数据以实现更快检索的过程。它是一种预先过滤数据以用于常见查询或渲染的方法。索引通常在大型数据库中用于加速查询的返回。空间数据也不例外。即使是中等规模的地理数据库也可能包含数百万个点或对象。如果你执行空间查询,数据库中的每个点都必须由系统考虑,以便将其包含在结果中或从结果中排除。空间索引以允许在详细且较慢的分析剩余项目之前,通过进行计算上更简单的检查来消除数据集的大部分内容。
元数据
元数据被定义为关于数据的数据。因此,地理空间元数据是关于地理空间数据集的数据,它提供了数据集来源和历史追踪,以及技术细节的摘要。元数据还通过随着时间的推移记录资产来提供数据的长期保存。
地理空间元数据可以用几种可能的标准来表示。最突出的标准之一是国际标准ISO 19115-1,它包括数百个潜在字段来描述单个地理空间数据集。此外,ISO 19115-2标准还包括地理空间影像和栅格数据的扩展。一些示例字段包括空间表示、时间范围和来源。ISO 19115-3 是描述从 ISO 地理元数据生成 XML 模式的程序的规范。Dublin Core 是另一个国际标准,它最初是为数字数据开发的,后来扩展到地理空间数据,以及相关的 DCAT 词汇表,用于构建单一来源的数据目录。
元数据的主要用途是编目数据集。现代元数据可以被地理搜索引擎摄取,使其可能被其他系统自动发现。它还列出了数据集的联系方式,如果你有问题的话。元数据是地理空间分析师的重要支持工具,为你的工作增加了可信度和可访问性。创建网络目录服务(CSW)的开放地理空间联盟(OGC)用于管理元数据。pycsw Python 库实现了 CSW 标准。
元数据是用于管理地理空间数据的重要文档工具,而pycsw是一个符合 OGC 标准的 CSW 实现。你可以在pycsw.org了解更多关于pycsw的信息。
地图投影
地图投影有整本书专门介绍,对于新分析师来说可能是一个挑战。如果你将任何三维物体平铺在一个平面上,比如你的屏幕或一张纸,这个物体就会变形。许多小学地理课通过让学生剥橙子并尝试将皮平铺在桌子上来展示这个概念,以便理解产生的变形。当你将地球的圆形投影到计算机屏幕上时,也会产生相同的效果。
在地理空间分析中,你可以操纵这种变形以保留常见的属性,如面积、比例、方位、距离或形状。地图投影没有一种适合所有情况的解决方案。投影的选择总是为了在某一维度上获得精度而牺牲另一维度的误差。投影通常表示为一组超过 40 个参数,可以是 XML 格式,也可以是称为已知文本(WKT)的文本格式,用于定义转换算法。
国际油气生产商协会(IOGP)维护着一个最知名投影的注册表。该组织以前被称为欧洲石油调查组(EPSG)。注册表中的条目仍被称为 EPSG 代码。EPSG 维护这个注册表是为了石油和天然气行业的一个共同利益,该行业是能源勘探中地理空间分析的大用户。据最后一次统计,这个注册表包含超过 5,000 个条目。
10 年前,地图投影对地理空间分析师来说至关重要。数据存储成本高昂,高速互联网很少见,云计算尚未真正存在。地理空间数据通常在各自感兴趣的不同领域的小组之间交换。当时的技术限制意味着地理空间分析高度本地化。分析师会使用最适合他们兴趣领域的投影。
由于不同的投影代表地球的两种不同模型,因此不同投影的数据不能显示在同一张地图上。每当分析师从第三方接收数据时,他们必须在使用现有数据之前重新投影,这个过程既繁琐又耗时。
大多数地理空间数据格式都没有提供存储投影信息的方法。这些信息通常存储在一个辅助文件中,通常是文本或 XML 格式。由于分析师不经常交换数据,许多人不会费心定义投影信息。每个分析师的噩梦就是遇到一个极其宝贵的缺失投影信息的数据集。这使得数据集变得无用。文件中的坐标只是数字,并不能提供关于投影的线索。在超过 5,000 种选择中,几乎不可能猜对。
现在,多亏了现代软件和互联网使数据交换变得更加容易和普遍,几乎每种数据格式都增加了一种元数据格式,用于定义投影或将它放在文件头中(如果支持的话)。技术的进步也允许全球基础地图的存在,这允许更普遍地使用投影,例如 Google Maps 使用的通用墨卡托投影。这个投影也被称为 Web Mercator,使用代码 EPSG:3857(或已弃用的 EPSG:900913)。
地理空间门户网站项目,如 OpenStreetMap(www.openstreetmap.org/#map=5/51.500/-0.100)和 NationalAtlas.gov,已经将世界大部分地区的数据集在共同投影下进行了整合。现代地理空间软件还可以即时重新投影数据,从而省去了分析师在使用数据之前预处理数据的麻烦。与地图投影密切相关的是大地基准。基准是一个用于将地球表面上的特征与坐标系统匹配的地球表面模型。一个常见的基准是 WGS 84,它被 GPS 使用。
渲染
地理空间分析令人兴奋的部分是可视化。由于地理空间分析是一个基于计算机的过程,了解地理数据在计算机屏幕上的显示方式是很好的。
地理数据,包括点、线和多边形,以数值形式存储为一个或多个点,这些点以(x,y)对或(x,y,z)元组的形式出现。x代表图表上的水平轴,而y代表垂直轴。z代表地形高程。在计算机图形学中,计算机屏幕由x轴和y轴表示。z轴不使用,因为大多数图形软件 API 将计算机屏幕视为二维平面。然而,由于桌面计算能力持续提升,三维地图开始变得更加常见。
另一个重要因素是屏幕坐标与世界坐标。地理数据存储在一个代表地球网格的坐标系中,该地球是三维和圆形的。屏幕坐标,也称为像素坐标,代表二维计算机屏幕上的像素网格。将x和y世界坐标映射到像素坐标相对简单,涉及一个简单的缩放算法。然而,如果存在z坐标,则必须执行更复杂的转换,将坐标从三维空间映射到二维平面。这些转换可能计算成本高昂,因此如果不正确处理,可能会减慢速度。
在遥感数据的情况下,挑战通常是文件大小。即使是中等大小的压缩卫星图像也可能达到数十甚至数百兆字节。可以使用两种方法来压缩图像:
-
无损方法:它们使用技巧来减少文件大小,而不丢弃任何数据
-
有损压缩算法:它们通过减少图像中的数据量来减小文件大小,同时避免图像外观发生重大变化
在屏幕上渲染图像可能计算密集。大多数遥感文件格式允许存储多个较低分辨率的图像版本,称为概览或金字塔,仅为了在不同比例下更快地渲染。当从图像缩放到可以看到全分辨率图像细节的比例时,会快速且无缝地显示预先处理过的、较低分辨率的图像版本。
遥感概念
我们所描述的大多数 GIS 概念也适用于栅格数据。然而,栅格数据也有一些独特的属性。在本章早期,当我们回顾遥感的历史时,重点是地球从空中平台成像。需要注意的是,栅格数据可以以多种形式出现,包括地面雷达、激光测距仪以及其他用于在地理环境中检测气体、辐射和其他形式能量的专用设备。
为了本书的目的,我们将重点关注捕获大量地球数据的遥感平台。这些来源包括地球成像系统、某些类型的高程数据,以及在某些情况下的一些天气系统。
图像作为数据
栅格数据以方形瓦片的形式数字化捕获。这意味着数据以行和列的数值数组形式存储在计算机上。如果数据是多光谱的,数据集通常将包含多个相同大小的数组,这些数组在地理空间上相互参照,以表示地球上的单个区域。这些不同的数组被称为波段。任何数值数组都可以在计算机上表示为图像。实际上,所有计算机数据最终都是数字。在地理空间分析中,将图像视为数值数组非常重要,因为数学公式被用来处理它们。
在遥感图像中,每个像素代表空间(地球上一定大小的位置)和从该位置反射到空间的光的反射率。因此,每个像素都有一个地面大小,并包含一个表示强度的数字。由于每个像素都是一个数字,我们可以对这份数据执行数学方程,以结合来自不同波段的 数据,并突出图像中特定的物体类别。如果波长值超出可见光谱,我们可以突出人眼看不到的特征。例如,使用称为归一化植被指数(NDVI)的特定公式,植物中的叶绿素可以产生很大的对比度。
通过处理遥感图像,我们可以将这些数据转化为视觉信息。使用 NDVI 公式,我们可以回答问题,这张图像中植物的健康状况如何? 你还可以创建新的数字信息类型,这些信息可以用作计算机程序的输入,以输出其他类型的信息。
遥感与颜色
计算机屏幕将图像显示为红、绿、蓝(RGB)的组合,以匹配人眼的感知能力。卫星和其他遥感成像设备可以捕捉到超出可见光谱的光线。在计算机上,超出可见光谱的波长被表示在可见光谱中,以便我们可以看到它们。这些图像被称为假彩色图像。在遥感中,例如,红外光使水分变得非常明显。
这种现象有各种用途,例如在洪水期间监测地面饱和度或寻找屋顶或堤坝中的隐藏泄漏。
常见的矢量 GIS 概念
在本节中,我们将讨论在地理空间分析中常用的一些不同类型的 GIS 处理过程。这个列表并不详尽;然而,它为你提供了所有其他操作的基础操作。如果你理解了这些操作,你将很快理解更复杂的过程,因为它们要么是这些过程的衍生物,要么是这些过程的组合。
数据结构
GIS 向量数据使用由至少一个x水平值和一个y垂直值组成的坐标来表示地球上的位置。在许多情况下,一个点也可能包含一个z值。其他辅助值也是可能的,包括测量值或时间戳。
这些坐标用于形成点、线和多边形,以模拟现实世界中的对象。点可以是自身就是几何特征的几何要素,或者它们可以连接线段。由线段创建的封闭区域被认为是多边形。多边形可以模拟诸如建筑、地形或政治边界等对象。
一个 GIS 要素可以由一个点、线或多边形组成,或者可以由多个形状组成。例如,在一个包含世界国家边界的 GIS 多边形数据集中,由 7,107 个岛屿组成的菲律宾将被表示为一个由数千个多边形组成的单一国家。
向量数据通常比栅格数据更好地表示地形特征。向量数据具有更高的精度潜力,并且更加精确。然而,在大规模收集向量数据方面,传统上比栅格数据成本更高。
与向量数据结构相关的另外两个重要术语是边界框和凸包。边界框,或最小边界框,是包含数据集中所有点的可能最小的正方形。以下图示演示了点集合的边界框:

数据集的凸包与边界框类似,但不同之处在于,它是一个可以包含数据集的最小多边形。以下图示显示了与上一个示例相同的点数据,其中凸包多边形用红色表示:

如你所见,数据集的边界框总是包含凸包。
多边形的地理空间规则
在地理空间分析中,关于多边形有一些一般性的经验法则,这些法则与多边形的数学描述不同:
-
多边形至少必须有四个点——第一个和最后一个点必须相同
-
多边形的边界不应该重叠自身
-
层中的多边形不应该重叠
-
在另一个多边形内部的层中的多边形被认为是下层数据的多边形中的空洞
不同的地理空间软件包和库对这些规则的例外情况处理不同,这可能导致令人困惑的错误或软件行为。最安全的做法是确保您的多边形遵守这些规则。关于多边形,我们还需要讨论的一个重要信息。
根据定义,多边形是一个封闭的形状,这意味着多边形的第一个和最后一个顶点是相同的。一些地理空间软件如果未明确地将第一个点作为多边形数据集中的最后一个点进行复制,将会抛出一个错误。其他软件会自动关闭多边形而不会抱怨。您用于存储地理空间数据的数据格式也可能决定了多边形的定义。这个问题是一个灰色地带,因此它没有成为多边形规则的一部分,但了解这个特性在遇到难以解释的错误时可能会很有帮助。
缓冲
缓冲操作可以应用于空间对象,包括点、线和多边形。这个操作在指定距离处创建一个围绕对象的 polygon。缓冲操作用于邻近分析:例如,在危险区域周围建立安全区。让我们回顾一下这个图:

黑色形状代表原始几何形状,而红色轮廓代表从原始形状生成的更大的缓冲 polygon。
溶解
溶解操作可以将相邻的多边形合并成一个单一的多边形。溶解操作也用于简化从遥感中提取的数据,如下所示:

溶解操作的一个常见用途是将单一所有者购买的土地数据库中相邻的两个属性合并。
概括
对于地理空间模型来说,具有比必要点数更多的对象可以通过概括来减少表示形状所使用的点数。这个操作通常需要尝试几次才能得到最优的点数,同时不损害整体形状。这是一种数据优化技术,用于简化数据以提高计算效率或更好的可视化。这项技术在网络地图应用中非常有用。
这里是一个多边形概括的例子:

由于计算机屏幕的分辨率为每英寸 72 个点(dpi),高度详细的数据点,这些点在视觉上不可见,可以被减少,以便使用更少的带宽向用户发送视觉上等效的地图。
交集
交集操作用于检查一个特征的一部分是否与一个或多个特征相交。这个操作用于邻近分析中的空间查询,通常是缓冲分析的后继操作:

合并操作将两个或更多非重叠形状合并为一个单一的多形状对象。多形状对象是那些保持独立几何形状但被 GIS 视为具有单一属性集的单个特征的形状:

一个基本的地理空间操作是检查一个点是否在多边形内部。这个操作是许多不同类型空间查询的原子构建块。如果点在多边形的边界上,则被认为是内部的。存在非常少的空间查询不依赖于这种计算。然而,在大量点的情况下,它可能非常慢。
检测一个点是否在多边形内部最常见且高效的算法称为射线投射算法。首先,进行一个测试以查看该点是否在多边形边界上。接下来,算法从问题点沿单一方向绘制一条线。程序计算该线与多边形边界的交叉次数,直到达到多边形的边界框,如图所示:

边界框是围绕整个多边形可以绘制的最小框。如果数字是奇数,则点在内部。如果边界交叉次数是偶数,则点在外部。
并集
并集操作不如合并操作常见,但在需要将两个或多个重叠多边形合并为一个单一形状时非常有用。它与溶解操作类似,但在此情况下,多边形是重叠的,而不是相邻的:

通常,此操作用于清理来自遥感操作自动生成的要素数据集。
连接
连接或 SQL 连接是一种数据库操作,用于合并两个或多个信息表。关系型数据库旨在避免为多对一关系存储冗余信息。例如,一个美国州可能包含许多城市。而不是为每个州创建包含其所有城市的表,可以创建一个包含州数字 ID 的州表,同时为每个州的所有城市创建一个包含州数字 ID 的表。
在 GIS 中,还可以有空间连接,这是数据库空间扩展软件的一部分。在空间连接中,您以与 SQL 连接相同的方式组合属性。然而,这种关系基于两个特征的地理邻近性。
为了遵循前面的城市示例,我们可以使用空间连接添加每个城市所在的县名。城市层可以在包含县名的县多边形层上加载。空间连接将确定哪个城市位于哪个县,并执行 SQL 连接以将县名添加到每个城市的属性行中。
常见的栅格数据概念
如我们之前提到的,遥感栅格数据是一个数字矩阵。遥感包含数千种可以在数据上执行的操作。随着新卫星的发射和计算机能力的提升,这个领域几乎每天都在变化。
尽管这个领域已有十年的历史,但我们甚至还没有触及到这个领域可以为人类提供的知识的表面。再次强调,与常见的 GIS 过程类似,这个最小操作列表允许你评估遥感中使用的任何技术。
波段数学
波段数学是多维数组数学。在数组数学中,数组被视为单一单位,可以进行加、减、乘、除运算。然而,在数组中,多个数组中每一行和每一列对应的数字是同时计算的。这些数组被称为矩阵,涉及矩阵的计算是线性代数的研究重点。
变化检测
变化检测是将同一地点在不同时间的两张图像进行对比,并突出显示这些变化的过程。变化可能是由于地面上的新建筑物的增加,或者特征的损失,如海岸侵蚀。有许多算法可以检测图像之间的变化,并确定定性因素,如变化发生的时间。
以下来自美国橡树岭国家实验室(ORNL)的研究项目的图像显示了 1984 年至 2000 年间巴西朗多尼亚州的热带雨林砍伐情况:

颜色用于显示森林被砍伐的时间。绿色代表原始雨林,白色代表在日期范围结束后的两年内被砍伐的森林,红色代表在 22 年内,其他颜色介于描述的图例之间。
直方图
直方图是数据集中值的统计分布。横轴代表数据集中唯一的值,而纵轴代表在栅格中该唯一值的频率。以下来自 NASA 的例子展示了一个被分类为不同类别的卫星图像的直方图,代表了地表特征:

直方图是大多数栅格处理中的关键操作。它可以用于从增强图像对比度到作为对象分类和图像比较的基础等一切用途。
特征提取
特征提取是将图像中的特征手动或自动数字化为点、线或多边形的过程。这个过程是图像矢量化(将栅格转换为矢量数据集)的基础。特征提取的一个例子是从卫星图像中提取海岸线并将其保存为矢量数据集。
如果这种提取在几年内进行,您可以监控这一海岸线的侵蚀或其他变化。
监督和非监督分类
地球上的物体根据其材料反射不同波长的光。在遥感中,分析师收集特定类型土地覆盖(例如,混凝土)的波长特征,并为特定区域建立一个库。然后,计算机可以使用这个库在相同区域的新图像中自动定位库中的类别。
在非监督分类中,计算机在没有任何其他参考信息(除了图像的直方图)的情况下,将具有相似反射值的光像素分组。
创建最简单的 Python GIS
现在我们对地理空间分析有了更好的理解,下一步是使用 Python 构建一个简单的 GIS,称为SimpleGIS。这个小程序将是一个技术完整的 GIS,具有地理数据模型和将数据渲染为显示不同城市人口的视觉主题地图的能力。
数据模型也将被构建,以便您可以进行基本查询。我们的SimpleGIS将包含科罗拉多州、三个城市以及每个城市的人口统计。
最重要的是,我们将通过在纯 Python 中构建这个小型系统来展示 Python 编程的强大和简单。我们将只使用标准 Python 发行版中可用的模块,而不会下载任何第三方库。
Python 入门
如我们之前所述,本书假设您对 Python 有一些基本知识。以下示例中使用的唯一模块是turtle模块,它基于 Python 中包含的 Tkinter 库提供了一个非常简单的图形引擎。如果您使用了 Windows 或 macOS 的安装程序,Tkinter 库应该已经包含在内。如果您自己编译了 Python 或者使用的是来自 Python.org 以外的发行版(www.python.org),那么请确保您可以在命令提示符中输入以下内容来导入turtle模块。这将运行turtle演示脚本:
python –m turtle
以下命令将启动一个实时绘图程序,它将展示 turtle 模块的功能,类似于以下截图:

现在我们已经看到了 turtle 图形模块能做什么,让我们用它来构建一个实际的 GIS!
构建简单的 SimpleGIS
代码分为两个不同的部分:
-
数据模型部分
-
绘制数据的地图渲染器
对于数据模型,我们将使用简单的 Python 列表。Python 列表是一种原生数据类型,它作为其他 Python 对象的容器,并按照指定顺序排列。Python 列表可以包含其他列表,非常适合简单的数据结构。如果决定进一步开发脚本,它们也很好地映射到更复杂的结构或甚至数据库。
代码的第二部分将使用 Python turtle 图形引擎来渲染地图。在 GIS 中,我们将只有一个函数,它将世界坐标——在本例中,经度和纬度——转换为像素坐标。所有图形引擎都有一个原点 (0,0),通常位于画布的右上角或左下角。
Turtle 图形设计用于通过视觉方式教授编程。turtle 图形画布使用中心的原点 (0,0),类似于图形计算器。以下图表说明了 turtle 模块使用的类型为笛卡尔图的类型。一些点在正负空间中都被绘制:

这也意味着 turtle 图形引擎可以具有负像素坐标,这在图形画布中是不常见的。然而,对于这个例子,turtle 模块是渲染我们地图最快、最简单的方法。
设置数据模型
您可以在 Python 解释器中交互式地运行此程序,或者将完整的程序保存为脚本并运行。Python 解释器是一种非常强大的学习新概念的方法,因为它会实时提供错误或意外程序行为的反馈。您可以轻松地从这些问题中恢复,并尝试其他方法,直到得到您想要的结果:
- 在 Python 中,您通常在脚本的开头导入模块,因此我们将首先导入
turtle模块。我们将使用 Python 的import功能将模块命名为t,以节省在输入turtle命令时的空间和时间:
import turtle as t
- 接下来,我们将设置数据模型,从一些简单的变量开始,这些变量允许我们通过名称而不是数字来访问列表索引,从而使代码更容易理解。Python 列表从数字
0开始索引包含的对象。因此,如果我们想访问名为myList的列表中的第一个项目,我们将如下引用它:
myList[0]
- 为了使我们的代码更容易阅读,我们还可以使用分配给常用索引的变量名:
firstItem = 0
myList[firstItem]
在计算机科学中,将常用数字分配给易于记忆的变量是一种常见做法。这些变量被称为常量。因此,对于我们的例子,我们将为所有城市使用的某些常用元素分配常量。所有城市都将有一个名称、一个或多个点和人口计数:
NAME = 0
POINTS = 1
POP = 2
- 现在,我们将设置科罗拉多州的数据,作为一个包含名称、多边形点和人口列表。请注意,坐标是一个列表内的列表:
state = ["COLORADO", [[-109, 37],[-109, 41],[-102, 41],[-102, 37]], 5187582]
- 城市将被存储为嵌套列表。每个城市的地理位置由一个经纬度对的单一点组成。这些条目将完成我们的 GIS 数据模型。我们将从一个名为
cities的空列表开始,然后为每个城市将数据追加到这个列表中:
cities = []
cities.append(["DENVER",[-104.98, 39.74], 634265])
cities.append(["BOULDER",[-105.27, 40.02], 98889])
cities.append(["DURANGO",[-107.88,37.28], 17069])
- 现在,我们将通过首先定义地图大小来将我们的 GIS 数据渲染为地图。宽度和高度可以是您想要的任何值,取决于您的屏幕分辨率:
map_width = 400
map_height = 300
- 为了将地图缩放到图形画布,我们首先必须确定最大层(即状态)的边界框。我们将地图的边界框设置为全局比例,并将其缩小到状态的大小。为此,我们将遍历每个点的经度和纬度,并将其与当前的当前最小和最大x和y值进行比较。如果它大于当前最大值或小于当前最小值,我们将此值分别设置为新的最大值或最小值:
minx = 180
maxx = -180
miny = 90
maxy = -90
forx,y in state[POINTS]:
if x < minx:
minx = x
elif x > maxx:
maxx = x
if y < miny:
miny = y
elif y > maxy:
maxy = y
- 在缩放方面,第二步是计算实际状态和我们将要渲染的微小画布之间的比例。这个比例用于坐标到像素的转换。我们得到状态沿x和y轴的大小,然后我们将地图的宽度和高度除以这些数字以获得缩放比例:
dist_x = maxx - minx
dist_y = maxy - miny
x_ratio = map_width / dist_x
y_ratio = map_height / dist_y
- 以下名为
convert()的函数是SimpleGIS中的唯一函数。它使用之前的计算将地图坐标中的一个点从我们的数据层转换成像素坐标。你会注意到,最后,我们将地图的宽度和高度各除以一半,并从最终的转换中减去,以考虑到海龟图形画布不寻常的中心原点。每个地理空间程序都有这种函数的形式:
def convert(point):
lon = point[0]
lat = point[1]
x = map_width - ((maxx - lon) * x_ratio)
y = map_height - ((maxy - lat) * y_ratio)
# Python turtle graphics start in the
# middle of the screen
# so we must offset the points so they are centered
x = x - (map_width/2)
y = y - (map_height/2)
return [x,y]
现在到了最激动人心的部分!我们准备好将我们的 GIS 渲染成专题地图。
渲染地图
turtle模块使用称为笔的光标概念。在画布上移动光标就像在一张纸上移动笔一样。当你移动光标时,光标会画线。你会注意到,在整个代码中,我们使用t.up()和t.down()命令在想要移动到新位置时抬起笔,在准备好绘图时放下笔。在这一节中,我们有几个重要的步骤要遵循,所以让我们开始吧:
- 由于科罗拉多州的边界是一个多边形,我们必须在最后一个点和第一个点之间画线以闭合多边形。我们也可以省略闭合步骤,只需将一个重复的点添加到科罗拉多州数据集中。一旦我们画出了州,我们将使用
write()方法来标注多边形:
t.up()
first_pixel = None
for point in state[POINTS]:
pixel = convert(point)
if not first_pixel:
first_pixel = pixel
t.goto(pixel)
t.down()
t.goto(first_pixel)
t.up()
t.goto([0,0])
t.write(state[NAME], align="center", font=("Arial",16,"bold"))
- 如果我们现在运行代码,我们会看到一个简化的科罗拉多州地图,如下面的截图所示:

如果你尝试运行代码,你需要在最后临时添加以下行,否则 Tkinter 窗口将在绘图完成后立即关闭:t.done()。
- 现在,我们将城市渲染为点位置,并用它们的名称和人口数进行标注。由于城市是一系列列表中的特征,我们将遍历它们以进行渲染。我们不会通过移动笔来绘制线条,而是使用海龟的
dot()方法在由我们的SimpleGISconvert()函数返回的像素坐标处绘制一个小圆圈。然后,我们将用城市的名称标注这个点,并添加人口数。你会注意到,我们必须将人口数转换为字符串才能在海龟的write()方法中使用它。为此,我们将使用 Python 的内置str()函数:
for city in cities:
pixel = convert(city[POINTS])
t.up()
t.goto(pixel)
# Place a point for the city
t.dot(10)
# Label the city
t.write(city[NAME] + ", Pop.: " + str(city[POP]),
align="left")
t.up()
-
现在,我们将执行最后一个操作来证明我们已经创建了一个真正的 GIS。我们将对我们的数据进行属性查询,以确定哪个城市的人口最多。然后,我们将执行空间查询,看看哪个城市位于最西边。最后,我们将安全地将我们的问题的答案打印在我们的专题地图页面上,超出地图的范围。
-
对于我们的查询引擎,我们将使用 Python 的内置
min()和max()函数。这些函数接受一个列表作为参数,并返回该列表的最小和最大值。这些函数有一个特殊功能,称为键参数,允许你排序复杂对象。由于我们在数据模型中处理嵌套列表,我们将利用这些函数中的键参数。键参数接受一个函数,该函数在返回最终值之前暂时改变列表的评估。在这种情况下,我们想要隔离用于比较的人口值,然后是点。我们可以编写一个全新的函数来返回指定的值,但我们可以使用 Python 的 lambda 关键字。lambda 关键字定义了一个匿名函数,它用于内联。其他 Python 函数也可以内联使用,例如字符串函数str(),但它们不是匿名的。这个临时函数将隔离我们感兴趣的价值。 -
因此,我们的第一个问题是,哪个城市的人口最多?
biggest_city = max(cities, key=lambda city:city[POP])
t.goto(0,-200)
t.write("The biggest city is: " + biggest_city[NAME])
- 下一个问题,哪个城市位于最西边?
western_city = min(cities, key=lambda city:city[POINTS])
t.goto(0,-220)
t.write("The western-most city is: " + western_city[NAME])
-
在前面的查询中,我们使用 Python 的内置
min()函数来选择最小的经度值。这是因为我们用经度和纬度对表示了我们的城市位置。使用不同的点表示方法是可能的,包括可能需要修改此代码才能正确工作的表示方法。然而,对于我们的SimpleGIS,我们使用了一种常见的点表示方法,使其尽可能直观。 -
这最后两个命令只是为了清理。首先,我们隐藏光标。然后,我们调用海龟的
done()方法,这将保持带有我们地图的海龟图形窗口打开,直到我们选择使用窗口顶部的关闭句柄来关闭它:
t.pen(shown=False)
t.done()
- 无论你是使用 Python 解释器跟随操作,还是作为脚本运行整个程序,你应该看到以下地图被实时渲染:

恭喜!你已经追随了史前猎人的脚步,GIS 之父罗杰·汤姆林森博士,地理空间先驱霍华德·费舍尔,以及改变游戏规则的程序员,共同创建了一个功能强大、可扩展且技术完善的地理信息系统。
不到 60 行纯 Python 代码就能实现!你很难找到一种编程语言,仅使用其核心库就能在如此有限的可读代码量中创建一个完整的 GIS,就像 Python 一样。即使你能做到,这种语言在接下来的这本书中你将经历的地理空间 Python 之旅中存活下来的可能性也非常低。
正如你所见,在SimpleGIS方面有大量的扩展空间。以下是一些你可能使用本节开头链接的 Tkinter 和 Python 参考材料来扩展这个简单工具的其他方法:
-
在右上角创建一个包含美国边界轮廓和科罗拉多州在美国位置的概览地图
-
添加颜色以增强视觉效果和清晰度
-
为不同的特征创建地图图例
-
列出州和城市,并添加更多州和城市
-
为地图添加标题
-
创建一个条形图来直观比较人口数量
可能性是无限的。SimpleGIS也可以用作快速测试和可视化你所遇到的地理空间算法的方式。如果你想添加更多数据层,你可以创建更多列表,但这些列表将变得难以管理。在这种情况下,你可以使用另一个包含在标准分布中的 Python 模块。SQLite 模块在 Python 中提供了一个类似 SQL 的数据库,可以保存到磁盘或运行在内存中。
摘要
干得好!你现在是一名地理空间分析师。在本章中,你了解了地理空间分析的历史及其支持的技术。你看到了莎拉·帕卡克博士的研究如何对历史产生了重大影响。你还熟悉了本书余下部分将为你服务的 GIS 和遥感基础概念。最后,你运用所有这些知识构建了一个可以扩展以实现你所能想象的一切的工作 GIS!
在下一章中,我们将探讨作为地理空间分析师你将遇到的数据格式。地理空间分析师在处理数据上花费的时间远多于实际的分析。理解你所处理的数据对于高效工作和享受乐趣至关重要。
进一步阅读
这里是一份你可能需要参考的参考文献列表:
-
如果你的 Python 发行版没有 Tkinter,你可以从以下页面找到安装信息:
tkdocs.com/tutorial/install.html -
可以在这里找到 Tkinter 的官方 Python 维基页面:
wiki.python.org/moin/TkInter。 -
Tkinter 的文档位于 Python 标准库文档中,可以在
docs.python.org/2/library/tkinter.html找到。 -
如果你刚接触 Python,由 Mark Pilgrim 编写、由 Apress 出版的《Dive into Python》是一本免费的在线书籍,涵盖了 Python 的所有基础知识,并能帮助你快速入门。更多信息,请参阅
www.diveintopython.net.
第二章:学习地理空间数据
地理空间分析中最具挑战性的方面之一是数据。地理空间数据已经包括数十种文件格式和数据库结构,并且仍在不断发展和增长,以包括新的数据类型和标准。此外,几乎任何文件格式在技术上都可以包含地理空间信息,只需简单地添加一个位置即可。
在本章中,我们将探讨以下主题:
-
了解常见的数据格式
-
检查地理空间数据的常见特征
-
理解空间索引
-
了解最广泛使用的矢量数据类型
-
理解栅格数据类型
我们还将对一些更复杂的新类型有所了解,包括点云数据、网络服务和地理空间数据库。
了解常见的数据格式
作为地理空间分析师,你可能会经常遇到以下几种常见的数据类型:
-
电子表格和逗号分隔值(CSV 文件)或制表符分隔值(TSV 文件)
-
标注地理信息的照片
-
轻量级二进制点、线和多边形
-
多吉字节卫星或航空图像
-
高程数据,如网格、点云或基于整数的图像
-
XML 文件
-
JSON 文件
-
数据库(包括服务器和文件数据库)
-
网络服务
-
地理数据库
每种格式都包含其自身的访问和处理挑战。当你对数据进行分析时,通常需要先进行某种形式的预处理。你可能需要将大区域的卫星图像裁剪或子集化到仅包含你感兴趣的区域,或者你可能需要减少集合中的点数,只保留符合你数据模型中某些标准的数据。这种类型预处理的良好例子是我们在第一章末尾查看的SimpleGIS示例,使用 Python 学习地理空间分析。该州数据集只包含科罗拉多州而不是所有 50 个州。城市数据集只包含三个样本城市,展示了三个不同的人口水平,以及不同的相对位置。
在第一章,“使用 Python 学习地理空间分析”,中提到的常见地理空间操作是该类型预处理的基础。然而,值得注意的是,地理空间分析领域已经逐渐转向易于获取的基础地图。直到大约 2004 年,地理空间数据获取困难,桌面计算能力远不及今天。预处理数据是任何地理空间项目的绝对第一步。然而,在 2004 年,谷歌发布了谷歌地图,这并不久于谷歌地球之后。微软也一直在开发一项名为TerraServer的技术收购,他们大约在这个时候重新推出了这项技术。2004 年,开放地理空间联盟(OGC)更新了其网络地图服务(WMS)的版本,该服务正在增长使用和受欢迎程度。同年,Esri 也发布了其 ArcGIS 服务器系统的第 9 版。这些创新是由谷歌的网页地图瓦片模型驱动的,它允许在不同分辨率下提供平滑、全球、可滚动的地图,通常被称为滑块地图。
在谷歌地图出现之前,人们已经在互联网上使用地图服务器,最著名的是 MapQuest 驾驶方向网站。然而,这些地图服务器一次只能提供少量数据,通常在有限的区域内。谷歌的瓦片系统将全球地图转换为图像和地图数据的分层图像瓦片。这些瓦片通过 JavaScript 和基于浏览器的XMLHttpRequest API 动态提供,更常见的是异步 JavaScript 和 XML(AJAX)。谷歌的系统可以扩展到数百万用户,使用普通的网络浏览器。更重要的是,它允许程序员利用 JavaScript 编程来创建混合应用,以便他们可以使用谷歌地图 JavaScript API 向地图添加额外的数据。混合应用的概念实际上是一个共享地理空间层系统。只要数据是网络可访问的,用户就可以将来自不同网络服务的组合和重组数据合并到一个地图中。其他商业和开源系统很快模仿了这一概念。
分布式地理空间层的显著例子是OpenLayers,它提供了一个类似谷歌的开源 API,现在已经超越了谷歌的 API,提供了额外的功能。与 OpenLayers 相辅相成的是OpenStreetMap,它是开源的,是对 OpenLayers 等系统使用的瓦片地图服务的回应。OpenStreetMap 拥有全球性的街道级矢量数据和其他空间特征,这些数据来自可用的政府数据源和来自全球数千名编辑者的贡献。OpenStreetMap 的数据维护模式类似于在线百科全书维基百科,为文章的信息创建和更新提供众包。最近,甚至出现了更多的地图 API,包括 Leaflet 和 Mapbox,它们继续在灵活性、简单性和功能上增加。
混合革命对数据产生了有趣且有益的副作用。地理空间数据传统上难以获取。收集、处理和分发数据的成本使得地理空间分析仅限于那些能够通过生产数据或购买数据来承担这种高昂的前期成本的群体。几十年来,地理空间分析一直是政府、非常大的组织和大学的工具。一旦网络地图趋势转向大规模、全球瓦片地图,组织开始基本上免费提供基础图层,以吸引开发者使用他们的平台。大规模可扩展的全球地图系统需要大规模可扩展、高分辨率的数据才能发挥作用。地理空间软件生产商和数据提供商希望保持他们的市场份额,并跟上技术趋势。
地理空间分析师从这一市场转变中受益匪浅。首先,数据提供商开始以一个称为墨卡托投影的通用投影来分发数据。墨卡托投影是一种 400 多年前引入的航海导航投影。正如我们在第一章中提到的,《用 Python 学习地理空间分析》,所有投影都有实际的好处,以及扭曲。墨卡托投影的扭曲是其大小。在全球视图中,格陵兰岛看起来比南美洲大陆还要大。然而,像每个投影一样,它也有一些好处。墨卡托保留了角度。可预测的角度使得中世纪的航海家在绘制穿越海洋的航线时能够画出直线航向线。谷歌地图最初并没有使用墨卡托投影。然而,很快就很明显,高纬度和低纬度的道路在地图上以奇特的角相遇,而不是现实中 90 度的角度。
由于谷歌地图的主要目的是提供街级驾驶方向,谷歌牺牲了全球视图的准确性,以换取在查看单个城市时街道之间的相对更好的准确性。竞争性的地图系统也效仿了这一做法。谷歌还标准化了 WGS 84 基准。这个基准定义了一个特定的地球球面模型,称为大地水准面。这个模型定义了正常海平面。谷歌做出这一选择的意义在于,全球定位系统(GPS)也使用这个基准。因此,大多数 GPS 设备默认使用这个基准,使得谷歌地图与原始 GIS 数据兼容变得容易。
墨卡托投影的谷歌变体通常被称为谷歌墨卡托。欧洲石油调查组(EPSG)为投影分配简短的数字代码,作为引用它们的一种简单方式。他们开始将投影称为 EPSG:900913,即用数字拼写的Google。后来,EPSG 分配了代码 EPSG:3857,废弃了旧代码。大多数 GIS 系统将这两个代码视为同义的。需要注意的是,谷歌对其使用的标准墨卡托投影进行了轻微的调整;然而,这种变化几乎察觉不到。谷歌在所有地图比例尺上使用球面公式,而标准墨卡托在大比例尺上假设椭圆形状。
以下墨卡托投影的图像(en.wikipedia.org/wiki/File:Tissot_mercator.png)来自维基百科:

它使用蒂索的指示器展示了由墨卡托投影引起的扭曲,该指示器在地图上投影了大小相等的椭圆。椭圆的扭曲清楚地显示了投影如何影响大小和距离:网络地图服务减少了寻找数据的工作量,并为分析师创建基础图预处理的大部分工作。然而,要创建有价值的东西,你必须了解地理空间数据以及如何处理它。本章概述了你在地理空间分析中可能会遇到的一些常见数据类型和问题。
在本章中,将常用两个术语:
-
矢量数据:矢量数据包括任何使用点、线或多边形最小表示地理定位数据的格式。
-
栅格数据:栅格数据包括任何以行和列的网格存储数据的格式。栅格数据包括所有图像格式。
这些是大多数地理空间数据集可以归组的两个主要类别。
如果你想看到更准确地显示大陆相对大小的投影,请参考古德同位素投影:en.wikipedia.org/wiki/Goode_homolosine_projection。
理解数据结构
尽管有数十种格式,地理空间数据有一些共同的特点。了解这些特点可以帮助你通过识别几乎所有空间数据共有的成分来接近和理解不熟悉的数据格式。给定数据格式的结构通常由其预期用途驱动。
一些数据是为了高效存储或压缩而优化的,一些是为了高效访问而优化的,一些是为了轻量级和可读性(网络格式)而设计的,而其他数据格式则试图包含尽可能多的不同数据类型。
有趣的是,今天一些最受欢迎的格式也是最简单的一些,甚至缺乏在更强大和复杂的格式中找到的功能。对于地理空间分析师来说,易用性非常重要,因为他们花费大量时间将数据集成到地理信息系统,以及在分析师之间交换数据。简单的数据格式最能促进这些活动。
常见特性
地理空间分析是一种将信息处理技术应用于具有地理背景的数据的方法。这个定义包含了地理空间数据最重要的元素:
-
地理位置数据:地理位置信息可以简单到地球上的一个点,指明照片拍摄的位置。它也可以复杂到卫星相机工程模型和轨道力学信息被用来重建卫星捕获图像的确切条件和位置。
-
主题信息:主题信息也可以涵盖广泛的可能。有时,图像中的像素是地面的视觉表示中的数据。其他时候,图像可能使用多光谱波段(如红外光)进行处理,以提供图像中不可见的信息。处理后的图像通常使用与键相关联的结构化调色板进行分类,该键描述了每种颜色代表的信息。其他可能性包括某种形式的数据库,其中包含每个地理定位特征的信息行和列,例如我们来自第一章“使用 Python 学习地理空间分析”的
SimpleGIS中每个城市的关联人口。
这些两个因素存在于所有可以被认为是地理空间数据的格式中。地理空间数据的另一个常见特性是空间索引。概览数据集也与索引有关。
理解空间索引
地理空间数据集通常是非常大的文件,大小轻易达到数百兆字节,甚至达到数吉字节。在执行分析时,地理空间软件在尝试反复访问大文件时可能会相当慢。
如在第一章“使用 Python 学习地理空间分析”中简要讨论的那样,空间索引创建了一个指南,允许软件快速定位查询结果,而无需检查数据集中每个单独的特征。空间索引允许软件消除可能性,并在数据的一个更小的子集上进行更详细的搜索或比较。
空间索引算法
许多空间索引算法是几十年来用于非空间信息的已建立算法的衍生。最常见的前两个空间索引算法是四叉树索引和R 树索引。
四叉树索引
四叉树算法实际上代表了一系列基于共同主题的不同算法。四叉树索引中的每个节点包含四个子节点。这些子节点通常是方形或矩形的。当一个节点包含指定数量的特征并且添加了更多特征时,节点会分裂。
将空间划分为嵌套正方形的概念可以加快空间搜索。软件只需一次处理五个点,并使用简单的大于/小于比较来检查一个点是否在节点内部。四叉树索引最常见于基于文件的索引格式。
下图显示了一个按四叉树算法排序的点数据集。黑色点是实际数据集,而框是索引的边界框。请注意,没有任何边界框重叠。左边的图显示了索引的空间表示,而右边的图显示了典型索引的层次关系,这是空间软件如何看待索引和数据的方式。
这种结构允许空间搜索算法在尝试定位一个或多个点与某些其他特征集的关系时快速排除可能性,如下面的图所示:

现在我们已经了解了四叉树索引,让我们来看看另一种常见的空间索引类型,称为 R 树。
R 树索引
R 树索引比四叉树更复杂。R 树旨在处理 3D 数据,并优化以将索引存储在兼容数据库使用磁盘空间和内存的方式。使用来自各种空间算法的算法将邻近对象分组在一起。组中的所有对象都由一个最小矩形所包围。这些矩形被聚合到每个级别都平衡的层次节点中。
与四叉树不同,R 树的边界框可能跨越节点重叠。由于它们的相对复杂性和面向数据库的结构,R 树通常在空间数据库中找到,而不是基于文件的格式中。
下图来自en.wikipedia.org/wiki/File:R-tree.svg,显示了一个用于 2D 点数据集的平衡 R 树:

索引将大型数据集分割成小块,但为了加快搜索速度,它们可能会采用一种称为网格的技术。我们将在下一节中探讨这一点。
网格
空间索引也经常采用整数网格的概念。地理坐标通常是带有 2 到 16 位小数的浮点十进制数。对浮点数进行比较的计算成本远高于使用整数。索引搜索是关于首先消除不需要精度的可能性。
因此,大多数空间索引算法将浮点坐标映射到固定大小的整数网格。在搜索特定特征时,软件可以使用更有效的整数比较而不是处理浮点数。一旦结果被缩小,软件就可以访问完整分辨率的数据。
网格大小可以是 256 x 256,对于简单的文件格式,也可以是 3 百万 x 3 百万,这对于旨在包含每个已知坐标系和可能分辨率的大型地理空间数据库来说。
整数映射技术与在地图程序中用于在图形画布上绘制数据的渲染技术非常相似。在第一章“使用 Python 学习地理空间分析”中,SimpleGIS脚本也使用这种技术,通过内置的 Python 海龟图形引擎来绘制点和多边形。
概览是什么?
概览数据最常见于栅格格式。概览是对栅格数据集进行重采样和降低分辨率的版本,提供了不同地图比例的缩略图视图或更快的图像加载视图。它们也被称为金字塔,创建它们的过程被称为金字塔化图像。这些概览通常预先处理并存储与完整分辨率数据一起,要么嵌入文件中,要么存储在单独的文件中。
这种便利性的妥协是,额外的图像会增加数据集的总文件大小;然而,它们可以加快图像查看器的速度。矢量数据也有概览的概念,通常用于在概览图中为数据集提供地理上下文。然而,由于矢量数据是可缩放的,因此通常由软件在实时通过泛化操作创建缩小尺寸的概览,如第一章“使用 Python 学习地理空间分析”中所述。
有时,矢量数据会被转换为缩略图图像,并存储在图像头中或嵌入其中。以下图表展示了图像概览的概念,直观地说明了为什么它们经常被称为金字塔:

空间索引和概览有助于加快分析软件对数据的访问速度。接下来,我们将探讨元数据,它提供了一种既适合人类阅读也适合机器阅读的方式来理解、搜索甚至编目数据。
什么是元数据?
如第一章“使用 Python 学习地理空间分析”中所述,元数据是描述相关数据集的任何数据。常见的元数据示例包括基本元素,如数据集在地球上的足迹,以及更详细的信息,如空间投影和描述数据集如何创建的信息。
大多数数据格式包含数据在地球上的足迹或边界框。详细的元数据通常存储在标准格式(如美国联邦地理数据委员会 FGDC、数字地理空间元数据内容标准 CSDGM、ISO 或较新的欧盟倡议)的单独位置,该倡议包括元数据要求,并称为欧洲共同体空间信息基础设施 INSPIRE。
理解文件结构
前述元素可以以多种方式存储在单个文件、多个文件或数据库中,具体取决于格式。此外,这种地理空间信息可以存储在多种格式中,包括嵌入的二进制标题、XML、数据库表、电子表格/CSV、单独的文本或二进制文件。
如 XML 文件、电子表格和结构化文本文件等人类可读格式,只需使用文本编辑器进行调查。这些文件也易于使用 Python 的内置模块、数据类型和字符串操作函数进行解析和处理。基于二进制格式的格式更复杂。因此,通常更容易使用第三方库来处理二进制格式。
然而,您不必使用第三方库,尤其是如果您只想从高层次调查数据的话。Python 的内置 struct 模块拥有您所需的一切。struct 模块允许您将二进制数据作为字符串读取和写入。在使用 struct 模块时,您需要了解字节序的概念。字节序是指构成文件的字节信息在内存中的存储方式。这种顺序通常是平台特定的,但在某些罕见情况下,包括形状文件,字节序会混合到文件中。
Python 的 struct 模块使用大于号 (>) 和小于号 (<) 符号来指定字节序(分别表示大端和小端)。
以下简要示例演示了如何使用 Python 的 struct 模块从 Esri 形状文件矢量数据集中解析边界框坐标。您可以从以下网址下载此形状文件作为压缩文件:github.com/GeospatialPython/Learn/blob/master/hancock.zip?raw=true。hancock.zip
当您解压时,您将看到三个文件。在这个例子中,我们将使用 hancock.shp。Esri 形状文件格式在文件头部的字节 36 到字节 37 处具有固定位置和数据类型,用于最小 x、最小 y、最大 x 和最大 y 边界框值。在这个例子中,我们将执行以下步骤:
-
导入
struct模块。 -
以二进制读取模式打开
hancock.zip形状文件。 -
定位到字节
36。 -
读取指定为
d的每个 8 字节双精度变量,并使用<符号指定的按小端顺序使用struct模块进行解包。
执行此脚本的最好方式是在交互式 Python 解释器中。然后我们将读取最小经度、最小纬度、最大经度和最大纬度:
>>> import struct
>>> f = open("hancock.shp","rb")
>>> f.seek(36)
>>> struct.unpack("<d", f.read(8))
(-89.6904544701547,) >>> struct.unpack("<d", f.read(8))
(30.173943486533133,)
>>> struct.unpack("<d", f.read(8))
(-89.32227546981174,)
>>> struct.unpack("<d", f.read(8))
(30.6483914869749,)
你会注意到,当struct模块解包一个值时,它返回一个包含一个值的 Python 元组。你可以通过一次指定所有四个双精度值并将字节长度增加到 32 字节来缩短前面的解包代码,如下面的代码所示:
>>> f.seek(36)
>>> struct.unpack("<dddd", f.read(32))
(-89.6904544701547, 30.173943486533133, -89.32227546981174,
30.6483914869749)
现在我们已经了解了如何描述数据,让我们学习最常见的一种地理空间数据类型——矢量数据。
了解最广泛使用的矢量数据类型
矢量数据无疑是地理空间中最常见的格式,因为它是最有效存储空间信息的方式。一般来说,它比栅格数据存储和处理所需的计算机资源更少。OGC 有超过 16 种与矢量数据直接相关的格式。矢量数据仅存储几何原语,包括点、线和多边形。然而,每种形状类型仅存储点。例如,在简单直线矢量线形状的情况下,只需存储和定义端点作为线。显示此数据的软件将读取形状类型,然后动态地将端点连接成线。
地理空间矢量数据类似于矢量计算机图形的概念,但有几个显著的例外。地理空间矢量数据包含基于地球的正负坐标,而矢量图形通常存储计算机屏幕坐标。地理空间矢量数据通常还与表示该几何形状的对象的其他信息相关联。这些信息可能像 GPS 数据中的时间戳那样简单,或者对于更大的地理信息系统,可能是一个完整的数据库表。
矢量图形通常存储描述颜色、阴影和其他显示相关指令的样式信息,而地理空间矢量数据通常不包含这些信息。另一个重要的区别是形状。地理空间矢量通常仅包括基于点、直线和直线多边形的非常原始的几何形状,而许多计算机图形矢量格式有曲线和圆的概念。然而,地理空间矢量可以使用更多的点来模拟这些形状。
其他可读性格式,如 CSV、简单的文本字符串、GeoJSON 和基于 XML 的格式,在技术上属于矢量数据,因为它们存储几何形状,而不是栅格,栅格表示数据集边界框内的所有数据。直到 20 世纪 90 年代末 XML 的爆炸式增长,矢量数据格式几乎都是二进制的。XML 提供了一种既适合计算机又适合人类阅读的混合方法。妥协是,与二进制格式相比,GeoJSON 和 XML 数据等文本格式会大大增加文件大小。这些格式将在本节后面讨论。
可供选择的矢量格式数量令人震惊。开源矢量库 OGR (www.gdal.org/ogr_formats.html) 列出了超过 86 种受支持的矢量格式。其商业对应产品,Safe Software 的 特征操作引擎(FME),列出了超过 188 种受支持的矢量格式 (www.safe.com/fme/format-search/#filters%5B%5D=VECTOR)。这些列表包括一些矢量图形格式以及可读的地理空间格式。仍然有数十种格式至少应该知道,以防你遇到它们。
现在,让我们看看一种特定且广泛使用的矢量数据类型,称为 Shapefile。
Shapefiles
最普遍使用的地理空间格式是 Esri Shapefile。地理空间软件公司 Esri 于 1998 年将 Shapefile 格式规范作为开放格式发布 (www.esri.com/library/whitepapers/pdfs/shapefile.pdf)。Esri 开发它作为他们 ArcView 软件的格式,ArcView 软件被设计为低端 GIS 选项,以补充他们高端专业包 ArcInfo,以前称为 ARC/INFO。然而,该格式的开放规范、效率和简单性使其成为 15 年后仍然非常受欢迎的非官方 GIS 标准。
几乎所有标有地理空间软件的软件都支持 Shapefile,因为 Shapefile 格式非常常见。因此,你可以通过熟悉 Shapefile 并主要忽略其他格式来几乎作为一个分析师来应对。你可以通过源格式本机软件或第三方转换器(如 OGR 库)将几乎任何其他格式转换为 Shapefile,对于 OGR 库,有一个 Python 模块。其他处理 Shapefile 的 Python 模块是 Shapely 和 Fiona,它们基于 OGR。
Shapefile 最显著的特点之一是它的格式由多个文件组成(从最少到最多,可以有 3-15 个不同的文件)。下表描述了文件格式。.shp、.shx 和 .dbf 文件是有效 Shapefile 所必需的:
| Shapefile 支持的文件扩展名 | 支持文件用途 | 注意事项 |
|---|---|---|
.shp |
这是 Shapefile。它包含几何形状。 | 这是一个必需的文件。一些只需要几何形状的软件可以接受没有 .shx 或 .dbf 文件的 .shp 文件。 |
.shx |
这是 Shapefile 索引文件。它是一个固定大小的记录索引,用于引用几何形状以实现更快的访问。 | 这是一个必需的文件。没有 .shp 文件,此文件没有意义。 |
.dbf |
这是数据库文件。它包含几何属性。 | 是一个必需的文件。某些软件在没有 .shp 文件的情况下也会访问此格式,因为该规范早于 shapefiles。它基于非常古老的 FoxPro 和 dBase 格式。存在一个名为 Xbase 的开放规范。.dbf 文件可以被大多数类型的电子表格软件打开。 |
.sbn |
这是空间分箱文件,即 shapefile 空间索引。 | 它包含映射到 256 x 256 整数网格的特征边界框。这个文件经常与大型 shapefile 数据集一起出现。 |
.sbx |
.sbn 文件的固定大小记录索引。 |
空间索引的传统有序记录索引。经常见到。 |
.prj |
这包含存储在知名文本格式中的地图投影信息。 | 是 GIS 软件进行即时投影的非常常见和必需的文件。此格式也可以与栅格数据一起使用。 |
.fbn |
只读特征的空間索引。 | 很少见到。 |
.fbx |
.fbn 空间索引的固定大小记录索引。 |
很少见到。 |
.ixs |
地理编码索引。 | 在地理编码应用中很常见,包括驾驶方向类型的应用。 |
.mxs |
另一种类型的地理编码索引。 | 比 .ixs 格式少见。 |
.ain |
属性索引。 | 主要为遗留格式,在现代软件中很少使用。 |
.aih |
属性索引。 | 伴随 .ain 文件。 |
.qix |
四叉树索引。 | 由开源社区创建的空间索引格式,因为 Esri 的 .sbn 和 .sbx 文件直到最近才被记录。 |
.atx |
属性索引。 | 一种更近期的 Esri 软件专用属性索引,用于加速属性查询。 |
.shp.xml |
元数据。 | 一个地理空间元数据 .xml 容器。可以是多个 XML 标准,包括 FGDC 和 ISO。 |
.cpg |
.dbf 的代码页文件。 |
用于 .dbf 文件的国际化。 |
你可能永远不会同时遇到所有这些格式。然而,你使用的任何 shapefile 都将包含多个文件。你通常会看到 .shp、.shx、.dbf、.prj、.sbn、.sbx 和偶尔的 .shp.xml 文件。如果你想重命名 shapefile,你必须将所有相关文件重命名为相同的名称;然而,在 Esri 软件和其他 GIS 软件包中,这些数据集将作为一个单一文件出现。|
shapefiles 的另一个重要特性是记录没有编号。记录包括几何形状、.shx 索引记录和 .dbf 记录。这些记录按固定顺序存储。当你使用软件检查 shapefile 记录时,它们看起来像是编号的。|
当人们删除一个 shapefile 记录,保存文件,然后重新打开它时,往往会感到困惑;被删除的记录编号仍然出现。原因是 shapefile 记录在加载时是动态编号的,但并未保存。所以,例如,如果你删除了编号为 23 的记录并保存了 shapefile,下一次读取 shapefile 时,编号 24 将变为 23。要以此方式跟踪 shapefile 记录的唯一方法是在.dbf文件中创建一个新的属性,例如 ID,并为每个记录分配一个永久且唯一的标识符。
就像重命名 shapefile 一样,在编辑 shapefile 时必须小心。最好使用将 shapefile 视为单个数据集的软件。如果你单独编辑任何文件并添加/删除记录而不编辑相关文件,大多数地理空间软件都会将 shapefile 视为损坏。
CAD 文件
CAD代表计算机辅助设计。CAD 数据的主要格式是由 Autodesk 为其领先的 AutoCAD 软件包创建的。常见的两种格式是绘图交换格式(DXF)和 AutoCAD 的本地绘图(DWG)格式。
DWG 传统上是一个封闭的格式,但它已经变得更加开放。
CAD 软件用于所有与工程相关的事物,从设计自行车到汽车、公园和城市下水道系统。作为一名地理空间分析师,你不必担心机械工程设计;然而,土木工程设计成为一个相当大的问题。大多数工程公司只有限度地使用地理空间分析,但几乎将所有数据存储在 CAD 格式中。DWG 和 DXF 格式可以使用地理空间软件中找不到或仅由地理空间系统弱支持的特性来表示对象。以下是一些这些特性的例子:
-
曲线
-
表面(用于与地理空间高程表面不同的对象)
-
3D 实体
-
文本(以对象的形式渲染)
-
文本样式
-
视口配置
这些 CAD 和工程特定特性使得将 CAD 数据干净地转换为地理空间格式变得困难。如果你遇到 CAD 数据,最简单的选择是询问数据提供者他们是否有 shapefile 或其他以地理空间为中心的格式。
基于标签和标记的格式
基于标签的标记格式通常是 XML 格式。它们还包括其他结构化文本格式,如已知文本(WKT)格式,该格式用于投影信息文件以及不同类型的数据交换。
XML 格式包括 Keyhole 标记语言(KML)、OpenStreetMap(OSM)格式以及用于 GPS 数据的 Garmin GPX 格式,这些格式已成为流行的交换格式。开放地理空间联盟的 地理标记语言(GML)标准是其中最古老且最广泛使用的基于 XML 的地理格式之一。它也是 OGC 网络要素服务(WFS)标准的基础,该标准用于网络应用程序。然而,GML 已经被 KML 和 GeoJSON 格式所取代。
XML 格式通常包含的不仅仅是几何信息。它们还包含属性和渲染指令,如颜色、样式和符号。Google 的 KML 格式已成为一个完全支持的 OGC 标准。以下是一个简单的 KML 示例,展示了一个标记:
<?xml version="1.0" encoding="utf-8"?>
<kml >
<Placemark>
<name>Mockingbird Cafe</name>
<description>Coffee Shop</description>
<Point>
<coordinates>-89.329160,30.310964</coordinates>
</Point>
</Placemark>
</kml>
XML 格式对地理空间分析师有吸引力的原因如下:
-
它是一种人类可读的格式。
-
它可以在文本编辑器中编辑。
-
它得到了编程语言的良好支持(尤其是 Python)。
-
根据定义,它很容易扩展。
尽管如此,XML 并不完美。它对于非常大的数据格式来说是一个低效的存储机制,并且很快就会变得难以编辑。数据集中的错误很常见,而且大多数解析器都没有稳健地处理错误。尽管存在缺点,XML 在地理空间分析中仍然被广泛使用。
可缩放矢量图形(SVG)是一种广泛支持的 XML 格式,用于计算机图形。它得到了浏览器的良好支持,常用于地理空间渲染。然而,SVG 并非作为地理格式而设计的。
WKT 格式也是一种较老的 OGC 标准。它最常见的使用是定义通常存储在 .prj 投影文件中的投影信息,以及与 shapefile 或栅格一起。WGS 84 坐标系的 WKT 字符串如下:
GEOGCS["WGS 84",
DATUM["WGS_1984",
SPHEROID["WGS 84",6378137,298.257223563,
AUTHORITY["EPSG","7030"]],
AUTHORITY["EPSG","6326"]],
PRIMEM["Greenwich",0,
AUTHORITY["EPSG","8901"]],
UNIT["degree",0.01745329251994328,
AUTHORITY["EPSG","9122"]],
AUTHORITY["EPSG","4326"]]
定义投影的参数可能相当长。由 EPSG 创建的一个标准委员会引入了一个数值编码系统来引用投影。这些代码,如 EPSG:4326,用作前面代码的简写。还有一些常用投影的简称,如墨卡托投影,可以在不同的软件包中用来引用投影。
关于这些参考系统更多信息可以在空间参考网站上找到:spatialreference.org/ref/。
GeoJSON
GeoJSON 是一种基于 JavaScript 对象表示法(JSON)的相对较新且出色的文本格式,这种格式多年来一直是一种常用的数据交换格式。尽管它的历史很短,GeoJSON 仍然可以嵌入到所有主要的地理空间软件系统和大多数分发数据的网站上。这是因为 JavaScript 是动态网页的语言,GeoJSON 可以直接输入到 JavaScript 中。
GeoJSON 是对流行的 JSON 格式的一个完全向后兼容的扩展。JSON 的结构非常相似,在某些情况下,与常见编程语言的现有数据结构完全相同。JSON 几乎与 Python 的字典和列表数据类型相同。由于这种相似性,在脚本中从头开始解析 JSON 很简单,但有许多库可以使它变得更加容易。Python 包含一个名为 json 的内置库。
GeoJSON 为您提供了一种标准的方式来定义几何形状、属性、边界框和投影信息。GeoJSON 具有 XML 的所有优点,包括人类可读的语法、优秀的软件支持以及在行业中的广泛应用。它还超越了 XML。
GeoJSON 比 XML 更加紧凑,这主要是因为它使用简单的符号来定义对象,而不是使用包含大量文本的打开和关闭标签。这种紧凑性也有助于大型数据集的可读性和可管理性。然而,从数据量方面来看,它仍然不如二进制格式。以下是一个 GeoJSON 语法示例,定义了一个包含点和线的几何集合:
{ "type": "GeometryCollection",
"geometries": [
{ "type": "Point",
"coordinates": [-89.33, 30.0]
},
{ "type": "LineString",
"coordinates": [ [-89.33, 30.30], [-89.36, 30.28] ]
}
{"type": "Polygon",
"coordinates": [[
[-104.05, 48.99],
[-97.22, 48.98]
}
]
}
上述代码是有效的 GeoJSON,但它也是一个有效的 Python 数据结构。您可以直接将上述代码示例复制到 Python 解释器中作为变量定义,它将无错误地评估,如下所示:
gc = { "type": "GeometryCollection",
"geometries": [
{ "type": "Point",
"coordinates": [-89.33, 30.0]
},
{ "type": "LineString",
"coordinates": [ [-89.33, 30.30], [-89.36, 30.28] ]
}
]
}
gc
{'type': 'GeometryCollection', 'geometries': [{'type': 'Point',
'coordinates': [
-89.33, 30.0]}, {'type': 'LineString', 'coordinates': [[-89.33,
30.3], [-89.36,30.28]]}]}
由于其紧凑的尺寸、JavaScript 编写的互联网友好语法以及主要编程语言的支持,GeoJSON 是领先 REST 地理空间 Web API 的关键组件,这将在本章后面讨论。它目前在计算机资源效率、文本格式的可读性和程序性效用之间提供了最佳折衷。
GeoPackage
我们将简要介绍 GeoPackage 格式,因为它在 第三章,地理空间技术景观 中有所涉及,以及因为它是一种地理数据库。geopackage 格式是一个基于 SQLite 文件数据库容器的 OGC 开放标准,它是平台、供应商和软件独立的。它试图摆脱由专有数据格式或有限数据格式引起的所有问题。
接下来,我们将探讨其他主要数据类型:栅格数据。
理解栅格数据类型
栅格数据由行和列的单元格或像素组成,每个单元格代表一个单一值。将栅格数据视为图像是思考它的最简单方式,这也是它们通常由软件表示的方式。然而,栅格数据集不一定以图像的形式存储。它们也可以是数据库中的 ASCII 文本文件或 二进制大对象(BLOBs)。
地理空间栅格数据与常规数字图像之间的另一个区别是它们的分辨率。如果以全尺寸打印,数字图像将分辨率表示为每英寸点数。分辨率也可以表示为图像中的总像素数,并定义为百万像素。然而,地理空间栅格数据使用每个单元格代表的地面距离。例如,具有两英尺分辨率的栅格数据集意味着单个单元格代表地面上的两英尺,这也意味着在数据集中只能视觉识别大于两英尺的物体。
栅格数据集可能包含多个波段,这意味着可以在同一区域同时收集不同波长的光。通常,这个范围是 3-7 个波段,但在高光谱系统中可能有几百个波段。这些波段可以单独查看,或者像图像的 RGB 波段一样互换。它们还可以通过数学方法重新组合成一个单波段派生图像,然后使用代表数据集中值的固定数量的类别重新着色。
栅格数据在科学计算领域的应用也很常见,它共享了许多地理空间遥感元素,但增加了一些有趣的变体。科学计算通常使用复杂的栅格格式,包括网络公共数据格式(NetCDF)、GRIB和HDF5,这些格式存储整个数据模型。这些格式更像是文件系统中的目录,可以包含多个数据集或同一数据集的多个版本。海洋学和气象学是这类分析最常见应用。一个科学计算数据集的例子是天气模型的输出,其中栅格数据集在不同波段中的单元格可能代表模型在时间序列中不同变量的输出。
与矢量数据一样,栅格数据也可以以各种格式存在。开源的地理空间数据抽象库(GDAL)raster库,实际上还包括我们之前提到的矢量 OGR 库,列出了超过 130 种支持的栅格格式(www.gdal.org/formats_list.html)。FME 软件包也支持这么多格式。然而,就像 shapefile 和 CAD 数据一样,也有一些突出的栅格格式。
TIFF 文件
标记图像文件格式(TIFF)是最常见的地理空间栅格格式。TIFF 格式的灵活标记系统允许它在单个文件中存储任何类型的数据。TIFF 文件可以包含概览图像、多个波段、整数高程数据、基本元数据、内部压缩以及其他各种数据,这些数据通常由其他格式存储在额外的支持文件中。任何人都可以通过向文件结构添加标记数据来非官方地扩展 TIFF 格式。这种可扩展性既有优点也有缺点。然而,一个 TIFF 文件可能在某个软件中运行良好,但在另一个软件中访问时可能会失败,因为这两个软件包对庞大的 TIFF 规范的实施程度不同。关于 TIFF 的一个古老笑话中有许多令人沮丧的真实性:TIFF 代表 数千种不兼容的文件格式。GeoTIFF 扩展定义了地理空间数据是如何存储的。存储为 TIFF 文件的地理空间栅格可能具有以下任何一种文件扩展名:.tiff、.tif 或 .gtif。
JPEG、GIF、BMP 和 PNG
JPEG、GIF、BMP 和 PNG 格式是通用的图像格式,但也可以用于基本地理空间数据存储。通常,这些格式依赖于伴随支持文本文件来对信息进行地理参考,以便与 GIS 软件兼容,如 WKT、.prj 或世界文件。
JPEG 格式在地理空间数据中也很常见。JPEGs 有一个内置的元数据标记系统,类似于 TIFF,称为 EXIF。JPEGs 常用于地理标记照片,以及栅格 GIS 层。位图(BMP)图像用于桌面应用程序和文档图形。然而,JPEG、GIF 和 PNG 是在网络地图应用中使用,尤其是在用于通过滑块地图快速访问的预生成服务器地图瓦片中的格式。
压缩格式
由于地理空间栅格往往非常大,它们通常使用高级压缩技术进行存储。最新的开放标准是 JPEG 2000 格式,它是 JPEG 格式的一个升级,包括小波压缩和一些其他功能,如地理参考数据。多分辨率无缝图像数据库(MrSID)(.sid)和增强型压缩小波(ECW)(.ecw)是两种在地理空间环境中常见的专有波压缩格式。
TIFF 格式支持压缩,包括 Lempel-Ziv-Welch(LZW)算法。需要注意的是,压缩数据适合作为基础地图的一部分,但不应用于遥感处理。压缩图像旨在在视觉上正确显示,但通常会改变原始单元格值。无损压缩算法试图避免降低源数据,但通常认为对经过压缩的数据进行光谱分析是一个糟糕的主意。JPEG 格式旨在成为一个有损格式,牺牲数据以换取更小的文件大小。它也很常见,因此记住这个事实很重要,以避免得到无效的结果。
ASCII 网格
另一种存储栅格数据的方式,通常是高程数据,是使用 ASCII 网格文件。这种文件格式最初由 Esri 创建,但已成为大多数软件包支持的非官方标准。ASCII 网格是一个简单的文本文件,包含作为行和列的 (x, y) 值。栅格的空间信息包含在简单的标题中。文件格式如下:
<NCOLS xxx>
<NROWS xxx>
<XLLCENTER xxx | XLLCORNER xxx>
<YLLCENTER xxx | YLLCORNER xxx>
<CELLSIZE xxx>
{NODATA_VALUE xxx}
row 1
row 2
.
.
.
row n
虽然不是存储数据的最高效方式,但 ASCII 网格文件因其不需要任何特殊的数据库来创建或访问地理空间栅格数据而非常流行。这些文件通常以 .zip 文件的形式分发。前述格式中的标题值包含以下信息:
-
列数
-
行数
-
x-轴单元格中心坐标 | x-轴左下角坐标
-
y-轴单元格中心坐标 | y-轴左下角坐标
-
在地图单位中的单元格大小
-
无数据值(通常是 9,999)
世界文件
世界文件是简单的文本文件,可以为任何图像提供地理空间参考信息,这些图像通常没有对空间信息提供原生支持,包括 JPEG、GIF、PNG 和 BMP。世界文件因其命名约定而被地理空间软件识别。最常见的世界文件命名方式是使用栅格文件名,然后更改扩展名以删除中间字母,并在末尾添加 w。
下表展示了不同格式的栅格图像及其根据惯例关联的世界文件名的一些示例:
| 栅格文件名 | 世界文件名 |
|---|---|
World.jpg |
World.jgw |
World.tif |
World.tfw |
World.bmp |
World.bpw |
World.png |
World.pgw |
World.gif |
World.gfw |
世界文件的结构非常简单。它是一个六行文本文件,如下所示:
-
第 1 行:沿 x-轴的单元格大小(地面单位)
-
第 2 行:沿 y-轴的旋转
-
第 3 行:沿 x-轴的旋转
-
第 4 行:沿 y-轴的单元格大小(地面单位)
-
第 5 行:左上单元格的中心 x-坐标
-
第 6 行:左上单元格的中心 y-坐标
以下是世界文件值的示例:
15.0
0.0
0.0
-15.0
-89,38
45.0
第 1、4、5 和 6 行包含的(x, y)坐标和(x, y)单元格大小,允许您计算任何单元格的坐标或一组单元格之间的距离。旋转值对于地理空间软件很重要,因为遥感图像通常由于数据收集平台的原因而旋转。
旋转图像存在重新采样数据的风险,因此可能会丢失数据,因此旋转值允许软件考虑这种扭曲。图像周围的像素通常被分配一个无数据值,并以黑色表示。
以下图像由美国地质调查局(U.S. Geological Survey,USGS)提供,来自viewer.nationalmap.gov/advanced-viewer/,展示了图像旋转,其中卫星收集路径从东南向东北方向延伸,但底图是北向:

在 Python 处理栅格数据时,世界文件是一个非常有用的工具。大多数地理空间软件和数据库支持世界文件,因此在地理参照时通常是一个不错的选择。
您会发现世界文件非常有用,但由于您不经常使用它们,您可能会忘记未标记的内容代表什么。有关世界文件的快速参考可在kralidis.ca/gis/worldfile.htm找到。
向量数据和栅格数据是两种最常见的数据类型。然而,由于收集成本逐渐降低,另一种类型的数据正在变得越来越受欢迎。这种类型是点云数据,我们将在下一节中对其进行探讨。
点云数据是什么?
点云数据是指基于某种聚焦能量返回的表面点的(x, y, z)位置收集的任何数据。这可以使用激光、雷达波、声纳或其他波形生成设备创建。点之间的间距是任意的,并且取决于收集数据的传感器的类型和位置。
在这本书中,我们将主要关注 LIDAR 数据和雷达数据。雷达点云数据通常在太空任务中收集,而 LIDAR 通常由地面或空中车辆收集。从概念上讲,这两种类型的数据是相似的。
LIDAR
LIDAR 使用强大的激光测距系统以非常高的精度来模拟世界。术语LIDAR,或 LiDAR,是光和雷达这两个词的组合。有些人声称它还代表光探测与测距。LIDAR 传感器可以安装在空中平台上,包括卫星、飞机或直升机。它们也可以安装在车辆上进行地面收集。
由于激光雷达提供的高速、连续数据收集和广阔的视野——通常是传感器的 360 度——激光雷达数据通常不具有其他形式栅格数据那样的矩形足迹。激光雷达数据集通常被称为点云,因为数据是一系列(x,y,z)位置,其中z是从激光到检测到的对象的距离,(x,y)值是从传感器位置计算出的对象的投影位置。
以下图像,由美国地质调查局提供,展示了一个城市区域使用地面传感器获取的点云激光雷达数据集,而不是空中传感器。颜色基于激光能量返回的强度,红色区域靠近激光雷达传感器,绿色区域较远,这可以精确到几厘米的高度:

激光雷达数据最常见的数据格式是激光雷达交换格式(LAS),这是一个社区标准。激光雷达数据可以用多种方式表示,包括每行一个简单的(x, y, z)元组的文本文件。有时,可以使用同时收集到的图像像素颜色对激光雷达数据进行着色。激光雷达数据还可以用于创建 2D 高程栅格。
这种技术是激光雷达在地理空间分析中最常见的用途。任何其他用途都需要专门的软件,允许用户在 3D 中进行工作。在这种情况下,其他地理空间数据不能与点云结合。
什么是网络服务?
地理空间网络服务允许用户在网络上进行数据发现、数据可视化和数据访问。网络服务通常通过基于用户输入的应用程序访问,例如放大在线地图或搜索数据目录。最常用的协议是网络地图服务(WMS),它返回一个渲染的地图图像,以及网络要素服务(WFS),它通常返回 GML,这在本章的引言中已提到。
许多 WFS 服务也可以返回 KML、JSON、压缩的 shapefile 和其他格式。这些服务通过 HTTP GET请求调用。以下 URL 是一个 WMS GET请求的示例,它返回一个 640 像素宽、400 像素高的世界地图图像,具有 EPSG 代码 900913:ows.mundialis.de/services/service?SERVICE=wms&VERSION=1.1.1&REQUEST=GetMap&FORMAT=image/png&STYLES=&WIDTH=600&HEIGHT=400&LAYERS=TOPO-OSM-WMS&SRS=EPSG:900913&BBOX=-20037508,-20037508,20037508,20037508。链接
网络服务正在迅速发展。开放地理信息系统联盟正在为传感器网络和其他地理空间环境添加新的标准。表示状态转移(REST)服务也被广泛使用。REST 服务使用简单的 URL,通过相应地定制 URL 参数及其值,使得在几乎任何编程语言中实现数据请求变得非常容易。几乎每种编程语言都有强大的 HTTP 客户端库,能够使用 REST 服务。
这些 REST 服务可以返回多种类型的数据,包括图像、XML 或 JSON。目前还没有统一的地理空间 REST 标准,但 OGC 已经在这方面工作了一段时间。Esri 已经创建了一个工作实现,目前被广泛使用。
以下 URL 是一个 Esri 地理空间 REST 服务的示例,该服务基于天气雷达图像层返回 KML。您可以将此 URL 添加到 Google Earth 中作为网络链接,或者您可以在浏览器中将其下载为压缩的 KML(KMZ)文件以导入到另一个程序:idpgis.ncep.noaa.gov/arcgis/rest/services/NWS_Observations/radar_base_reflectivity/MapServer/generateKml?docName=NWSRadar&layers=0&layerOptions=separateImage。
您可以在以下链接找到关于 OGC 服务的教程:cite.opengeospatial.org/pub/cite/files/edu/fundamental-concepts/text/basic.html。
在撰写本书时,开放地理空间联盟(OGC)正在经历一次 API 演变,这将通过 REST、OpenAPI、JSON/HTML 和 Swagger 等技术显著降低使用地理空间 API 的门槛。您可以通过 OGC 的技术路线图跟踪这些趋势:github.com/opengeospatial/OGC-Technology-Trends。
现在,我们将从单个文件格式转向功能强大的地理数据库,通过单个 API 整合数据。
理解地理空间数据库
地理空间数据库,或称地理数据库,指的是一类文件格式、数据模式,甚至软件。在第三章《地理空间技术景观》中,我们将介绍地理数据库作为软件包,正式称为数据库管理系统。但在此节中,我们将描述它们作为文件格式的属性。地理数据库历史上仅存储矢量数据,尽管现代地理数据库也非常适合进行栅格数据管理。
地理数据库可以表现出我们之前提到的所有常见特征。这些信息存储在数据库中,我们称之为数据库模型。一个非常流行的模型是传统的关联模型,它使用行和列的表格。每一行和列的组合称为单元格。行可以通过指定列与其他表相关联,以使用每个单元格作为引用另一个表中单元格的关键,从而将行连接起来。
列的实际名称和数据之间的关系构成了数据定义。至少,地理数据库将几何描述与表示该几何对象的属性关联起来。单个点通常由x和y列表示。然而,多边形和多折线有任意数量的点。这意味着地理数据库通常将几何信息作为BLOB存储,使用一种称为已知二进制或WKB的格式标准。
属性信息通常定义为整数、浮点小数、字符串或日期等数据类型。表格还可以包括用于地图显示的投影信息,以及一个用于加速搜索和地理空间比较的空间索引列。地理数据库还可能有一个相关的表,以便链接关于地理空间数据的详细元数据。
大型地理空间栅格数据集很少直接存储在数据库中。通常,栅格数据存储在磁盘上,并带有名称,同时在数据库中存储一个文件系统引用,该引用指向栅格数据。地理数据库还可能存储一个表示栅格数据地面足迹的几何列,然后可以用作地理空间操作的代理。
摘要
您现在拥有了处理常见类型地理空间数据所需的背景知识。您还了解了地理空间数据集的常见特征,这将使您能够评估不熟悉的数据类型,并识别出将引导您选择使用哪些工具与这些数据进行交互的关键元素。
在下一章中,我们将探讨您可以使用以处理地理空间数据集的模块和库。我们将了解地理空间技术生态系统,它由数千个软件库和包组成。我们还将了解地理空间软件的层次结构以及它是如何帮助您快速理解和评估任何地理空间工具的。
进一步阅读
您可以在此处找到关于 OGC 服务的教程:cite.opengeospatial.org/pub/cite/files/edu/fundamental-concepts/text/basic.html.
第三章:地理空间技术景观
地理空间技术生态系统由数百个软件库和包组成。对于地理空间分析的新手来说,如此众多的选择可能会令人感到不知所措。快速学习地理空间分析的秘诀在于理解那些真正重要的少数库和包。大多数软件,无论是商业的还是开源的,都是从这些关键包派生出来的。了解地理空间软件生态系统及其使用方法,可以帮助你快速理解和评估任何地理空间工具。
地理空间库可以被分配到以下一个或多个高级核心能力中,它们以某种程度实现这些能力。在本章中,我们将学习这些能力:
-
数据访问
-
计算几何(包括数据重投影)
-
图像处理
-
可视化工具
-
元数据工具
在本章中,我们将探讨对地理空间分析影响最大的软件包,以及你可能会经常遇到的软件包。然而,与任何信息过滤一样,我们鼓励你进行自己的研究并得出自己的结论。
以下网站提供了本章未包含的软件的更多信息:
-
维基百科 GIS 软件列表:
en.wikipedia.org/wiki/List_of_geographic_information_systems_software -
OSGeo 项目列表和孵化器项目:
www.osgeo.org
图像处理软件能力用于遥感。然而,这类软件非常碎片化,包含数十个软件包,很少被集成到派生软件中。大多数遥感图像处理软件基于相同的数据访问库,并在其之上实现了定制的图像处理算法。
查看以下这些类型软件的示例,包括开源和商业软件包:
-
开源软件图像地图(OSSIM)
-
地理资源分析支持系统(GRASS)
-
Orfeo 工具箱(OTB)
-
ERDAS IMAGINE
-
ENVI
技术要求
以下是该章节的技术要求列表:
-
Python 3.6 或更高版本
-
RAM:最低 6 GB(Windows),8 GB(macOS);推荐 8 GB
-
存储:最低 7200 RPM SATA,可用空间 20 GB;推荐 SSD,可用空间 40 GB
-
处理器:最低要求 Intel Core i3 2.5 GHz;推荐 Intel Core i5
理解数据访问
如第二章中所述,学习地理空间数据,地理空间数据集通常是大型、复杂且多样化的。这一挑战使得能够高效读取(在某些情况下写入)这些数据的库变得对地理空间分析至关重要。没有数据访问,地理空间分析无法开始。
此外,准确性和精度是地理空间分析中的关键因素。未经许可重新采样数据的图像库,或者四舍五入坐标甚至只有几个小数的计算几何库,都可能对分析质量产生不利影响。此外,这些库必须高效地管理内存。复杂的地理空间过程可能持续数小时,甚至数天。
如果数据访问库存在内存故障,它可能会延迟整个项目,甚至整个工作流程,涉及数十人,他们依赖于该分析的结果。
数据访问库,如地理空间数据抽象库(GDAL),主要是为了速度和跨平台兼容性而用 C 或 C++编写的。由于地理空间数据集通常很大,速度很重要。然而,你也会看到许多用 Java 编写的包。当编写得很好时,纯 Java 可以接近处理大型矢量或栅格数据集的可接受速度,这对于大多数应用通常是可接受的。
下面的概念图显示了主要的地理空间软件库和包以及它们之间的关系。粗体的库代表积极维护的根库,并且没有显著地源自其他库。这些根库代表地理空间操作,这些操作相当难以实现,绝大多数人选择使用这些库之一,而不是创建一个竞争者。正如你所看到的,少数库构成了不成比例的地理空间分析软件。下面的图表远非详尽无遗。在这本书中,我们将只讨论最常用的包:

GDAL、GEOS(代表Geometry Engine - Open Source)和PROJ库是商业和开源两方面的地理空间分析社区的灵魂。重要的是要注意,这些库都是用 C 或 C++编写的。还有大量的工作是用 Java 完成的,形式是GeoTools和Java Topology Suite(JTS)核心库,这些库被广泛应用于桌面、服务器和移动软件。鉴于有数百个地理空间包可用,几乎所有这些包都依赖于这些库来完成任何有意义的工作,你将开始了解地理空间数据访问和计算几何的复杂性。将这个软件领域与文本编辑器进行比较,在开源项目网站上搜索时,会返回超过 5,000 个选项(sourceforge.net/)。
地理空间分析是一个真正的全球社区,该领域的重要贡献来自全球的每一个角落。但是,当你更多地了解软件景观中心的重量级包时,你会发现这些程序往往来自加拿大,或者是由加拿大开发者大量贡献的。
被誉为现代 GIS 的摇篮,地理空间分析是国家自豪的事情。此外,加拿大政府和公私合营的 GeoConnections 项目在研究和公司方面投入了大量资金,既是为了经济原因推动行业发展,也是出于必要性——更好地管理国家丰富的自然资源和满足人口需求。
GDAL
GDAL 在地理空间行业中承担了最繁重的任务。GDAL 网站列出了超过 80 个使用该库的软件,而这个列表远非完整。其中许多包是行业领先的开源和商业工具。这个列表不包括数百个较小的项目和独立分析师,他们正在使用该库进行地理空间分析。GDAL 还包括一组命令行工具,可以在不进行任何编程的情况下执行各种操作。
可以在以下 URL 找到使用 GDAL 的项目列表:trac.osgeo.org/gdal/wiki/SoftwareUsingGdal。
GDAL 和栅格数据
GDAL 为地理空间行业中发现的众多栅格数据类型提供了一个单一、抽象的数据模型。它整合了不同格式的独特数据访问库,并为读取和写入数据提供了一个共同的 API。在开发者 Frank Warmerdam 在 20 世纪 90 年代末创建 GDAL 之前,每种数据格式都需要一个不同的数据访问库和 API 来读取数据,或者在最坏的情况下,开发者经常编写自定义数据访问例程。
以下图表提供了 GDAL 如何抽象栅格数据的视觉描述:

在先前的软件概念图中,你可以看到 GDAL 对任何单一地理空间软件的影响最大。将 GDAL 与其姐妹库 OGR 结合,用于矢量数据,其影响几乎翻倍。PROJ 库也产生了巨大的影响,但它通常是通过 OGR 或 GDAL 访问的。
GDAL 的主页可以在www.gdal.org/找到。
GDAL 和矢量数据
除了栅格数据外,GDAL 列出了至少对 70 多种矢量数据格式的部分支持。GDAL 包成功的一部分是其 X11/MIT 开源许可证。这个许可证既适合商业也适合开源友好。GDAL 库可以包含在专有软件中,而无需向用户透露专有源代码。
GDAL 具有以下矢量功能:
-
统一的矢量数据和建模抽象
-
矢量数据重投影
-
矢量数据格式转换
-
属性数据过滤
-
基本几何过滤,包括裁剪和点在多边形内测试
GDAL 有几个命令行实用程序,展示了其在向量化数据方面的能力。这种能力也可以通过其编程 API 访问。以下图概述了 GDAL 向量化架构:

考虑到该模型能够表示 70 多种不同的数据格式,GDAL 的向量化架构相当简洁:
-
几何形状: 此对象代表开放地理空间联盟(OGC)简单特征规范的数据模型,包括点、线字符串、多边形、几何集合、多边形、多点和多线字符串。
-
要素定义: 此对象包含一组相关要素的属性定义。
-
要素: 此对象将几何形状和要素定义信息联系起来。
-
空间参考: 此对象包含 OGC 空间参考定义。
-
图层: 此对象代表在数据源中按图层分组的功能。
-
数据源: 此对象是 GDAL 访问的文件或数据库对象。
-
驱动器: 此对象包含 GDAL 可用的 70 多种数据格式的转换器。
这种架构运行顺畅,只有一个小的瑕疵——即使对于只包含单个图层的格式,也使用了图层概念。例如,Shapefiles 只能表示单个图层。但是,当你使用 GDAL 访问 Shapefile 时,你仍然必须使用不带文件扩展名的 Shapefile 的基本名称调用一个新的图层对象。这个设计特性只是一个小的不便,但 GDAL 提供的强大功能远远超过了这一点。
现在,让我们超越访问数据,转向使用它进行分析。
理解计算几何
计算几何包括执行向量化数据操作所需的算法。该领域在计算机科学中非常古老;然而,由于地理空间坐标系,用于地理空间操作的库大多与计算机图形库分开。如第一章结尾所述,使用 Python 学习地理空间分析,计算机屏幕坐标几乎总是用正数表示,而地理空间坐标系在向西和向南移动时通常使用负数。
几种不同的地理空间库属于这一类别,但它们也服务于广泛的用途,从空间选择到渲染。需要注意的是,GDAL 之前描述的一些功能使其超越了数据访问的范畴,进入了计算几何领域。但是,它被包含在前者类别中,因为那是其主要目的。
计算几何是一个迷人的主题。当编写一个简单的脚本来自动化地理空间操作时,你不可避免地需要空间算法。那么问题就来了,你是尝试自己实现这个算法,还是通过使用第三方库来承担额外的开销? 这个选择总是具有欺骗性,因为有些任务看起来容易理解和实现,有些看起来复杂但实际上很容易,还有些是容易理解的,但实现起来却异常困难。一个这样的例子就是地理空间缓冲区操作。
概念本身足够简单,但算法实际上相当困难。本节中以下库是用于计算几何算法的主要软件包。
PROJ 投影库
美国地质调查局(USGS)分析师 Jerry Evenden 在 1990 年代中期在美国地质调查局工作时创建了现在所知的 PROJ 投影库。从那时起,它已成为 开源地理空间基金会(OSGeo)的项目,许多其他开发者也做出了贡献。PROJ 完成了在数千个坐标系之间转换数据的艰巨任务。在这么多坐标系之间转换点所需的数学非常复杂。没有其他库能接近 PROJ 的能力。这一事实以及应用程序需要执行的将来自不同来源的数据集转换为通用投影的常规操作,使 PROJ 在这个领域成为无可争议的领导者。
下面的图表是 PROJ 支持的投影可以多么具体的例子。此图表来自 calcofi.org,代表 加利福尼亚合作海洋渔业调查(CalCOFI)项目的伪投影,该投影仅由 NOAA(即 国家海洋和大气管理局)、加州大学圣克鲁兹分校海洋学研究所和加利福尼亚州鱼类和野生动物部门在过去 60 年中沿加利福尼亚海岸收集海洋学和渔业数据时使用:

PROJ 使用一种简单的语法,能够描述任何投影,包括自定义的本地化投影,如前一个图表所示。PROJ 几乎可以在每个主要的 GIS 软件包中找到,提供重投影支持,并且它还有自己的命令行工具。
它可以通过 GDAL 提供矢量数据和栅格数据。然而,直接访问库通常很有用,因为它让您能够重新投影单个点。大多数包含 PROJ 的库只允许您重新投影整个数据集。
想了解更多关于 PROJ 的信息,请访问 proj4.org.
CGAL
计算几何算法库(CGAL)最初于 20 世纪 90 年代末发布,是一个强大且成熟的开源计算几何库。它并非专门为地理空间分析设计,但在该领域被广泛使用。
CGAL 经常被引用为可靠几何处理算法的来源。以下来自CGAL 用户和参考手册的图示,提供了一种 CGAL 中经常引用的算法的可视化,称为多边形直线骨架,这是准确放大或缩小多边形所需的:

直线骨架算法复杂且重要,因为缩放或放大多边形不仅仅是使其变大或变小的问题。多边形实际上会改变形状。当多边形缩小时,非相邻边会碰撞并消除连接边。当多边形放大时,相邻边会分离,并形成新边来连接它们。这个过程是地理空间多边形缓冲的关键。以下来自CGAL 用户和参考手册的图示,通过在先前多边形上的内嵌展示了这一效果:

CGAL 可在www.cgal.org/在线找到。
JTS
JTS 是一个 100%纯 Java 编写的地理空间计算几何库。JTS 通过实现 OGC 简单特征规范为 SQL 来将自己与其他计算几何库区分开来。有趣的是,其他开发人员已经将 JTS 移植到其他语言,包括 C++、Microsoft .NET,甚至 JavaScript。
JTS 包含一个名为 JTS TestBuilder 的出色测试程序,它提供了一个 GUI 来测试功能,而无需设置整个程序。地理空间分析中最令人沮丧的方面之一是奇怪的几何形状,这些形状会破坏大多数情况下都能正常工作的算法。另一个常见问题是由于数据中的微小错误(如多边形在非常小的、不易察觉的区域相交)而导致的意外结果。JTS TestBuilder 允许您交互式地测试 JTS 算法以验证数据,或者只是直观地理解一个过程,如下所示:

这个工具即使不使用 JTS,也可以在其他几种语言端口中使用,非常方便。需要注意的是,JTS 的维护者 Vivid Solutions 自 2006 年 12 月 JTS 版本 1.8 以来就没有发布过新版本。这个包非常稳定,仍在积极使用中。
JTS 的主页可在locationtech.github.io/jts.找到。
GEOS
GEOS 是之前解释过的 JTS 库的 C++ 版本。这里提到它是因为这个版本对地理空间分析的影响比原始的 JTS 大得多。C++ 版本可以在许多平台上编译,因为它避免了任何平台特定的依赖。GEOS 流行的一个因素是,存在大量基础设施来创建到各种脚本语言的自动化或半自动化绑定,包括 Python。另一个因素是,大多数地理空间分析软件是用 C 或 C++ 编写的。GEOS 最常见的用途是通过包含 GEOS 作为库的其他 API。
GEOS 提供以下功能:
-
OGC 简单特征
-
地理空间谓词函数
-
相交
-
相触
-
不相交
-
相交
-
在内
-
包含
-
拥有
-
等于
-
覆盖
-
地理空间操作
-
并集
-
距离
-
交集
-
对称差
-
凸包
-
边界
-
缓冲区
-
简化
-
多边形组装
-
多边形验证
-
面积
-
长度
-
空间索引
-
OGC 已知文本(WKT)和已知二进制(WKB)输入/输出
-
C 和 C++ API
-
线程安全
GEOS 可以与 GDAL 一起编译以使用其所有功能。
GEOS 可以在 trac.osgeo.org/geos 上找到。
PostGIS
在开源地理空间数据库中,PostGIS 是最常用的空间数据库。PostGIS 实际上是在知名的 PostgreSQL 关系数据库之上的一个模块。PostGIS 的许多功能都来自于之前提到的 GEOS 库。像 JTS 一样,它也实现了 OGC 简单特征规范。这种在地理空间环境中的计算几何能力使 PostGIS 处于一个独特的类别。
PostGIS 允许你对数据集执行属性和空间查询。回想一下第二章中的内容,“学习地理空间数据”,一个典型的空间数据集由多种数据类型组成,包括几何、属性(一行中的一列或多列数据)以及在大多数情况下,索引数据。在 PostGIS 中,你可以像查询任何数据库表一样使用 SQL 查询属性数据。
这种能力并不令人惊讶,因为属性数据存储在传统的数据库结构中。然而,你也可以使用 SQL 语法查询几何数据。空间操作通过 SQL 函数提供,这些函数是你查询的一部分。以下示例 PostGIS SQL 语句在佛罗里达州周围创建了一个 14.5 公里的缓冲区:
SELECT ST_Buffer(the_geom, 14500)
FROM usa_states
WHERE state = 'Florida'
FROM子句指定usa_states图层为查询的位置。我们在WHERE子句中隔离Florida,以过滤该图层。Florida是usa_states图层state列中的一个值。SELECT子句通过使用 PostGIS 的ST_Buffer()函数对Florida的几何形状进行实际的空间选择,该几何形状通常包含在the_geom列中。the_geom列是本例中 PostGIS 图层的几何列。函数名称中的ST缩写代表空间类型。ST_Buffer()函数接受包含空间几何形状的列和底层图层的地图单位距离。
usa_states图层中的地图单位以米为单位,因此在先前的例子中,14.5 公里将是 14,500 米。回想一下第一章 1,“使用 Python 学习地理空间分析”,其中像这样的缓冲区查询用于邻近度分析。碰巧的是,佛罗里达州的水域边界从州的西部和西北海岸线向外扩展了 9 海里,或大约 14.5 公里进入墨西哥湾。
下图显示了佛罗里达州官方水域边界作为虚线,并在地图上标注:

应用 9 海里缓冲区后,您可以在下图中看到,橙色突出显示的结果与官方法律边界非常接近,该边界基于详细的地面调查:

目前,PostGIS 维护以下功能集:
-
地理空间几何类型,包括点、线字符串、多边形、多点、多线字符串、多边形集合和几何集合,可以存储不同类型的几何形状,包括用于测试几何关系的其他空间函数集合(例如,点在多边形内或并集)
-
用于派生新几何形状的空间函数(例如,缓冲区和交集)
-
包括周长、长度和面积在内的空间测量
-
使用 R 树算法的空间索引
-
基本地理空间栅格数据类型
-
拓扑数据类型
-
基于TIGER(即拓扑集成地理编码和参照)人口普查数据的美国地理编码器(TIGER是Topologically Integrated Geographic Encoding and Referencing的缩写)
-
一种新的 JSONB 数据类型,允许对 JSON 和 GeoJSON 进行索引和查询
PostGIS 功能集在所有地理数据库中都具有竞争力,在开源或免费地理数据库中是最广泛的。PostGIS 开发社区的活跃动力也是这个系统成为最佳选择的原因之一。PostGIS 维护在postgis.net。
其他支持空间功能的数据库
PostGIS 是免费和开源地理空间数据库中的黄金标准。然而,作为地理空间分析师,您应该了解几个其他系统。此列表包括商业和开源系统,每个系统都有不同程度的地理空间支持。
地理数据库与地理空间软件、标准和互联网的发展是平行的。互联网推动了需要大型、多用户地理空间数据库服务器,这些服务器能够服务大量数据。以下由 www.OSGeo.org 提供的图表显示了地理空间架构是如何演变的,其中这一演变的大部分发生在数据库层面:

Oracle Spatial and Graph
Oracle 关系数据库是一个广泛使用的数据库系统,通常由大型组织使用,因为其成本和大规模可扩展性。它也非常稳定和快速。它运行世界上一些最大和最复杂的数据库,通常在医院、银行和政府机构中找到,这些机构管理着数百万条关键记录。
地理空间数据功能首次出现在 Oracle 第 4 版中,这是加拿大水文服务(CHS)的一项修改。CHS 还实现了 Oracle 的第一个空间索引,形式为一种不寻常但高效的立体螺旋。Oracle 随后采用了这项修改,并在主数据库的第 7 版中发布了 Oracle Spatial Database Option(SDO)。SDO 系统在 Oracle 第 8 版中成为 Oracle Spatial。Oracle Spatial 的数据库模式在部分列和表名上仍然保留了 SDO 前缀,类似于 PostGIS 使用 OGC 规范,ST,在模式级别将空间信息与传统的关系数据库表和函数分开。
截至 2012 年,Oracle 开始将此软件包称为 Oracle Spatial and Graph,以强调网络数据模块。此模块用于分析网络化数据集,例如交通或公用事业。然而,该模块也可以用于抽象网络,例如社交网络。社交网络数据分析是大数据分析的一个常见目标,现在正成为一种增长趋势。大数据社交网络分析可能是 Oracle 更改产品名称的原因。
Oracle Spatial 具有以下功能:
-
一个地理空间数据模式
-
一个基于 R-tree 索引的空间索引系统
-
用于执行几何操作的 SQL API
-
一个用于优化特定数据集的空间数据调优 API
-
一个拓扑数据模型
-
一个网络数据模型
-
一个用于存储、索引、查询和检索栅格数据的 GeoRaster 数据类型
-
三维数据类型,包括不规则三角网络(TINs)和激光雷达(LIDAR,即光探测与测距)点云
-
一个用于搜索位置名称并返回坐标的地理编码器
-
一个用于驾驶方向查询的路由引擎
-
OGC 兼容性
Oracle Spatial 和 PostGIS 可以合理地比较,并且两者都普遍使用。您在执行地理空间分析时迟早会看到这两个系统作为数据源。
Oracle Spatial and Graph 是与 Oracle 本身分开销售的。一个鲜为人知的事实是,SDO 数据类型是 Oracle 主数据库的本地数据类型。如果您有一个简单的应用程序,只需输入点并检索它们,您可以使用主 Oracle API 来添加、更新和检索 SDO,而无需 Oracle Spatial and Graph。
美国能源、管理和监管局(BOEMRE)使用 Oracle 来管理数十亿美元的石油、天然气和矿产资源的环境、商业和地理空间数据,这是世界上最大的地理空间系统之一。以下地图由 BOEMRE 提供:

Oracle Spatial and Graph 可以在以下网址找到:www.oracle.com/us/products/database/options/spatial/overview.
ArcSDE
ArcSDE 是 Esri 的 空间数据引擎(SDE)。在作为独立产品超过十年后,它现在已被整合到 Esri 的 ArcGIS Server 产品中。ArcSDE 的有趣之处在于,该引擎主要是数据库无关的,支持多个数据库后端。ArcSDE 支持 IBM DB2、Informix、Microsoft SQL Server、Oracle 和 PostgreSQL 作为数据存储系统。虽然 ArcSDE 有能力在 Microsoft SQL Server 和 Oracle 等系统上从头创建和管理空间模式,但如果可用,它将使用本机空间引擎。这种情况适用于 IBM DB2、Oracle 和 PostgreSQL。对于 Oracle,ArcSDE 管理表结构,但可以依赖 Oracle SDO 数据类型进行要素存储。
与之前提到的地理数据库一样,ArcSDE 也拥有丰富的空间选择 API,并且可以处理栅格数据。然而,ArcSDE 的 SQL 空间 API 没有 Oracle 和 PostGIS 那么丰富。Esri 技术上支持与 ArcSDE 相关的基本 SQL 功能,但它鼓励用户和开发人员使用 Esri 软件或编程 API 来操作通过 ArcSDE 存储的数据,因为它是为 Esri 软件作为数据源而设计的。
Esri 提供了软件库,允许开发者在使用 ArcSDE 或 Esri 的基于文件的地理数据库(称为个人地理数据库)的情况下,构建 Esri 软件之外的应用程序。但是,这些库是黑盒,ArcSDE 使用的通信协议从未被逆向工程。通常,ArcSDE 和第三方应用程序在 Web 服务级别通过 ArcGIS Server API(在一定程度上支持 OGC 服务)以及一个相当直接的 REST API 服务进行交互,该服务返回 GeoJSON。
以下截图来自美国联邦网站,catalog.data.gov,这是一个基于 ArcSDE 的非常大的地理空间数据目录,它反过来又连接了美国联邦数据,包括来自其他联邦机构的其他 ArcSDE 安装。
ArcSDE 集成到 ArcGIS Server 中;然而,有关它的信息可以在 www.esri.com/software/arcgis/arcsde 找到。
Microsoft SQL Server
微软在 Microsoft SQL Server 2008 中为其旗舰数据库产品添加了空间数据支持。自那个版本以来,它已经逐渐改进,但仍然远远不如 Oracle Spatial 或 PostGIS 复杂。微软支持与 PostGIS 相同的数据类型,但使用略有不同的命名约定,除了栅格数据类型,它不支持直接支持。它还支持输出到 WKT 和 WKB 格式。
它提供了一些非常基本的地理空间选择支持,但显然目前对微软来说并不是优先事项。这种有限的支持可能是因为它仅适用于微软软件地图组件,并且有几个第三方引擎可以在 SQL Server 上提供空间支持。
微软对 SQL Server 中空间数据的支持在以下链接中有记录:msdn.microsoft.com/en-us/library/bb933790.aspx.
MySQL
MySQL 是另一个非常流行的免费数据库,它提供的支持几乎与 Microsoft SQL Server 相同。OGC 几何类型由基本空间关系函数支持。通过一系列的收购,MySQL 已经成为 Oracle 的财产。
虽然 Oracle 目前仍然致力于将 MySQL 作为开源数据库,但这次收购引发了世界上最受欢迎的开源数据库最终未来的疑问。然而,就地理空间分析而言,MySQL 几乎没有竞争力,不太可能成为任何项目的首选。
有关 MySQL 空间支持的更多信息,请访问以下链接:dev.mysql.com/doc/refman/8.0/en/spatial-types.html
SpatiaLite
SpatiaLite 是开源 SQLite 数据库引擎的扩展。SQLite 使用文件数据库,旨在集成到应用程序中,而不是集成到大多数关系型数据库服务器使用的典型客户端-服务器模型中。SQLite 已经具有空间数据类型和空间索引,但 SpatiaLite 添加了对 OGC 简单特征规范的支持,以及地图投影。
应该注意的是,极其流行的 SQLite 与 Oracle、PostgreSQL 或 MySQL 不在同一类别中,因为它是一个基于文件的数据库,旨在为单用户应用程序设计。
SpatiaLite 可以在 www.gaia-gis.it/gaia-sins/ 找到。
GeoPackage
GeoPackage 是一种基于文件的地理数据库格式。官方 GeoPackage 网站 geopackage.org 将其描述为:
“一种开放、基于标准的、平台无关的、便携的、自我描述的、紧凑的格式,用于传输地理空间信息。”
它也是对 Esri 文件地理数据库格式的直接回应,也是对开放地理空间社区指定的形状文件杀手的回应,以取代过时的、部分封闭的形状文件格式。这两种格式实际上都是文件规范,依赖于其他软件来读取和写入数据。
GeoPackage 是一个 OGC 规范,这意味着它作为行业数据格式的未来是安全的。它也是一个通用的格式,可以处理矢量数据、栅格数据、属性信息以及用于满足新要求的扩展。而且,像任何好的数据库一样,它可以处理多个图层。您可以将整个 GIS 项目存储在一个单独的包中,因此使数据管理变得更加简单。
您可以在此处了解更多关于 Esri 文件地理数据库格式的信息:desktop.arcgis.com/en/arcmap/10.3/manage-data/administer-file-gdbs/file-geodatabases.htm。
路由
路由是计算几何的一个非常专业化的领域。它也是一个非常丰富的研究领域,远远超出了熟悉的驾驶方向用例。路由算法的要求仅仅是网络数据集和影响该网络旅行速度的阻抗值。通常,数据集是矢量基础的,但栅格数据也可以用于某些应用。
该领域的两大主要竞争者是 Esri 的网络分析师和针对 PostGIS 的开源 pgRouting 引擎。最常见的路由问题是访问多个点位置的最有效方式。这个问题被称为旅行商问题(TSP)。TSP 是计算几何中最受深入研究的问题之一。它通常被认为是任何路由算法的基准。
关于 TSP 的更多信息可以在 en.wikipedia.org/wiki/Travelling_salesman_problem 找到。
Esri 网络分析师和空间分析师
Esri 进入路由领域的举措,网络分析师,是一个真正通用的路由引擎,可以处理大多数路由应用,无论其上下文如何。空间分析师是 Esri 的另一个扩展,它专注于栅格数据,并且可以在栅格地形数据上执行最低成本路径分析。
ArcGIS 网络分析师产品页面位于 Esri 网站上:www.esri.com/software/arcgis/extensions/networkanalyst。
pgRouting
PostGIS 的 pgRouting 扩展为地理数据库添加了路由功能。它面向道路网络,但可以适应与其他类型的网络数据一起工作。
下图显示了 pgRouting 计算出的驾驶距离半径输出,该输出在 QGIS 中显示。点根据其与起始位置的距离从绿色到红色进行着色。如下所示,这些点是网络数据集中的节点,由 QGIS.org([qgis.org/en/site/](https://qgis.org/en/site/))提供,在这种情况下是道路:

pgRouting PostGIS 扩展由pgrouting.org/维护。
接下来,我们将探讨可视化像之前那样的图表所需的工具。
理解桌面工具(包括可视化)
地理空间分析需要能够可视化输出的能力。这一事实使得能够可视化数据的工具对该领域至关重要。地理空间可视化工具分为两大类。
第一类是地理空间查看器,第二类是地理空间分析软件。第一类——地理空间查看器——允许你访问、查询和可视化数据,但不能以任何方式编辑它。第二类允许你执行这些任务,并编辑数据。查看器的主要优势是它们通常是轻量级的软件,启动和加载数据快速。
地理空间分析软件需要更多的资源来编辑复杂的地理空间数据,因此它加载得较慢,并且通常渲染数据也较慢,以便提供动态编辑功能。
Quantum GIS
量子地理信息系统,更常被称为QGIS,是一个完整的开源地理信息系统。QGIS 在可视化软件的两个类别中的地理空间分析类别中表现良好。该系统的开发始于 2002 年,并于 2009 年发布了 1.0 版本。
这是本章之前提到的大多数库的最佳展示。QGIS 是用 C++编写的,使用 Qt 库进行 GUI 开发。GUI 设计良好,易于使用。事实上,在专有软件包(如 Esri 的 ArcGIS 或 Manifold 系统)上受过培训的地理空间分析师在使用 QGIS 时会感到非常自在。工具和菜单系统逻辑性强,符合 GIS 系统的典型特征。QGIS 的整体速度与任何其他可用的系统相当,甚至更好。
QGIS 的一个优点是,其底层库和实用程序隐藏在表面之下。模块可以由任何第三方使用 Python 编写并添加到系统中。QGIS 还拥有一个强大的在线包管理系统,用于搜索、安装和更新这些扩展。Python 集成包括一个控制台,允许你在控制台中发出命令并看到 GUI 中的结果。提供这种功能的软件不止 QGIS 一种。
与大多数地理空间软件包一样,如果你使用自动安装程序,它会安装 Python 的完整版本。如果你已经安装了 Python,无需担心。在单台机器上拥有多个 Python 版本相当普遍且得到良好支持。许多人出于测试软件或因为它是许多不同软件包的常见脚本环境的目的,在他们的计算机上安装多个 Python 版本。
当 Python 控制台在 QGIS 中运行时,整个程序 API 通过一个自动加载的对象qgis.utils.iface可用。以下截图显示了运行 Python 控制台的 QGIS:

由于 QGIS 基于 GDAL/OGR 和 GEOS,并且可以使用 PostGIS,因此它支持那些包提供的所有数据源。它还具有很好的栅格处理功能。QGIS 非常适合使用可用的扩展生成纸质地图或整个地图集。
QGIS 在以下链接的 QGIS 网站上得到了很好的文档说明:www.qgis.org/en/documentation.html。你也可以通过搜索 QGIS 或特定操作找到许多在线和视频教程。
OpenEV
OpenEV 是一个开源的地理空间查看器,最初由 Atlantis Scientific 于 2002 年左右开发,后来成为 Vexcel,在微软收购之前。Vexcel 将 OpenEV 开发为一个免费下载的卫星图像查看器,用于加拿大地理空间数据基础设施(CGDI)。它是使用 GDAL 和 Python 构建的,并由 GDAL 的创建者 Frank Warmerdam 部分维护。
OpenEV 是可用的最快的栅格查看器之一。尽管最初设计为查看器,但 OpenEV 提供了 GDAL 和 PROJ 的所有实用功能。虽然它被创建为一个栅格工具,但它可以叠加矢量数据,如 shapefiles,甚至支持基本的编辑。使用内置的栅格计算器也可以修改栅格图像,并且可以转换、重新投影和裁剪数据格式。
以下截图显示了 OpenEV 查看器窗口中的一个 25MB、16 位整数的 GeoTIFF 高程文件:

OpenEV 主要使用 Python 构建,并提供了一个 Python 控制台,可以访问程序的全部功能。OpenEV 的 GUI 不如其他工具,如 QGIS 复杂。例如,你不能像在 QGIS 中那样将地理空间数据集拖放到查看器中。但是,OpenEV 的原始速度使其对于简单的栅格查看或基本处理和数据转换非常吸引人。
OpenEV 的主页可在以下链接找到:openev.sourceforge.net。
GRASS GIS
GRASS 是现存的最古老的持续开发地理空间系统之一。美国陆军工程兵团于 1982 年开始开发 GRASS。它最初是为 Unix 系统设计的。1995 年,军队发布了最后一个补丁,软件被转移到社区开发,并一直如此。
尽管用户界面经过了重新设计,GRASS 对于现代 GIS 用户来说仍然有些神秘。然而,由于其数十年的传统和不存在价格标签,多年来许多地理空间工作流程和高度专业化的模块已在 GRASS 中实现,使其对许多组织和个人,尤其是在研究社区中,高度相关。出于这些原因,GRASS 仍在积极开发。
GRASS 也已与 QGIS 集成,因此可以使用更现代且熟悉的 QGIS GUI 来运行 GRASS 功能。GRASS 也与 Python 深度集成,可以作为库或命令行工具使用。以下截图显示了在原生 GRASS GUI 中进行的某些地形分析,该 GUI 是使用wxPython库构建的:

GRASS 可在grass.osgeo.org/在线找到[.]。
gvSIG
另一个基于 Java 的桌面 GIS 是gvSIG。gvSIG 项目始于 2004 年,作为将西班牙瓦伦西亚地区基础设施和交通部 IT 系统迁移到开源软件的更大项目的一部分。结果是产生了 gvSIG,它一直在不断成熟。其功能集主要与 QGIS 相当,也有一些独特的能力。
官方的 gvSIG 项目有一个非常活跃的分支,称为gvSIG 社区版(gvSIG CE)。还有一个名为 gvSIG mobile 的移动版本。gvSIG 代码库是开源的。
gvSIG 的官方主页可在www.gvsig.org/web/找到。
OpenJUMP
OpenJUMP 是另一个开源的基于 Java 的桌面 GIS。JUMP代表Java Unified Mapping Platform,最初由 Vivid Solutions 为不列颠哥伦比亚省政府创建。在 Vivid Solutions 交付 JUMP 后,开发停止。Vivid Solutions 最终将 JUMP 发布到开源社区,在那里它被重命名为 OpenJUMP。
OpenJUMP 能够读取和写入 shapefiles 和 OGC GML(地理标记语言的简称),并支持 PostGIS 数据库。它还可以显示某些图像格式和来自 OGC WMS(Web 地图服务器的简称)和WFS(Web 特征服务的简称)服务的数据。它具有插件架构,也可以作为定制应用程序的开发平台。
你可以在官方网页www.openjump.org/上了解更多关于 OpenJUMP 的信息。
Google Earth
Google Earth 如此普遍,几乎不值得一提。2001 年 EarthViewer 3D 的第一个版本(由一家名为 Keyhole Inc.的公司创建)和 EarthViewer 3D 项目由非营利风险投资公司 In-Q-Tel 资助,而 In-Q-Tel 反过来又由美国中央情报局资助。这个间谍机构的血统以及 Google 收购 Keyhole 以创建和分发 Google Earth,使全球关注地理空间分析领域。
自从 2005 年首次以 Google Earth 软件形式发布以来,Google 一直在不断改进它。其中一些引人注目的新增功能包括创建 Google 月球、Google 火星、Google 天空和 Google 海洋。这些是虚拟地球应用,它们具有月球和火星的数据,除了 Google 海洋,它还为 Google Earth 添加了海底地形图,称为测深。
Google Earth 引入了旋转虚拟地球概念,用于地理数据的探索。在几个世纪以来查看二维地图或低分辨率的物理地球仪之后,虚拟地飞越地球并在世界任何角落的街角降落都是令人震惊的——尤其是对于地理空间分析师和其他地理爱好者来说,如下面的 Google Earth 截图所示,俯瞰路易斯安那州新奥尔良的商业区:

正如 Google 通过基于瓦片的滑动映射方法革命性地改变了网络地图一样,虚拟地球概念极大地促进了地理空间可视化。
在最初的兴奋过后,许多地理空间分析师意识到 Google Earth 是一个非常生动有趣的地理探索工具,但它对于任何类型的有意义地理空间分析的实际用途非常有限。Google Earth 完全属于地理空间查看软件领域。
它仅消耗其原生钥匙孔标记语言(KML),这是一个集数据和管理风格于一体的格式,在第二章“学习地理空间数据”中有详细讨论。由于该格式现在是 OGC 标准,仅消耗一种数据格式立即限制了任何工具的实用性。任何涉及 Google Earth 的项目都必须首先开始于在 KML 中进行完整的数据转换和样式设计,这让人联想到大约 10-20 年前的地理空间分析。支持 KML 的工具,包括 Google Maps,只支持 KML 的有限子集。
谷歌地球的本地数据集具有全球覆盖范围,但它是由跨越几年和来源的数据集混合而成的。谷歌大大改进了工具中的内联元数据,这些元数据确定了当前视图的来源和近似日期。但是,这种方法在普通人中造成了混淆。许多人认为谷歌地球中的数据更新频率远高于实际情况。谷歌街景系统,展示了世界许多地区的街景,360 度全景视图,在一定程度上有助于纠正这种误解。
人们能够轻易地识别出熟悉地点的图片是几年前拍摄的。谷歌地球创造的另一种常见误解是,整个世界已经被详细地绘制在地图上,因此为地理空间分析创建基础地图应该是微不足道的。正如第二章中讨论的,学习地理空间数据,使用现代数据和软件绘制感兴趣的区域比几年前要容易得多,但这仍然是一项复杂且劳动密集型的任务。这种误解是地理空间分析师在开始项目时必须管理的第一个客户期望之一。
尽管存在这些误解,谷歌对地理空间分析产生的影响几乎完全是积极的。几十年来,地理空间行业增长的一个最困难的挑战是说服潜在的股东,在关于人、资源和环境的决策中,地理空间分析几乎总是最佳方法。这个障碍与汽车经销商形成鲜明对比。当潜在客户来到汽车销售场地时,销售员不需要说服买家他们需要一辆车,只需要说服他们需要什么类型的汽车。
地理空间分析师首先必须教育项目赞助商了解这项技术,然后说服他们地理空间方法是解决挑战的最佳方式。谷歌在很大程度上消除了分析师需要采取的这些步骤。
谷歌地球可以在网上找到,点击这里访问。
NASA WorldWind
NASA WorldWind 是一个开源的虚拟地球和地理空间查看器,最初由美国国家航空航天局(NASA)于 2004 年发布。它最初基于微软的.NET 框架,使其成为一个以 Windows 为中心的应用程序。
以下是美国国家航空航天局(NASA)世界风(WorldWind)的截图看起来与谷歌地球相似:

2007 年,发布了一个基于 Java 的软件开发工具包(SDK),称为WorldWind Java,这使得 WorldWind 更加跨平台。转向 Java 还导致了为 WorldWind 创建浏览器插件。
WorldWind Java SDK 被视为一个 SDK,而不是像.NET 版本那样的桌面应用程序。然而,SDK 中包含的示例提供了一个无需额外开发的查看器。虽然 NASA WorldWind 最初是受 Google Earth 的启发,但其作为开源项目的地位使其走向了完全不同的方向。
Google Earth 是一个通用工具,受 KML 规范的限制。NASA WorldWind 现在是一个任何人都可以无限制开发的平台。随着新类型的数据变得可用,计算资源增长,虚拟地球范式在地理空间可视化方面的潜力显然还有更多未被探索的。
NASA WorldWind 可在worldwind.arc.nasa.gov/java/在线获取。
ArcGIS
Esri 在成为理解我们世界的地理空间分析方法的最大推广者之一的同时,也是一个私有控股、盈利性企业,它必须在一定程度上关注自己的利益。ArcGIS 软件套件代表了已知的所有类型的地理空间可视化,包括矢量、栅格、地球仪和 3D。它也是许多国家的市场领导者。正如本章前面所示的地理空间软件地图所示,Esri 越来越多地将开源软件纳入其工具套件中,包括 GDAL 用于栅格显示,以及 Python 作为 ArcGIS 的脚本语言。
以下截图显示了核心 ArcGIS 应用程序 ArcMap,以及海洋跟踪密度数据分析。界面与 QGIS 有很多共同之处,如本截图所示,由marinecadastre.gov/提供:

ArcGIS 产品页面可在www.esri.com/software/arcgis在线获取。
现在我们已经了解了可视化和分析数据的工具,让我们来看看如何管理数据。
理解元数据管理
互联网上数据的分发增加了元数据的重要性。数据管理员能够将数据集发布给全世界下载,而无需任何个人互动。地理空间数据集的元数据记录可以遵循这一原则,以确保数据的完整性和问责制得到维护。
正确格式的元数据还允许自动编目、搜索索引和数据集的集成。元数据变得如此重要,以至于地理空间社区中有一个常见的格言是没有元数据的数据不是数据,这意味着没有元数据,地理空间数据集就无法被充分利用和理解。
以下部分列出了一些可用的常见元数据工具。OGC 的元数据管理标准是网络目录服务(CSW),它创建了一个基于元数据的目录系统和用于分发和发现数据集的 API。
Python 的 pycsw 库
pycsw 是一个符合 OGC 标准的 CSW,用于地理空间元数据的发布和发现。它支持多个 API,包括 CSW 2/CSW 3、OpenSearch、OAI-PMH 和 SRU。它非常轻量级,且完全由 Python 实现。以下是一个使用 pycsw 构建的 CSW 和客户端的优秀示例,即太平洋岛屿海洋观测系统(PacIOOS)目录,可在以下链接找到:pacioos.org/search/。pycsw 库也被用于一个名为 GeoNode 的更大软件包中。
GeoNode
GeoNode 是一个基于 Python 的地理空间内容管理系统。它将地理空间数据创建、元数据和可视化集成在一个服务器软件包中。它还包括社交功能,如评论和评分系统。它是开源的,可在 geonode.org/ 找到。以下截图来自 GeoNode 在线演示:

GeoNode 和 pycsw 是 Python 的两个主要元数据工具。接下来,我们将探讨一些用其他语言编写的工具。
GeoNetwork
GeoNetwork 是一个开源的、基于 Java 的目录服务器,用于管理地理空间数据。它包括元数据编辑器和搜索引擎,以及一个交互式网络地图查看器。该系统旨在全球范围内连接空间数据基础设施。它可以通过元数据编辑工具通过网页发布元数据。它还可以通过内置的 GeoServer 地图服务器发布空间数据。它具有用户和组安全权限,以及网页和桌面配置工具。
GeoNetwork 还可以配置为定期从其他目录中收集元数据。以下截图展示了联合国粮食及农业组织的 GeoNetwork 实现:

你可以在 geonetwork-opensource.org/ 了解更多关于 GeoNetwork 的信息[.]。
摘要
在本章中,你了解了地理空间分析软件的层次结构。你还学习了一个框架,通过将现有的数百个地理空间软件包和库分类到一到多个主要功能中,包括数据访问、计算几何、栅格处理、可视化和元数据管理,来接近这些软件包和库。
我们还检查了常用的基础库,包括 GDAL、OGR、PROJ 和 GEOS,这些库在地理空间软件中反复出现。你可以通过追踪任何新的地理空间软件,回溯到这些核心库,然后问自己,“这些开发者为什么要逆流而上?”以更好地理解这个软件包。如果软件没有使用这些库之一,你需要问,“为什么这些开发者要逆流而上?”以了解该系统带来了什么。
在本章中,Python 只被提及了几次,目的是为了避免在理解地理空间软件景观时产生任何干扰。但是,正如我们将看到的,Python 与本章中的每一款软件都紧密相连,并且它本身就是一个完全有能力的地理空间工具。Python 是 ArcGIS、QGIS、GRASS 以及许多其他软件包的官方脚本语言,这并非巧合。同样,GDAL、OGR、PROJ、CGAL、JTS、GEOS 和 PostGIS 都有 Python 绑定,这也不是偶然的。
至于这里没有提到的软件包,它们都可以通过 Jython Java 发行版、IronPython .NET 发行版、Python 的各种数据库和 Web API 以及内置的 ctypes 模块在 Python 的掌握之中。作为一名地理空间分析师,如果您有一种技术不能放弃,那就是 Python。
在下一章中,我们将看到 Python 如何在地理空间行业中发挥作用。我们还将了解 GIS 脚本语言、混合粘合语言以及完整的编程语言。
进一步阅读
这里有一份您可以参考的网页列表:
第二部分:地理空间分析概念
本节展示了本书的主要构建模块,在这里,你将通过不同的代码示例和数据编辑概念了解 Python 在行业中的作用。你将学习地理空间产品及其如何应用于解决问题。继续前进,你将了解如何使用 Python 实际处理遥感数据。在本节的最后,你将学习如何使用任何地理空间格式分析高程数据。
本节包括以下章节:
-
第四章,地理空间 Python 工具箱
-
第五章,Python 与地理信息系统
-
第六章,Python 与遥感
-
第七章,Python 与高程数据
第四章:地理空间 Python 工具箱
本书的前三章涵盖了地理空间分析的历史、分析师使用的地理空间数据类型以及地理空间行业中的主要软件和库。我们在某些地方使用了一些简单的 Python 示例来说明某些观点,但我们主要关注地理空间分析领域,而不考虑任何特定技术。从现在开始,我们将使用 Python 来征服地理空间分析,并将继续使用这种方法完成本书的其余部分。本章解释了您工具箱中所需的软件,以便在地理空间领域做几乎所有您想做的事情。
我们将发现用于访问第二章“学习地理空间数据”中发现的矢量数据和栅格数据不同类型的 Python 库。其中一些库是纯 Python,还有一些是我们第三章“地理空间技术景观”中查看的不同软件包的绑定。
在本章中,我们将涵盖以下主题:
-
安装第三方 Python 模块
-
Python 虚拟环境
-
Conda
-
Docker
-
用于获取数据的 Python 网络库
-
Python 基于标签的解析器
-
Python JSON 库
-
OGR
-
PyShp
-
DBFPY
-
Shapely
-
GDAL
-
Fiona
-
NumPy
-
GeoPandas
-
Python 图像库(PIL)
-
PNGCanvas
-
ReportLab
-
GeoPDF
-
Python NetCDF 库
-
Python HDF 库
-
OSMnx
-
空间索引库
-
Jupyter
-
Conda
在可能的情况下,我们将检查纯 Python 解决方案。Python 是一种非常强大的编程语言,但某些操作,尤其是在遥感领域,计算量过于庞大,因此在使用纯 Python 或其他解释型语言时不太实用。幸运的是,可以通过 Python 以某种方式解决地理空间分析的各个方面,即使它绑定到高度高效的 C/C++/其他编译语言库。
我们将避免使用覆盖地理空间分析以外的其他领域的广泛科学库,以使解决方案尽可能简单。使用 Python 进行地理空间分析有许多原因,但其中最强有力的论据之一是其可移植性。
此外,Python 已被移植到 Java 作为 Jython 发行版,以及到.NET 公共语言运行时(CLR)作为 IronPython。Python 还有如 Stackless Python 这样的版本,适用于大量并发程序。还有专为在集群计算机上运行分布式处理而设计的 Python 版本。Python 还可在许多托管应用程序服务器上使用,这些服务器不允许您安装自定义可执行文件,例如具有 Python API 的 Google App Engine 平台。
技术要求
-
Python 3.6 或更高版本
-
RAM:最小 6 GB(Windows),推荐 8 GB(macOS),建议 8 GB
-
存储:最小 7200 RPM SATA,可用空间 20 GB;推荐 SSD,可用空间 40 GB
-
处理器:最小 Intel Core i3 2.5 GHz;推荐 Intel Core i5
安装第三方 Python 模块
使用纯 Python(使用标准库)编写的模块将在 Python 网站提到的 20 个平台中的任何一个上运行。每次你添加一个依赖于绑定到其他语言外部库的第三方模块时,你都会降低 Python 的固有可移植性。你还在代码中添加了另一层复杂性,通过添加另一种语言来彻底改变代码。纯 Python 保持简单。此外,Python 对外部库的绑定通常是由自动或半自动生成的。
这些自动生成的绑定非常通用且晦涩,它们只是通过使用该 API 的方法名将 Python 连接到 C/C++ API,而不是遵循 Python 的最佳实践。当然,也有一些值得注意的例外,这些例外是由项目需求驱动的,可能包括速度、独特的库功能或经常更新的库,在这些库中,自动生成的接口更可取。
我们将在 Python 的标准库中包含的模块和必须安装的模块之间做出区分。在 Python 中,words 模块和库是通用的。要安装库,你可以从 Python 包索引(PyPI) 获取,或者在许多地理空间模块的情况下,下载一个专门的安装程序。
PyPI 作为官方的软件仓库,提供了一些易于使用的设置程序,简化了包的安装。你可以使用 easy_install 程序,它在 Windows 上特别有用,或者使用在 Linux 和 Unix 系统上更常见的 pip 程序。一旦安装,你就可以通过运行以下代码来安装第三方包:
easy_install <package name>
要安装 pip,请运行以下代码:
pip install <package name>
本书将提供不在 PyPI 上可用的开源软件包的链接和安装说明。你可以通过下载 Python 源代码并将其放入当前工作目录,或者将其放入 Python 的 site-packages 目录中来手动安装第三方 Python 模块。这两个目录在尝试导入模块时都可用在 Python 的搜索路径中。如果你将模块放入当前工作目录,它只会在你从该目录启动 Python 时可用。
如果您将其放在site-packages目录中,每次启动 Python 时它都将可用。site-packages目录专门用于第三方模块。为了定位您安装的site-packages目录,您需要询问 Python 的sys模块。sys模块有一个path属性,其中包含 Python 搜索路径中的所有目录。site-packages目录应该是最后一个。您可以通过指定索引-1来定位它,如下面的代码所示:
>>> import sys
>>> sys.path[-1]
'C:\\Python34\\lib\\site-packages'
如果该调用没有返回site-packages路径,只需查看整个列表以定位它,如下面的代码所示:
>> sys.path
['', 'C:\\WINDOWS\\system32\\python34.zip', 'C:\\Python34\\DLLs',
'C:\\Python34\\lib', 'C:\\Python34\\lib\\plat-win
', 'C:\\Python34\\lib\\lib-tk', 'C:\\Python34',
'C:\\Python34\\lib\\site-packages']
这些安装方法将在本书的其余部分中使用。您可以在python.org/download/找到最新的 Python 版本、您平台安装的源代码以及编译说明。
Python 的virtualenv模块允许您轻松地为特定项目创建一个隔离的 Python 副本,而不会影响您的主 Python 安装或其他项目。使用此模块,您可以拥有具有相同库的不同版本的不同项目。一旦您有一个工作代码库,您就可以将其与您使用的模块或甚至 Python 本身的变化隔离开来。virtualenv模块简单易用,可以用于本书中的任何示例;然而,关于其使用的明确说明并未包含。
要开始使用virtualenv,请遵循以下简单指南:docs.python-guide.org/en/latest/dev/virtualenvs/。
Python 虚拟环境
Python 地理空间分析需要我们使用许多具有许多依赖关系的模块。这些模块通常使用特定版本的 C 或 C++库相互构建。当您向系统中添加 Python 模块时,经常会遇到版本冲突。有时,当您升级特定模块时,由于 API 的变化,它可能会破坏您现有的 Python 程序——或者您可能同时运行 Python 2 和 Python 3 以利用为每个版本编写的库。您需要的是一种安全安装新模块的方法,而不会破坏工作系统或代码。解决这个问题的方法是使用virtualenv模块的 Python 虚拟环境。
Python 的virtualenv模块为每个项目创建隔离的、独立的 Python 环境,这样您就可以避免冲突的模块污染您的主 Python 安装。您可以通过激活或停用特定环境来打开或关闭该环境。virtualenv模块在效率上很高,因为它在创建环境时实际上并不复制您整个系统 Python 安装。让我们开始吧:
- 安装
virtualenv就像运行以下代码一样简单:
pip install virtualenv
- 然后,为您的虚拟 Python 环境创建一个目录。命名它 whatever you want:
mkdir geospatial_projects
- 现在,您可以使用以下命令创建您的第一个虚拟环境:
virtualenv geospatial_projects/project1
- 然后,在输入以下命令后,你可以激活该环境:
source geospatial_projects/project1/bin/activate
- 现在,当你在这个目录中运行任何 Python 命令时,它将使用隔离的虚拟环境。当你完成时,你可以使用以下简单的命令来停用该环境:
deactivate
这就是安装、激活以供使用以及停用 virtualenv 模块的方法。然而,你还需要了解另一个环境。我们将在下一节中检查它。
Conda
在这里也值得提一下 Conda,它是一个开源的、跨平台的包管理系统,也可以创建和管理类似于 virtualenv 的环境。Conda 使得安装复杂的包变得容易,包括地理空间包。它还支持 Python 之外的其他语言,包括 R、Node.js 和 Java。
Conda 可在此处找到:docs.conda.io/en/latest/。
现在,让我们来看看如何安装 GDAL,这样我们就可以开始处理地理空间数据了。
安装 GDAL
地理空间数据抽象库(GDAL),包括 OGR,对于本书中的许多示例至关重要,也是更复杂的 Python 设置之一。因此,我们将在这里单独讨论它。最新的 GDAL 绑定可在 PyPI 上找到;然而,由于 GDAL 库需要额外的资源,安装需要额外的步骤。
有三种方法可以安装 GDAL 以用于 Python。你可以使用其中任何一种:
-
从源代码编译它。
-
作为更大软件包的一部分安装它。
-
安装二进制发行版,然后安装 Python 绑定。
如果你也有编译 C 库以及所需编译软件的经验,那么第一个选项会给你最大的控制权。然而,如果你只是想尽快开始,那么这个选项并不推荐,因为即使是经验丰富的软件开发者也会发现编译 GDAL 和相关的 Python 绑定具有挑战性。在主要平台上的 GDAL 编译说明可以在 trac.osgeo.org/gdal/wiki/BuildHints 找到;PyPI GDAL 页面上也有基本的构建说明;请查看 pypi.python.org/pypi/GDAL。
第二个选项无疑是最快和最简单的方法。开源地理空间基金会(OSGeo)分发了一个名为 OSGeo4W 的安装程序,只需点击一下按钮即可在 Windows 上安装所有顶级开源地理空间包。OSGeo4W 可在 trac.osgeo.org/osgeo4w/ 找到。
虽然这些包最容易使用,但它们带有自己的 Python 版本。如果你已经安装了 Python,那么仅为了使用某些库就安装另一个 Python 发行版可能会出现问题。在这种情况下,第三个选项可能适合你。
第三个选项安装了针对您的 Python 版本预编译的二进制文件。这种方法在安装简便性和定制之间提供了最佳折衷。但是,您必须确保二进制发行版和相应的 Python 绑定彼此兼容,与您的 Python 版本兼容,并且在许多情况下与您的操作系统配置兼容。
Windows
每年,Windows 上 Python 的 GDAL 安装都变得越来越容易。要在 Windows 上安装 GDAL,您必须检查您是否正在运行 32 位或 64 位版本的 Python:
- 要这样做,只需在命令提示符中启动 Python 解释器,如下面的代码所示:
Python 3.4.2 (v3.4.2:ab2c023a9432, Oct 6 2014, 22:15:05) [MSC v.1600
32 bit (Intel)] on win32
Type "help", "copyright", "credits" or "license" for more
information.
-
基于此实例,我们可以看到 Python 版本为 3.4.2 的
win32,这意味着它是 32 位版本。一旦您有了这些信息,请访问以下 URL:www.lfd.uci.edu/~gohlke/pythonlibs/#gdal。 -
这个网页包含了适用于几乎所有开源科学库的 Python Windows 二进制文件和绑定。在该网页的 GDAL 部分,找到与您的 Python 版本匹配的版本。版本名称使用 C Python 的缩写
cp,后跟主要的 Python 版本号,以及 32 位 Windows 的win32或 64 位 Windows 的win_amd64。
在前面的例子中,我们会下载名为GDAL-1.11.3-cp34-none-win32.whl的文件。
- 此下载包是较新的 Python
pipwheel 格式。要安装它,只需打开命令提示符并输入以下代码:
pip install GDAL-1.11.3-cp34-none-win32.whl
- 一旦安装了包,打开 Python 解释器并运行以下命令,通过检查其版本来验证 GDAL 是否已安装:
Python 3.4.2 (v3.4.2:ab2c023a9432, Oct 6 2014, 22:15:05) [MSC v.1600 32 bit (Intel)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> from osgeo import gdal
>>> gdal.__version__
1.11.3
现在,GDAL 应该返回版本号1.11.3。
如果您在使用easy_install或pip和 PyPI 安装模块时遇到问题,请尝试从与 GDAL 示例相同的网站下载并安装 wheel 包。
Linux
Linux 上的 GDAL 安装因发行版而异。以下gdal.org二进制网页列出了几个发行版的安装说明:trac.osgeo.org/gdal/wiki/DownloadingGdalBinaries。让我们开始吧:
- 通常,您的包管理器会安装 GDAL 和 Python 绑定。例如,在 Ubuntu 上,要安装 GDAL,您需要运行以下代码:
sudo apt-get install gdal-bin
- 然后,要安装 Python 绑定,您可以运行以下命令:
sudo apt-get install python3-gdal
-
大多数 Linux 发行版已经配置好了编译软件,它们的说明比 Windows 上的简单得多。
-
根据安装情况,您可能需要将
gdal和ogr作为osgeo包的一部分导入,如下面的命令所示:
>>> from osgeo import gdal
>>> from osgeo import ogr
macOS X
要在 macOS X 上安装 GDAL,您还可以使用 Homebrew 包管理系统,该系统可在brew.sh/找到。
或者,您可以使用 MacPorts 软件包管理系统,该系统可在 www.macports.org/ 获取。
这两个系统都有很好的文档记录,并包含适用于 Python 3 的 GDAL 包。你实际上只需要它们用于需要正确编译的二进制文件(用 C 语言编写,具有许多依赖项并包含许多科学和地理空间库)的库。
Python 网络库用于获取数据
大多数地理空间数据共享都是通过互联网完成的,Python 在处理几乎任何协议的网络库方面都准备得很充分。自动数据下载通常是自动化地理空间过程的一个重要步骤。数据通常从网站的 统一资源定位符 (URL) 或 文件传输协议 (FTP) 服务器检索,由于地理空间数据集通常包含多个文件,因此数据通常以 ZIP 文件的形式分发。
Python 的一个优点是其文件类似对象的概念。大多数用于读取和写入数据的 Python 库都使用一组标准方法,允许您从不同类型的资源访问数据,就像您在磁盘上写入一个简单的文件一样。Python 标准库中的网络模块也使用这种约定。这种方法的优点是它允许您将文件类似对象传递给其他库和方法,这些库和方法可以识别该约定,而无需为以不同方式分发的不同类型的数据进行大量设置。
Python 的 urllib 模块
Python 的 urllib 包旨在简单访问任何具有 URL 地址的文件。Python 3 中的 urllib 包由几个模块组成,这些模块处理管理网络请求和响应的不同部分。这些模块实现了 Python 的一些文件类似对象约定,从其 open() 方法开始。当你调用 open() 时,它会准备与资源的连接,但不会访问任何数据。有时,你只想获取一个文件并将其保存到磁盘上,而不是将其加载到内存中。这个功能可以通过 urllib.request.retrieve() 方法获得。
以下示例使用 urllib.request.retrieve() 方法下载名为 hancock.zip 的压缩形状文件,该文件在其他示例中使用。我们定义了 URL 和本地文件名作为变量。URL 作为参数传递,以及我们想要使用的文件名,以将其保存到我们的本地机器上,在这种情况下,只是 hancock.zip:
>>> import urllib.request
>>> import urllib.parse
>>> import urllib.error
>>> url = "https://github.com/GeospatialPython/
Learn/raw/master/hancock.zip"
>>> fileName = "hancock.zip"
>>> urllib.request.urlretrieve(url, fileName)
('hancock.zip', <httplib.HTTPMessage instance at 0x00CAD378>)
来自底层httplib模块的消息确认文件已下载到当前目录。URL 和文件名也可以直接作为字符串传递给retrieve()方法。如果你只指定文件名,下载将保存到当前工作目录。你也可以指定一个完全限定的路径名来将其保存到其他位置。你还可以指定一个回调函数作为第三个参数,该函数将接收文件的下载状态信息,这样你就可以创建一个简单的下载状态指示器或执行其他操作。
urllib.request.urlopen()方法允许你以更高的精度和控制访问在线资源。正如我们之前提到的,它实现了大多数 Python 文件类似对象方法,除了seek()方法,它允许你在文件中的任意位置跳转。你可以逐行读取在线文件,将所有行作为列表读取,读取指定数量的字节,或者遍历文件的每一行。所有这些功能都在内存中执行,因此你不需要将数据存储在磁盘上。这种能力对于访问可能需要处理但不想保存到磁盘上的在线频繁更新的数据非常有用。
在下面的示例中,我们通过访问美国地质调查局(USGS)地震源来演示这个概念,查看在过去一小时内发生的所有地震。这些数据以逗号分隔值(CSV)文件的形式分发,我们可以像文本文件一样逐行读取。CSV 文件类似于电子表格,可以在文本编辑器或电子表格程序中打开:
-
首先,你需要打开 URL 并读取包含文件列名的标题。
-
然后,你需要读取第一行,其中包含最近地震的记录,如下面的代码行所示:
>>> url = "http://earthquake.usgs.gov/earthquakes/feed/v1.0/
summary/all_hour.csv"
>>> earthquakes = urllib.request.urlopen(url)
>>> earthquakes.readline()
'time,latitude,longitude,depth,mag,magType,nst,gap,dmin,rms,net,
id,updated,place
\n'
>>> earthquakes.readline()
'2013-06-14T14:37:57.000Z,64.8405,-147.6478,13.1,0.6,Ml,
6,180,0.09701805,0.2,ak,
ak10739050,2013-06-14T14:39:09.442Z,"3km E of Fairbanks,
Alaska"\n'
-
我们也可以遍历这个文件,这是一种读取大文件的内存高效方式。
-
如果你在这个 Python 解释器中运行这个示例,你需要按下Enter或return键两次来执行循环。这个动作是必要的,因为它向解释器发出信号,表明你已经完成了循环的构建。在下面的示例中,我们简化了输出:
>>> for record in earthquakes: print(record)
2013-06-14T14:30:40.000Z,62.0828,-145.2995,22.5,1.6,
Ml,8,108,0.08174669,0.86,ak,
ak10739046,2013-06-14T14:37:02.318Z,"13km ESE of Glennallen,
Alaska"
...
2013-06-14T13:42:46.300Z,38.8162,-122.8148,3.5,0.6,
Md,,126,0.00898315,0.07,nc,nc
72008115,2013-06-14T13:53:11.592Z,"6km NW of The Geysers,
California"
Python 请求模块
urllib模块已经存在很长时间了。另一个第三方模块已经被开发出来,使得常见的 HTTP 请求更加容易。requests模块具有以下功能:
-
保持连接和连接池
-
国际域名和 URL
-
会话中保持 cookie 持久性
-
浏览器风格的 SSL 验证
-
自动内容解码
-
基本摘要认证
-
精美的键/值 cookie
-
自动解压缩
-
Unicode 响应体
-
HTTP(S)代理支持
-
多部分文件上传
-
流式下载
-
连接超时
-
分块请求
-
.netrc支持
在下面的例子中,我们将下载与使用urllib模块下载的相同 ZIP 文件,但这次我们将使用requests模块。首先,我们需要安装requests模块:
pip install requests
然后,我们可以导入它:
import requests
然后,我们可以设置 URL 和输出文件名的变量:
url = "https://github.com/GeospatialPython/Learning/raw/master/hancock.zip"
fileName = "hancock.zip"
使用requests模块的get()方法检索 ZIP 文件非常简单:
r = requests.get(url)
现在,我们可以从.zip文件中获取内容并将其写入我们的输出文件:
with open(fileName, 'wb') as f:
f.write(r.content)
requests模块还有许多其他高级功能,使用起来与这个例子一样简单。现在我们知道了如何通过 HTTP 协议获取信息,让我们来检查 FTP 协议,它通常用于从在线存档访问地理空间数据。
FTP
FTP 允许你使用 FTP 客户端软件浏览在线目录并下载数据。直到大约 2004 年,当地理空间网络服务变得非常普遍之前,FTP 是分发地理空间数据最常见的方式之一。现在 FTP 不太常见,但在搜索数据时偶尔会遇到它。再次强调,Python 的内置标准库有一个名为ftplib的合理 FTP 模块,其主要类名为FTP()。
在下面的例子中,我们将执行以下操作:
-
我们将访问由美国国家海洋和大气管理局(NOAA)托管的 FTP 服务器,以访问包含全球海啸监测网络深海评估和报告(DART)浮标数据的文本文件。这个特定的浮标位于秘鲁海岸。
-
我们将定义服务器和目录路径,然后我们将访问服务器。所有 FTP 服务器都需要用户名和密码。大多数公共服务器都有一个名为 anonymous 的用户,密码也是 anonymous,就像这个服务器一样。
-
使用 Python 的
ftplib,你可以不带任何参数调用login()方法以默认匿名用户身份登录。否则,你可以添加用户名和密码作为字符串参数。 -
登录后,我们将切换到包含 DART 数据文件的目录。
-
要下载文件,我们将打开一个名为 out 的本地文件,并将它的
write()方法作为回调函数传递给ftplib.ftp.retrbinary()方法,该方法同时下载文件并将其写入我们的本地文件。 -
文件下载完成后,我们可以关闭它以保存它。
-
然后,我们将读取文件并查找包含浮标纬度和经度的行,以确保数据已成功下载,如下面的代码行所示:
import ftplib
server = "ftp.ngdc.noaa.gov"
dir = "hazards/DART/20070815_peru"
fileName = "21415_from_20070727_08_55_15_tides.txt"
ftp = ftplib.FTP(server)
ftp.login()
ftp.cwd(dir)
with open(fileName, "wb") as out:
ftp.retrbinary("RETR " + fileName, out.write)
with open(fileName) as dart:
for line in dart:
if "LAT, " in line:
print(line)
break
输出如下:
LAT, LON 50.1663 171.8360
在这个例子中,我们以二进制写入模式打开了本地文件,并使用了retrbinary()``ftplib方法,而不是使用 ASCII 模式的retrlines()。二进制模式适用于 ASCII 和二进制文件,因此始终是一个更安全的赌注。实际上,在 Python 中,文件的二进制读写模式仅在 Windows 上需要。
如果你只是从 FTP 服务器下载一个简单的文件,许多 FTP 服务器也有一个网络界面。在这种情况下,你可以使用urllib来读取文件。FTP URL 使用以下格式来访问数据:
ftp://username:password@server/directory/file
这种格式对于密码保护的目录来说是不安全的,因为你正在通过互联网传输你的登录信息。但对于匿名 FTP 服务器来说,没有额外的安全风险。为了演示这一点,以下示例通过使用urllib而不是ftplib来访问我们刚刚看到的相同文件:
>>> dart = urllib.request.urlopen("ftp://" + server + "/" + dir +
"/" + fileName)
>>> for line in dart:
... line = str(line, encoding="utf8")
... if "LAT," in line:
... print(line)
... break
...
LAT, LON 50.1663 171.8360
现在我们已经可以下载文件了,让我们学习如何解压缩它们。
ZIP 和 TAR 文件
地理空间数据集通常由多个文件组成。因此,它们通常以 ZIP 或 TAR 文件归档的形式分发。这些格式也可以压缩数据,但它们捆绑多个文件的能力是它们被用于地理空间数据的主要原因。虽然 TAR 格式不包含压缩算法,但它结合了 gzip 压缩,并将其作为程序选项提供。Python 有用于读取和写入 ZIP 和 TAR 归档的标准模块。这些模块分别称为zipfile和tarfile。
以下示例使用urllib下载的hancock.zip文件中的hancock.shp、hancock.shx和hancock.dbf文件,用于在之前的示例中使用。此示例假定 ZIP 文件位于当前目录中:
>>> import zipfile
>>> zip = open("hancock.zip", "rb")
>>> zipShape = zipfile.ZipFile(zip)
>>> shpName, shxName, dbfName = zipShape.namelist()
>>> shpFile = open(shpName, "wb")
>>> shxFile = open(shxName, "wb")
>>> dbfFile = open(dbfName, "wb")
>>> shpFile.write(zipShape.read(shpName))
>>> shxFile.write(zipShape.read(shxName))
>>> dbfFile.write(zipShape.read(dbfName))
>>> shpFile.close()
>>> shxFile.close()
>>> dbfFile.close()
这个例子比必要的更详细,为了清晰起见。我们可以通过在zipfile.namelist()方法周围使用for循环来缩短这个例子,并使其更健壮,而不必明确地将不同的文件定义为变量。这种方法是一种更灵活且更 Pythonic 的方法,可以用于具有未知内容的 ZIP 归档,如下面的代码行所示:
>>> import zipfile
>>> zip = open("hancock.zip", "rb")
>>> zipShape = zipfile.ZipFile(zip)
>>> for fileName in zipShape.namelist():
... out = open(fileName, "wb")
... out.write(zipShape.read(fileName))
... out.close()
>>>
现在你已经了解了zipfile模块的基础知识,让我们用我们刚刚解压的文件创建一个 TAR 归档。在这个例子中,当我们打开 TAR 归档进行写入时,我们指定写入模式为w:gz以进行 gzip 压缩。我们还指定文件扩展名为tar.gz以反映这种模式,如下面的代码行所示:
>>> import tarfile
>>> tar = tarfile.open("hancock.tar.gz", "w:gz")
>>> tar.add("hancock.shp")
>>> tar.add("hancock.shx")
>>> tar.add("hancock.dbf")
>>> tar.close()
我们可以使用简单的tarfile.extractall()方法提取文件。首先,我们使用tarfile.open()方法打开文件,然后提取它,如下面的代码行所示:
>>> tar = tarfile.open("hancock.tar.gz", "r:gz")
>>> tar.extractall()
>>> tar.close()
我们将通过结合本章学到的元素以及第二章“学习地理空间数据”中向量数据部分的元素来工作一个额外的示例。我们将从hancock.zip文件中读取边界框坐标,而无需将其保存到磁盘。我们将使用 Python 的文件-like 对象约定来传递数据。然后,我们将使用 Python 的struct模块来读取边界框,就像我们在第二章“学习地理空间数据”中所做的那样。
在这种情况下,我们将未压缩的.shp文件读入一个变量,并通过指定数据起始和结束索引(由冒号:分隔)使用 Python 数组切片来访问数据。我们能够使用列表切片,因为 Python 允许您将字符串视为列表。在这个例子中,我们还使用了 Python 的StringIO模块,以文件对象的形式在内存中临时存储数据,该对象实现了包括seek()方法在内的各种方法,而seek()方法在大多数 Python 网络模块中是缺失的,如下面的代码行所示:
>>> import urllib.request
>>> import urllib.parse
>>> import urllib.error
>>> import zipfile
>>> import io
>>> import struct
>>> url =
"https://github.com/GeospatialPython/Learn/raw/master/hancock.zip"
>>> cloudshape = urllib.request.urlopen(url)
>>> memoryshape = io.BytesIO(cloudshape.read())
>>> zipshape = zipfile.ZipFile(memoryshape)
>>> cloudshp = zipshape.read("hancock.shp")
# Access Python string as an array
>>> struct.unpack("<dddd", cloudshp[36:68])
(-89.6904544701547, 30.173943486533133, -89.32227546981174,
30.6483914869749)
如您从迄今为止的示例中看到的,Python 的标准库包含了很多功能。大多数时候,您不需要下载第三方库就能访问在线文件。
Python 标记和基于标签的解析器
基于标签的数据,尤其是不同的 XML 方言,已经成为分发地理空间数据的一种非常流行的方式。既适合机器阅读又适合人类阅读的格式通常易于处理,尽管它们为了可用性牺牲了存储效率。这些格式对于非常大的数据集可能难以管理,但在大多数情况下工作得很好。
尽管大多数格式都是某种形式的 XML(例如 KML 或 GML),但有一个显著的例外。已知文本(WKT)格式相当常见,但使用外部标记和方括号([])来包围数据,而不是像 XML 那样使用尖括号包围数据。
Python 对 XML 有标准库支持,还有一些优秀的第三方库可用。所有合适的 XML 格式都遵循相同的结构,因此您可以使用通用的 XML 库来读取它。因为 XML 是基于文本的,所以通常很容易将其作为字符串编写,而不是使用 XML 库。大多数输出 XML 的应用程序都是这种方式。
使用 XML 库编写 XML 的主要优势是您的输出通常经过验证。在创建自己的 XML 格式时,很容易出错。单个缺失的引号就可以使 XML 解析器崩溃,并给试图读取您数据的人抛出错误。当这些错误发生时,它们几乎使您的数据集变得无用。您会发现这个问题在基于 XML 的地理空间数据中非常普遍。您会发现一些解析器对不正确的 XML 比其他解析器更宽容。通常,可靠性比速度或内存效率更重要。
在lxml.de/performance.html提供的分析中,提供了不同 Python XML 解析器在内存和速度方面的基准。
minidom 模块
Python 的minidom模块是一个非常古老且易于使用的 XML 解析器。它是 Python XML 包内建的一组 XML 工具的一部分。它可以解析 XML 文件或作为字符串输入的 XML。minidom模块最适合小于约 20 MB 的小到中等大小的 XML 文档,因为在此之前的速度开始下降。
为了演示 minidom 模块,我们将使用一个示例 KML 文件,这是 Google KML 文档的一部分,你可以下载。以下链接中的数据代表从 GPS 设备传输的时间戳点位置:github.com/GeospatialPython/Learn/raw/master/time-stamp-point.kml。让我们开始吧:
- 首先,我们将通过从文件中读取数据并创建一个
minidom解析器对象来解析这些数据。文件包含一系列<Placemark>标签,这些标签包含一个点和收集该点的时间戳。因此,我们将获取文件中所有Placemarks的列表,并且可以通过检查该列表的长度来计数,如下面的代码行所示:
>>> from xml.dom import minidom
>>> kml = minidom.parse("time-stamp-point.kml")
>>> Placemarks = kml.getElementsByTagName("Placemark")
>>> len(Placemarks)
361
- 如你所见,我们检索了所有的
Placemark,总数为361。现在,让我们看看列表中的第一个Placemark元素:
>>> Placemarks[0]
<DOM Element: Placemark at 0x2045a30>
现在,每个 <Placemark> 标签都是一个 DOM 元素数据类型。为了真正看到这个元素是什么,我们调用 toxml() 方法,如下所示:
>>> Placemarks[0].toxml()
u'<Placemark>\n <TimeStamp>\n \<when>2007-01-14T21:05:02Z</when>\n
</TimeStamp>\n <styleUrl>#paddle-a</styleUrl>\n <Point>\n
<coordinates>-122.536226,37.86047,0</coordinates>\n
</Point>\n </Placemark>'
-
toxml()函数将Placemark标签内包含的所有内容输出为一个字符串对象。如果我们想将此信息打印到文本文件中,我们可以调用toprettyxml()方法,这将添加额外的缩进来使 XML 更易于阅读。 -
现在,如果我们只想从这个 placemark 中获取坐标会怎样?坐标隐藏在
coordinates标签中,该标签位于point标签内,嵌套在Placemark标签内。minidom对象的每个元素都称为 节点。嵌套节点称为子节点或子节点。子节点不仅包括标签,还包括分隔标签的空白,以及标签内的数据。因此,我们可以通过标签名称钻到coordinates标签,但之后我们需要访问data节点。所有的minidom元素都有childNodeslist,以及一个firstChild()方法来访问第一个节点。 -
我们将结合这些方法来获取第一个坐标的
data节点的data属性,我们使用列表中的索引0来引用坐标标签列表:
>>> coordinates =
Placemarks[0].getElementsByTagName("coordinates")
>>> point = coordinates[0].firstChild.data
>>> point
u'-122.536226,37.86047,0'
如果你刚接触 Python,你会注意到这些示例中的文本输出被标记为字母 u。这种标记是 Python 表示支持国际化并使用不同字符集的多语言 Unicode 字符串的方式。Python 3.4.3 对此约定略有改变,不再标记 Unicode 字符串,而是用 b 标记 UTF-8 字符串。
- 我们可以更进一步,将这个
point字符串转换为可用的数据,通过分割字符串并将结果字符串转换为 Python 浮点类型来实现,如下所示:
>>> x,y,z = point.split(",")
>>> x
u'-122.536226'
>>> y
u'37.86047'
>>> z
u'0'
>>> x = float(x)
>>> y = float(y)
>>> z = float(z)
>>> x,y,z
(-122.536226, 37.86047, 0.0)
- 使用 Python 列表推导式,我们可以一步完成这个操作,如下面的代码行所示:
>>> x,y,z = [float(c) for c in point.split(",")]
>>> x,y,z
(-122.536226, 37.86047, 0.0)
这个例子只是触及了minidom库所能做到的一小部分。关于这个库的精彩教程,请查看以下教程:www.edureka.co/blog/python-xml-parser-tutorial/。
ElementTree
minidom模块是纯 Python 编写,易于使用,自 Python 2.0 以来一直存在。然而,Python 2.5 在标准库中增加了一个更高效但更高级的 XML 解析器,称为ElementTree。ElementTree很有趣,因为它已经实现了多个版本。
有一个纯 Python 版本和一个用 C 编写的更快版本,称为cElementTree。你应该尽可能使用cElementTree,但可能你所在的平台不包括基于 C 的版本。当你导入cElementTree时,你可以测试它是否可用,并在必要时回退到纯 Python 版本:
try:
import xml.etree.cElementTree as ET
except ImportError:
import xml.etree.ElementTree as ET
ElementTree的一个伟大特性是它实现了 XPath 查询语言的一个子集。XPath 代表 XML Path,允许你使用路径式语法搜索 XML 文档。如果你经常处理 XML,学习 XPath 是必不可少的。你可以在以下链接中了解更多关于 XPath 的信息:www.w3schools.com/xml/xpath_intro.asp。
这个特性的一个问题是,如果文档指定了命名空间,就像大多数 XML 文档一样,你必须将那个命名空间插入到查询中。ElementTree不会自动为你处理命名空间。你的选择是手动指定它或尝试从根元素的标签名中通过字符串解析提取它。
我们将使用ElementTree重复minidomXML解析示例:
-
首先,我们将解析文档,然后手动定义 KML 命名空间;稍后,我们将使用 XPath 表达式和
find()方法来查找第一个Placemark元素。 -
最后,我们将找到坐标和子节点,然后获取包含纬度和经度的文本。
在这两种情况下,我们都可以直接搜索coordinates标签。但是,通过获取Placemark元素,它给我们提供了选择,稍后如果需要的话,可以获取相应的 timestamp 子元素,如下面的代码所示:
>>> tree = ET.ElementTree(file="time-stamp-point.kml")
>>> ns = "{http://www.opengis.net/kml/2.2}"
>>> placemark = tree.find(".//%sPlacemark" % ns)
>>> coordinates =
placemark.find("./{}Point/{}coordinates".format(ns, ns))
>>> coordinates.text
'-122.536226,37.86047,0'
在这个例子中,请注意我们使用了 Python 字符串格式化语法,它基于 C 中的字符串格式化概念。当我们为 placemark 变量定义 XPath 表达式时,我们使用了%占位符来指定字符串的插入。然后,在字符串之后,我们使用了%运算符后跟变量名来在占位符处插入ns命名空间变量。在coordinates变量中,我们使用了ns变量两次,因此在字符串之后指定了包含两次ns的元组。
字符串格式化是 Python 中一种简单但极其强大且有用的工具,值得学习。你可以在以下链接中找到更多信息:docs.python.org/3.4/library/string.html。
使用 ElementTree 和 Minidom 构建 XML
大多数时候,XML 可以通过连接字符串来构建,如下面的命令所示:
xml = "<?xml version="1.0" encoding="utf-8"?>"
xml += "<kml >"
xml += " <Placemark>"
xml += " <name>Office</name>"
xml += " <description>Office Building</description>"
xml += " <Point>"
xml += " <coordinates>"
xml += " -122.087461,37.422069"
xml += " </coordinates>"
xml += " </Point>"
xml += " </Placemark>"
xml += "</kml>"
然而,这种方法很容易出错,这会创建无效的 XML 文档。一种更安全的方法是使用 XML 库。让我们使用 ElementTree 构建这个简单的 KML 文档:
-
我们将定义
rootKML元素并为其分配一个命名空间。 -
然后,我们将系统地添加子元素到根元素,将元素包装为
ElementTree对象,声明 XML 编码,并将其写入名为placemark.xml的文件中,如下面的代码行所示:
>>> root = ET.Element("kml")
>>> root.attrib["xmlns"] = "http://www.opengis.net/kml/2.2"
>>> placemark = ET.SubElement(root, "Placemark")
>>> office = ET.SubElement(placemark, "name")
>>> office.text = "Office"
>>> point = ET.SubElement(placemark, "Point")
>>> coordinates = ET.SubElement(point, "coordinates")
>>> coordinates.text = "-122.087461,37.422069, 37.422069"
>>> tree = ET.ElementTree(root)
>>> tree.write("placemark.kml",
xml_declaration=True,encoding='utf-8',method="xml")
输出与之前的字符串构建示例相同,但 ElementTree 不缩进标签,而是将其作为一条长字符串写入。minidom 模块具有类似的接口,这在 Mark Pilgrim 的《深入 Python》一书中有所记录,该书籍在前面看到的 minidom 示例中有所引用。
XML 解析器,如 minidom 和 ElementTree,在格式完美的 XML 文档上工作得非常好。不幸的是,绝大多数的 XML 文档并不遵循这些规则,并包含格式错误或无效字符。你会发现你经常被迫处理这些数据,并且必须求助于非常规的字符串解析技术来获取你实际需要的少量数据。但是,多亏了 Python 和 Beautiful Soup,你可以优雅地处理糟糕的甚至极差的基于标签的数据。
Beautiful Soup 是一个专门设计用来鲁棒地处理损坏的 XML 的模块。它面向 HTML,HTML 以格式错误而闻名,但也适用于其他 XML 方言。Beautiful Soup 可在 PyPI 上找到,因此可以使用 easy_install 或 pip 来安装它,如下面的命令所示:
easy_install beautifulsoup4
或者,你可以执行以下命令:
pip install beautifulsoup4
然后,为了使用它,你只需简单地导入它:
>>> from bs4 import BeautifulSoup
为了尝试它,我们将使用来自智能手机应用程序的 GPS 交换格式(GPX)跟踪文件,该文件存在故障并导出了略微损坏的数据。您可以从以下链接下载此样本文件:raw.githubusercontent.com/GeospatialPython/Learn/master/broken_data.gpx。
这个 2,347 行的数据文件处于原始状态,除了它缺少一个关闭的</trkseg>标签,这个标签应该位于文件的末尾,就在关闭的</trk>标签之前。这个错误是由源程序中的数据导出函数引起的。这个缺陷很可能是原始开发者手动生成导出时的 GPX XML 并忘记添加此关闭标签的代码行所导致的。看看如果我们尝试用minidom解析这个文件会发生什么:
>>> gpx = minidom.parse("broken_data.gpx")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "C:\Python34\lib\xml\dom\minidom.py", line 1914, in parse
return expatbuilder.parse(file)
File "C:\Python34\lib\xml\dom\expatbuilder.py", line 924, in
parse
result = builder.parseFile(fp)
File "C:\Python34\lib\xml\dom\expatbuilder.py", line 207, in
parseFile
parser.Parse(buffer, 0)
xml.parsers.expat.ExpatError: mismatched tag: line 2346, column 2
如您从错误信息的最后一行所看到的,minidom中的底层 XML 解析器确切地知道问题所在——文件末尾存在一个mismatched标签。然而,它拒绝做任何更多的事情,只是报告了错误。您必须拥有完美无瑕的 XML,或者根本不使用,以避免这种情况。
现在,让我们尝试使用相同数据的更复杂和高效的ElementTree模块:
>>> ET.ElementTree(file="broken_data.gpx")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "C:\Python34\lib\xml\etree\ElementTree.py", line 611, in
__init__
self.parse(file)
File "C:\Python34\lib\xml\etree\ElementTree.py", line 653, in
parse
parser.feed(data)
File "C:\Python34\lib\xml\etree\ElementTree.py", line 1624, in
feed
self._raiseerror(v)
File "C:\Python34\lib\xml\etree\ElementTree.py", line 1488, in
_raiseerror
raise err
xml.etree.ElementTree.ParseError: mismatched tag: line 2346,
column 2
如您所见,不同的解析器面临相同的问题。在地理空间分析中,格式不良的 XML 是一个过于常见的现实,每个 XML 解析器都假设世界上所有的 XML 都是完美的,除了一个。这就是 Beautiful Soup 的用武之地。这个库毫不犹豫地将不良 XML 撕成可用的数据,并且它可以处理比缺失标签更严重的缺陷。即使缺少标点符号或其他语法,它也能正常工作,并给出它能给出的最佳数据。它最初是为解析 HTML 而开发的,HTML 因其格式不良而臭名昭著,但它与 XML 也相当兼容,如下所示:
>>> from bs4 import BeautifulSoup
>>> gpx = open("broken_data.gpx")
>>> soup = BeautifulSoup(gpx.read(), features="xml")
>>>
Beautiful Soup 没有任何抱怨!为了确保数据实际上是可以使用的,让我们尝试访问一些数据。Beautiful Soup 的一个令人惊叹的特性是它将标签转换为解析树的属性。如果有多个具有相同名称的标签,它将获取第一个。我们的样本数据文件有数百个<trkpt>标签。让我们访问第一个:
>>> soup.trkpt
<trkpt lat="30.307267000" lon="-89.332444000">
<ele>10.7</ele><time>2013-05-16T04:39:46Z</time></trkpt>
我们现在确信数据已经被正确解析,并且我们可以访问它。如果我们想访问所有的<trkpt>标签,我们可以使用findAll()方法来获取它们,然后使用内置的 Python len()函数来计数,如下所示:
>>> tracks = soup.findAll("trkpt")
>>> len(tracks)
2321
如果我们将解析的数据写回到文件中,Beautiful Soup 会输出修正后的版本。我们将使用 Beautiful Soup 模块的prettify()方法将固定数据保存为一个新的 GPX 文件,以格式化 XML 并添加漂亮的缩进,如下面的代码行所示:
>>> fixed = open("fixed_data.gpx", "w")
>>> fixed.write(soup.prettify())
>>> fixed.close()
Beautiful Soup 是一个非常丰富的库,具有更多功能。要进一步探索它,请访问在线的 Beautiful Soup 文档www.crummy.com/software/BeautifulSoup/bs4/documentation.html。
虽然minidom、ElementTree和cElementTree是 Python 标准库的一部分,但还有一个更强大、更受欢迎的 Python XML 库,称为lxml。lxml模块通过ElementTree API 提供了对libxml2和libxslt C 库的 Pythonic 接口。更好的事实是,lxml还可以与 Beautiful Soup 一起解析基于标签的坏数据。在某些安装中,beautifulsoup4可能需要lxml。lxml模块可通过 PyPI 获取,但需要为 C 库执行一些额外步骤。更多信息可在以下链接的lxml主页上找到:lxml.de/。
Well-Known Text (WKT)
WKT 格式已经存在多年,是一种简单的基于文本的格式,用于表示几何形状和空间参考系统。它主要用作实现 OGC Simple Features for SQL 规范的系统的数据交换格式。请看以下多边形的 WKT 表示示例:
POLYGON((0 0,4 0,4 4,0 4,0 0),(1 1, 2 1, 2 2, 1 2,1 1))
目前,读取和写入 WKT 的最佳方式是使用 Shapely 库。Shapely 提供了一个非常 Python 导向或 Pythonic 的接口,用于我们第三章中描述的Geometry Engine - Open Source(GEOS)库,地理空间技术景观。
您可以使用easy_install或pip安装 Shapely。您还可以使用上一节中提到的网站上的 wheel。Shapely 有一个 WKT 模块,可以加载和导出这些数据。让我们使用 Shapely 加载之前的多边形样本,然后通过计算其面积来验证它是否已作为多边形对象加载:
>>> import shapely.wkt
>>> wktPoly = "POLYGON((0 0,4 0,4 4,0 4,0 0),(1 1, 2 1, 2 2, 1 2,
1 1))"
>>> poly = shapely.wkt.loads(wktPoly)
>>> poly.area
15.0
我们可以通过简单地调用其wkt属性,将任何 Shapely 几何形状转换回 WKT,如下所示:
>>> poly.wkt
'POLYGON ((0.0 0.0, 4.0 0.0, 4.0 4.0, 0.0 4.0, 0.0 0.0), (1.0 1.0,
2.0 1.0, 2.0 2.0, 1.0 2.0, 1.0 1.0))'
Shapely 还可以处理 WKT 的二进制对应物,称为 Well-Known Binary(WKB),它用于在数据库中以二进制对象的形式存储 WKT 字符串。Shapely 使用其wkb模块以与wkt模块相同的方式加载 WKB,并且可以通过调用该对象的wkb属性来转换几何形状。
Shapely 是处理 WKT 数据最 Pythonic 的方式,但您也可以使用 OGR 库的 Python 绑定,这是我们本章早些时候安装的。
对于这个例子,我们将使用一个包含一个简单多边形的 shapefile,它可以作为一个 ZIP 文件下载。您可以通过以下链接获取:github.com/GeospatialPython/Learn/raw/master/polygon.zip。
在以下示例中,我们将打开 shapefile 数据集中的polygon.shp文件,调用所需的GetLayer()方法,获取第一个(也是唯一一个)要素,然后将其导出为 WKT 格式:
>>> from osgeo import ogr
>>> shape = ogr.Open("polygon.shp")
>>> layer = shape.GetLayer()
>>> feature = layer.GetNextFeature()
>>> geom = feature.GetGeometryRef()
>>> wkt = geom.ExportToWkt()
>>> wkt
' POLYGON ((-99.904679362176353 51.698147686745074,
-75.010398603076666 46.56036851832075,-75.010398603076666
46.56036851832075,-75.010398603076666 46.56036851832075,
-76.975736557742451 23.246272688996914,-76.975736557742451
23.246272688996914,-76.975736557742451 23.246272688996914,
-114.31715769639194 26.220870210283724,-114.31715769639194
26.220870210283724,-99.904679362176353 51.698147686745074))'
注意,使用 OGR 时,你必须逐个读取每个要素并单独导出它,因为ExporttoWkt()方法是在要素级别。现在我们可以使用包含导出的wkt变量来读取 WKT 字符串。我们将将其导入ogr,并获取多边形的边界框,也称为包围盒,如下所示:
>>> poly = ogr.CreateGeometryFromWkt(wkt)
>>> poly.GetEnvelope()
(-114.31715769639194, -75.01039860307667, 23.246272688996914,
51.698147686745074)
Shapely 和 OGR 用于读取和写入有效的 WKT 字符串。当然,就像 XML 一样,它也是文本,你可以在必要时将少量 WKT 作为字符串进行操作。接下来,我们将探讨一个在地理空间领域变得越来越常见的现代文本格式。
Python JSON 库
JavaScript 对象表示法(JSON)正在迅速成为许多领域的首选数据交换格式。轻量级的语法以及它与 JavaScript 数据结构的相似性,使得它非常适合 Python,Python 也借鉴了一些数据结构。
以下 GeoJSON 样本文档包含一个单独的点:
{
"type": "Feature",
"id": "OpenLayers.Feature.Vector_314",
"properties": {},
"geometry": {
"type": "Point",
"coordinates": [
97.03125,
39.7265625
]
},
"crs": {
"type": "name",
"properties": {
"name": "urn:ogc:def:crs:OGC:1.3:CRS84"
}
}
}
这个样本只是一个带有新属性的基本点,这些属性将被存储在几何的属性数据结构中。在前面的例子中,ID、坐标和 CRS 信息将根据你的特定数据集而变化。
让我们使用 Python 修改这个示例 GeoJSON 文档。首先,我们将样本文档压缩成一个字符串,以便更容易处理:
>>> jsdata = """{
"type": "Feature",
"id": "OpenLayers.Feature.Vector_314",
"properties": {},
"geometry": {
"type": "Point",
"coordinates": [
97.03125,
39.7265625
]
},
"crs": {
"type": "name",
"properties": {
"name": "urn:ogc:def:crs:OGC:1.3:CRS84"
}
}
}"""
现在,我们可以使用前面代码中创建的 GeoJSON jsdata 字符串变量,在以下示例中使用。
json 模块
GeoJSON 看起来非常类似于 Python 的嵌套字典和列表集合。为了好玩,让我们尝试使用 Python 的eval()函数将其解析为 Python 代码:
>>> point = eval(jsdata)
>>> point["geometry"]
{'type': 'Point', 'coordinates': [97.03125, 39.7265625]}
哇!这成功了!我们只需一步就将那个随机的 GeoJSON 字符串转换成了原生的 Python 数据。记住,JSON 数据格式基于 JavaScript 语法,这恰好与 Python 相似。此外,随着你对 GeoJSON 数据的深入了解以及处理更大的数据,你会发现 JSON 允许使用 Python 不允许的字符。使用 Python 的eval()函数也被认为是非常不安全的。但就保持简单而言,这已经是最简单的方法了!
由于 Python 追求简单,更高级的方法并没有变得更加复杂。让我们使用 Python 的json模块,它是标准库的一部分,将相同的字符串正确地转换为 Python:
>>> import json
>>> json.loads(jsdata)
{u'geometry': {u'type': u'Point', u'coordinates': [97.03125,
39.7265625]}, u'crs': {u'type': u'name', u'properties': {u'name':
u'urn:ogc:def:crs:OGC:1.3:CRS84'}}, u'type': u'Feature', u'id': u'OpenLayers.Feature.Vector_314',
u'properties':
{}}
作为旁注,在先前的例子中,CRS84 属性是常见 WGS84 坐标系统的同义词。json模块添加了一些很好的功能,例如更安全的解析和将字符串转换为 Unicode。我们可以以几乎相同的方式将 Python 数据结构导出为 JSON:
>>> pydata = json.loads(jsdata)
>>> json.dumps(pydata)
'{"geometry": {"type": "Point", "coordinates": [97.03125,
39.7265625]}, "crs": {"type": "name", "properties": {"name":
"urn:ogc:def:crs:OGC:1.3:CRS84"}}, "type" : "Feature", "id": "OpenLayers.Feature.Vector_314", "properties":
{}}'
当你导出数据时,它会以一长串难以阅读的字符串形式输出。我们可以通过传递dumps()方法一个缩进值来打印数据,使其更容易阅读:
print(json.dumps(pydata, indent=4))
{
"type": "Feature",
"id":
"OpenLayers.Feature.Vector_314",
"properties": {},
"geometry": {
"type": "Point",
"coordinates": [
97.03125,
39.7265625
]
},
"crs": {
"type": "name",
"properties": {
"name": "urn:ogc:def:crs:OGC:1.3:CRS84"
}
}
}
现在我们已经了解了 json 模块,让我们看看地理空间版本 geojson。
geojson 模块
我们可以愉快地继续使用 json 模块读取和写入 GeoJSON 数据,但还有更好的方法。PyPI 上可用的 geojson 模块提供了一些独特的优势。首先,它了解 GeoJSON 规范的要求,这可以节省大量的输入。让我们使用此模块创建一个简单的点并将其导出为 GeoJSON:
>>> import geojson
>>> p = geojson.Point([-92, 37])
这次,当我们转储 JSON 数据以供查看时,我们将添加一个缩进参数,其值为 4,这样我们就可以得到格式良好的缩进 JSON 数据,更容易阅读:
>>> geojs = geojson.dumps(p, indent=4)
>>> geojs
我们的结果如下:
{
"type": "Point",
"coordinates": [
-92,
37
]
}
POINT (-92 37)
注意到 geojson 模块为不同的数据类型提供了一个接口,并使我们免于手动设置类型和坐标属性。现在,想象一下如果你有一个具有数百个要素的地理对象。你可以通过编程构建这个数据结构,而不是构建一个非常大的字符串。
geojson 模块也是 Python geo_interface 规范的参考实现。这个接口允许协作程序以 Pythonic 的方式无缝交换数据,而无需程序员显式导出和导入 GeoJSON 字符串。因此,如果我们想将使用 geojson 模块创建的点传递给 Shapely 模块,我们可以执行以下命令,该命令将 geojson 模块的点对象直接读取到 Shapely 中,然后将其导出为 WKT:
>>> from shapely.geometry import asShape
>>> point = asShape(p)
>>> point.wkt
'POINT (-92.0000000000000000 37.0000000000000000)'
越来越多的地理空间 Python 库正在实现 geojson 和 geo_interface 功能,包括 PyShp、Fiona、Karta 和 ArcGIS。对于 QGIS,存在第三方实现。
GeoJSON 是一种简单且易于人类和计算机阅读的文本格式。现在,我们将查看一些二进制矢量格式。
OGR
我们提到了 OGR 作为处理 WKT 字符串的方法,但它的真正力量在于作为一个通用的矢量库。本书力求提供纯 Python 解决方案,但没有单个库甚至接近 OGR 可以处理的格式多样性。
让我们使用 OGR Python API 读取一个示例点形状文件。示例形状文件可以作为 ZIP 文件在此处下载:github.com/GeospatialPython/Learn/raw/master/point.zip。
这个点形状文件有五个带有单个数字、正坐标的点。属性列表了点的创建顺序,这使得它在测试中非常有用。这个简单的例子将读取点形状文件,并遍历每个要素;然后,它将打印每个点的 x 和 y 值,以及第一个属性字段的值:
>>> import ogr
>>> shp = ogr.Open("point.shp")
>>> layer = shp.GetLayer()
>>> for feature in layer:
... geometry = feature.GetGeometryRef()
... print(geometry.GetX(), geometry.GetY(),
feature.GetField("FIRST_FLD"))
...
1.0 1.0 First
3.0 1.0 Second
4.0 3.0 Third
2.0 2.0 Fourth
0.0 0.0 Appended
这个例子很简单,但 OGR 在脚本变得更加复杂时可能会变得相当冗长。接下来,我们将看看处理形状文件的一种更简单的方法。
PyShp
PyShp 是一个简单的纯 Python 库,用于读取和写入形状文件。它不执行任何几何操作,仅使用 Python 的标准库。它包含在一个易于移动、压缩到小型嵌入式平台和修改的单个文件中。它也与 Python 3 兼容。它还实现了__geo_interface__。PyShp 模块可在 PyPI 上找到。
让我们用 PyShp 重复之前的 OGR 示例:
>>> import shapefile
>>> shp = shapefile.Reader("point.shp")
>>> for feature in shp.shapeRecords():
... point = feature.shape.points[0]
... rec = feature.record[0]
... print(point[0], point[1], rec)
...
1.0 1.0 First
3.0 1.0 Second
4.0 3.0 Third
2.0 2.0 Fourth
0.0 0.0 Appended //
dbfpy
OGR 和 PyShp 都读取和写入.dbf文件,因为它们是形状文件规范的一部分。.dbf文件包含形状文件的属性和字段。然而,这两个库对.dbf的支持非常基础。偶尔,你可能需要进行一些重型的 DBF 工作。dbfpy3模块是一个专门用于处理.dbf文件的纯 Python 模块。它目前托管在 GitHub 上。你可以通过指定下载文件来强制easy_install找到下载:
easy_install -f
https://github.com/GeospatialPython/dbfpy3/archive/master.zip
如果您使用pip安装软件包,请使用以下命令:
pip install
https://github.com/GeospatialPython/dbfpy3/archive/master.zip
以下形状文件包含超过 600 条.dbf记录,代表美国人口普查局的区域,这使得它成为尝试dbfpy的好样本:github.com/GeospatialPython/Learn/raw/master/GIS_CensusTract.zip.
让我们打开这个形状文件的.dbf文件并查看第一条记录:
>>> from dbfpy3 import dbf
>>> db = dbf.Dbf("GIS_CensusTract_poly.dbf")
>>> db[0]
GEODB_OID: 4029 (<type 'int'>)
OBJECTID: 4029 (<type 'int'>)
PERMANE0: 61be9239-8f3b-4876-8c4c-0908078bc597 (<type 'str'>)
SOURCE_1: NA (<type 'str'>)
SOURCE_2: 20006 (<type 'str'>)
SOURCE_3: Census Tracts (<type 'str'>)
SOURCE_4: Census Bureau (<type 'str'>)
DATA_SE5: 5 (<type 'str'>)
DISTRIB6: E4 (<type 'str'>)
LOADDATE: 2007-03-13 (<type 'datetime.date'>)
QUALITY: 2 (<type 'str'>)
SCALE: 1 (<type 'str'>)
FCODE: 1734 (<type 'str'>)
STCO_FI7: 22071 (<type 'str'>)
STATE_NAME: 22 (<type 'str'>)
COUNTY_8: 71 (<type 'str'>)
CENSUST9: 22071001734 (<type 'str'>)
POPULAT10: 1760 (<type 'int'>)
AREASQKM: 264.52661934 (<type 'float'>)
GNIS_ID: NA (<type 'str'>)
POPULAT11: 1665 (<type 'int'>)
DB2GSE_12: 264526619.341 (<type 'float'>)
DB2GSE_13: 87406.406192 (<type 'float'>)
该模块快速且容易地为我们提供列名和数据值,而不是将它们作为单独的列表处理,这使得它们更容易管理。现在,让我们将包含在POPULAT10中的人口字段增加1:
>>> rec = db[0]
>>> field = rec["POPULAT10"]
>>> rec["POPULAT10"] = field + 1
>>> rec.store()
>>> del rec
>>> db[0]["POPULAT10"]
1761
请记住,OGR 和 PyShp 都可以执行此相同的过程,但如果您只是对.dbf文件进行大量更改,dbfp3y会使它稍微容易一些。
Shapely
Shapely 在已知文本(WKT)部分被提及,因为它具有导入和导出功能。然而,它的真正目的是作为一个通用的几何库。Shapely 是 GEOS 库的几何操作的高级 Python 接口。实际上,Shapely 故意避免读取或写入文件。它完全依赖于从其他模块导入和导出数据,并专注于几何操作。
让我们做一个快速的 Shapely 演示,我们将定义一个 WKT 多边形,然后将其导入 Shapely。然后,我们将测量面积。我们的计算几何将包括通过五个任意单位对多边形进行缓冲,这将返回一个新的更大的多边形,我们将测量其面积:
>>> from shapely import wkt, geometry
>>> wktPoly = "POLYGON((0 0,4 0,4 4,0 4,0 0))"
>>> poly = wkt.loads(wktPoly)
>>> poly.area
16.0
>>> buf = poly.buffer(5.0)
>>> buf.area
174.41371226364848
然后,我们可以执行缓冲区面积与原始多边形面积的差值,如下所示:
>>> buf.difference(poly).area
158.413712264
如果您不能使用纯 Python,那么像 Shapely 一样干净、功能强大的 Python API 无疑是下一个最佳选择。
Fiona
Fiona 库提供了围绕 OGR 库的简单 Python API,用于数据访问,仅此而已。这种方法使其易于使用,并且在使用 Python 时比 OGR 更简洁。Fiona 默认输出 GeoJSON。您可以在 www.lfd.uci.edu/~gohlke/pythonlibs/#fiona 找到 Fiona 的 wheel 文件。
例如,我们将使用本章前面查看的 dbfpy 示例中的 GIS_CensusTract_poly.shp 文件。
首先,我们将导入 fiona 和 Python 的 pprint 模块以格式化输出。然后,我们将打开 shapefile 并检查其驱动程序类型:
>>> import fiona
>>> from pprint import pprint
>>> f = fiona.open("GIS_CensusTract_poly.shp")
>>> f.driver
ESRI shapefile
接下来,我们将检查其坐标参考系统并获取数据边界框,如下所示:
>>> f.crs
{'init': 'epsg:4269'}
>>> f.bounds
(-89.8744162216216, 30.161122135135138, -89.1383837783784,
30.661213864864862)
现在,我们将以 geojson 格式查看数据模式,并使用 pprint 模块进行格式化,如下面的代码行所示:
>>> pprint(f.schema)
{'geometry': 'Polygon',
'properties': {'GEODB_OID': 'float:11',
'OBJECTID': 'float:11',
'PERMANE0': 'str:40',
'SOURCE_1': 'str:40',
'SOURCE_2': 'str:40',
'SOURCE_3': 'str:100',
'SOURCE_4': 'str:130',
'DATA_SE5': 'str:46',
'DISTRIB6': 'str:188',
'LOADDATE': 'date',
'QUALITY': 'str:35',
'SCALE': 'str:52',
'FCODE': 'str:38',
'STCO_FI7': 'str:5',
'STATE_NAME': 'str:140',
'COUNTY_8': 'str:60',
'CENSUST9': 'str:20',
'POPULAT10': 'float:11',
'AREASQKM': 'float:31.15',
'GNIS_ID': 'str:10',
'POPULAT11': 'float:11',
'DB2GSE_12': 'float:31.15',
'DB2GSE_13': 'float:31.15'}}
接下来,让我们获取特征数量的统计:
>>> len(f)
45
最后,我们将打印一条记录作为格式化的 GeoJSON,如下所示:
pprint(f[1])
{'geometry': {'coordinates': [[[(-89.86412366375093,
30.661213864864862), (-89.86418691770497, 30.660764012731285),
(-89.86443391770518, 30.659652012730202),
...
'type': 'MultiPolygon'},
'id': '1',
'properties': {'GEODB_OID': 4360.0,
'OBJECTID': 4360.0,
'PERMANE0': '9a914eef-9249-44cf-a05f-af4b48876c59',
'SOURCE_1': 'NA',
'SOURCE_2': '20006',
...
'DB2GSE_12': 351242560.967882,
'DB2GSE_13': 101775.283967268},
'type': 'Feature'}
GDAL
GDAL 是用于栅格数据的占主导地位的地理空间库。其栅格功能非常显著,以至于它是任何语言中几乎所有地理空间工具包的一部分,Python 也不例外。要了解 GDAL 在 Python 中的基本工作原理,请下载以下示例栅格卫星图像的 ZIP 文件并解压:github.com/GeospatialPython/Learn/raw/master/SatImage.zip。让我们打开这张图片,看看它有多少波段以及每个轴上有多少像素:
>>> from osgeo import gdal
>>> raster = gdal.Open("SatImage.tif")
>>> raster.RasterCount
3
>>> raster.RasterXSize
2592
>>> raster.RasterYSize
2693
通过在 OpenEV 中查看,我们可以看到以下图像有三个波段,2,592 列像素和 2,693 行像素:

GDAL 是 Python 中一个极快的地理空间栅格读取器和写入器。它还可以很好地重新投影图像,除了能够执行一些其他技巧之外。然而,GDAL 的真正价值来自于它与下一个 Python 模块的交互,我们现在将对其进行检查。
NumPy
NumPy 是一个专为 Python 和科学计算设计的极快的多维 Python 数组处理器,但用 C 语言编写。它可以通过 PyPI 或作为 wheel 文件(可在 www.lfd.uci.edu/~gohlke/pythonlibs/#numpy 找到)获得,并且可以轻松安装。除了其惊人的速度外,NumPy 的魔力还包括其与其他库的交互。NumPy 可以与 GDAL、Shapely、Python 图像库(PIL)以及许多其他领域的科学计算 Python 库交换数据。
作为 NumPy 能力的快速示例,我们将将其与 GDAL 结合起来读取我们的示例卫星图像,然后创建其直方图。GDAL 和 NumPy 之间的接口是一个名为 gdal_array 的 GDAL 模块,它依赖于 NumPy。Numeric 是 NumPy 模块的传统名称。gdal_array 模块导入 NumPy。
在以下示例中,我们将使用gdal_array,它导入 NumPy,将图像作为数组读取,获取第一个波段,并将其保存为 JPEG 图像:
>>> from osgeo import gdal_array
>>> srcArray = gdal_array.LoadFile("SatImage.tif")
>>> band1 = srcArray[0]
>>> gdal_array.SaveArray(band1, "band1.jpg", format="JPEG")
这个操作在 OpenEV 中给出了以下灰度图像:

PIL
PIL 最初是为遥感开发的,但已经发展成为一个通用的 Python 图像编辑库。像 NumPy 一样,它是用 C 语言编写的,以提高速度,但它是专门为 Python 设计的。除了图像创建和处理外,它还有一个有用的栅格绘图模块。PIL 也可以通过 PyPI 获得;然而,在 Python 3 中,你可能想使用 Pillow 模块,它是 PIL 的升级版本。正如你将在以下示例中看到的那样,我们可以使用 Python 的 try 语句以两种可能的方式导入 PIL,这取决于你的安装方式。
在这个示例中,我们将结合 PyShp 和 PIL 将前一个示例中的hancock shapefile 转换为栅格并保存为图像。我们将使用类似于第一章中的 SimpleGIS 的世界到像素坐标转换,即使用 Python 进行地理空间分析。我们将创建一个图像作为 PIL 中的画布,然后使用 PIL 的ImageDraw模块来渲染多边形。最后,我们将将其保存为 PNG 图像,如下面的代码行所示:
>>> try:
>>> import Image
>>> import ImageDraw
>>> except:
>>> from PIL import Image
>>> from PIL import ImageDraw
>>> import shapefile
>>> r = shapefile.Reader("hancock.shp")
>>> xdist = r.bbox[2] - r.bbox[0]
>>> ydist = r.bbox[3] - r.bbox[1]
>>> iwidth = 400
>>> iheight = 600
>>> xratio = iwidth/xdist
>>> yratio = iheight/ydist
>>> pixels = []
>>> for x,y in r.shapes()[0].points:
... px = int(iwidth - ((r.bbox[2] - x) * xratio))
... py = int((r.bbox[3] - y) * yratio)
... pixels.append((px,py))
...
>>> img = Image.new("RGB", (iwidth, iheight), "white")
>>> draw = ImageDraw.Draw(img)
>>> draw.polygon(pixels, outline="rgb(203, 196, 190)",
fill="rgb(198, 204, 189)")
>>> img.save("hancock.png")
这个示例创建以下图像:

PNGCanvas
有时,你可能发现 PIL 对于你的目的来说过于强大,或者你不允许安装 PIL,因为你没有使用 Python 模块的 C 语言创建和编译的机器的管理权限。在这些情况下,你通常可以使用轻量级的纯 Python PNGCanvas 模块来解决问题。你可以使用easy_install或 pip 来安装它。
使用此模块,我们可以重复使用 PIL 执行的光栅形状文件示例,但使用纯 Python,如下所示:
>>> import shapefile
>>> import pngcanvas
>>> r = shapefile.Reader("hancock.shp")
>>> xdist = r.bbox[2] - r.bbox[0]
>>> ydist = r.bbox[3] - r.bbox[1]
>>> iwidth = 400
>>> iheight = 600
>>> xratio = iwidth/xdist
>>> yratio = iheight/ydist
>>> pixels = []
>>> for x,y in r.shapes()[0].points:
... px = int(iwidth - ((r.bbox[2] - x) * xratio))
... py = int((r.bbox[3] - y) * yratio)
... pixels.append([px,py])
...
>>> c = pngcanvas.PNGCanvas(iwidth,iheight)
>>> c.polyline(pixels)
>>> f = open("hancock_pngcvs.png", "wb")
>>> f.write(c.dump())
>>> f.close()
这个示例提供了一个简单的轮廓,因为 PNGCanvas 没有内置的填充方法:

GeoPandas
Pandas 是一个高性能的 Python 数据分析库,可以处理大型表格数据集(类似于数据库),有序/无序,标记矩阵或未标记的统计数据。GeoPandas 是 Pandas 的一个地理空间扩展,基于 Shapely、Fiona、PyProj、Matplotlib 和 Descartes 构建,所有这些都必须安装。它允许你轻松地在 Python 中执行操作,否则可能需要像 PostGIS 这样的空间数据库。你可以从www.lfd.uci.edu/~gohlke/pythonlibs/#panda下载 GeoPandas 的 wheel 文件。
以下脚本打开一个 shapefile 并将其转换为 GeoJSON。然后,它使用matplotlib创建一个地图:
>>> import geopandas
>>> import matplotlib.pyplot as plt
>>> gdf = geopandas.GeoDataFrame
>>> census = gdf.from_file("GIS_CensusTract_poly.shp")
>>> census.plot()
>>> plt.show()
以下图像是先前命令的结果地图:

PyMySQL
流行的 MySQL(可在 dev.mysql.com/downloads 获取)数据库正在逐渐发展空间功能。它支持 OGC 几何形状和一些空间函数。它还提供了 PyMySQL 库中的纯 Python API。有限的空间函数使用平面几何和边界矩形,而不是球面几何和形状。MySQL 的最新开发版本包含一些额外的函数,这些函数提高了这一功能。
在以下示例中,我们将创建一个名为 spatial_db 的 MySQL 数据库。然后,我们将添加一个名为 PLACES 的表,其中包含一个几何列。接下来,我们将添加两个城市作为点位置。最后,我们将使用 MySQL 的 ST_Distance 函数计算距离,并将结果从度数转换为英里。
首先,我们将导入我们的 mysql 库并设置数据库连接:
# Import the python mysql library
import pymysql
# Establish a database connection on our local
# machine as the root database user.
conn = pymysql.connect(host='localhost', port=3306,
user='root', passwd='', db='mysql')
接下来,我们获取数据库游标:
# Get the database cursor needed to change
# the database
cur = conn.cursor()
现在,我们检查数据库是否已存在,如果存在则将其删除:
# If the database already exists, delete
# it and recreate it either way.
cur.execute("DROP DATABASE IF EXISTS spatial_db")
cur.execute("CREATE DATABASE spatial_db")
# Close the cursor and the connection
cur.close()
conn.close()
现在,我们设置一个新的连接并获取游标:
# Set up a new connection and cursor
conn = pymysql.connect(host='localhost', port=3306,
user='root', passwd='', db='spatial_db')
cur = conn.cursor()
接下来,我们可以创建我们的新表并添加我们的字段:
# Create our geospatial table
cur.execute("CREATE TABLE PLACES (id int NOT NULL
# Add name and location fields. The location
# field is spatially enabled to hold GIS data
AUTO_INCREMENT PRIMARY KEY, Name varchar(50) NOT NULL, location
Geometry NOT NULL)")
添加了字段后,我们就可以为一些城市的地理位置插入记录了:
# Insert a name and location for the city of
# New Orleans
cur.execute("INSERT INTO PLACES (name, location) VALUES ('NEW
ORLEANS', GeomFromText('POINT(30.03 90.03)'))")
# Insert a name and location for the city of
# Memphis.
cur.execute("INSERT INTO PLACES (name, location) VALUES
('MEMPHIS', GeomFromText('POINT(35.05 90.00)'))")
然后,我们可以将更改提交到数据库:
# Commit the changes to the database
conn.commit()
现在,我们可以查询数据库了!首先,我们将获取所有点位置列表:
# Now let's read the data. Select all of
# the point locations from the database.
cur.execute("SELECT AsText(location) FROM PLACES")
现在,我们将从查询结果中提取两个点:
# We know there's only two points, so we'll
# just parse them.
p1, p2 = [p[0] for p in cur.fetchall()]
在我们能够测量距离之前,我们需要将点列表转换为地理空间几何形状:
# Now we'll convert the data
# to geometries to measure the distance
# between the two cities
cur.execute("SET @p1 = ST_GeomFromText('{}')".format(p1))
cur.execute("SET @p2 = ST_GeomFromText('{}')".format(p2))
最后,我们可以使用 Distance 存储过程来测量两个几何形状之间的距离:
# Now we do the measurement function which
# is also a database query.
cur.execute("SELECT ST_Distance(@p1, @p2)")
d = float(cur.fetchone()[0])
# Print the distance as a formatted
# string object.
print("{:.2f} miles from New Orleans to Memphis".format(d *
70))
cur.close()
conn.close()
输出如下:
351.41 miles from New Orleans to Memphis
其他空间数据库选项也可用,包括 PostGIS 和 SpatiaLite;然而,这些空间引擎在 Python 3 中的支持最多处于开发阶段。您可以通过 OGR 库访问 PostGIS 和 MySQL;然而,MySQL 的支持有限。
PyFPDF
纯 Python 的 PyFPDF 库是一种创建 PDF(包括地图)的轻量级方式。由于 PDF 格式是一种广泛使用的标准,PDF 通常用于分发地图。您可以通过 PyPI 以 fpdf 的方式安装它。该软件的官方名称是 PyFPDF,因为它 PHP 语言模块 fpdf 的一部分。此模块使用一个称为单元格的概念,在页面的特定位置布局项目。作为一个快速示例,我们将从 PIL 示例中导入的 hancock.png 图像放入名为 map.pdf 的 PDF 中,以创建一个简单的 PDF 地图。地图顶部将有标题文本,说明汉考克县边界,然后是地图图像:
>>> import fpdf
>>> # PDF constructor:
>>> # Portrait, millimeter units, A4 page size
>>> pdf=fpdf.FPDF("P", "mm", "A4")
>>> # create a new page
>>> pdf.add_page()
>>> # Set font: arial, bold, size 20
>>> pdf.set_font('Arial','B',20)
>>> # Layout cell: 160 x 25mm, title, no border, centered
>>> pdf.cell(160,25,'Hancock County Boundary', \
>>> border=0, align="C")
>>> # Write the image specifying the size
>>> pdf.image("hancock.png",25,50,110,160)
>>> # Save the file: filename, F = to file System
>>> pdf.output('map.pdf','F')
如果您在 Adobe Acrobat Reader 或其他 PDF 阅读器(如 Sumatra PDF)中打开名为 map.pdf 的 PDF 文件,您会看到图像现在位于 A4 页面的中心。地理空间产品通常作为更大报告的一部分,PyFPDF 模块简化了自动生成 PDF 报告的过程。
地理空间 PDF
便携式文档格式,或PDF,是一种存储和以跨平台和应用独立的方式呈现数字化文本和图像的文件格式。PDF 是一种广泛使用的文档格式,它也被扩展用于存储地理空间信息。
PDF 规范从 1.7 版本开始包括地理空间 PDF 的扩展,这些扩展将文档的部分映射到物理空间,也称为地理参照。您可以创建点、线或多边形作为地理空间几何形状,这些几何形状也可以有属性。
在 PDF 内部编码地理空间信息有两种方法。一家名为 TerraGo 的公司制定了一个规范,该规范已被开放地理空间联盟作为最佳实践采用,但不是一个标准。这种格式被称为GeoPDF。Adobe Systems 提出的扩展,即创建 PDF 规范的 ISO 32000,目前正在纳入规范的 2.0 版本。
TerraGo 的地理空间 PDF 产品符合 OGC 最佳实践文档和 Adobe PDF 扩展。但是,TerraGo 超越了这些功能,包括图层和其他 GIS 功能。然而,您必须使用 TerraGo 的 Adobe Acrobat 或其他软件的插件来访问这些功能。至少,TerraGo 支持至少在 PDF 软件中显示所需的功能。
在 Python 中,有一个名为geopdf的库,它与 TerraGo 无关,但支持 OGC 最佳实践。这个库最初是由 Prominent Edge 的 Tyler Garner 开发的,用于 Python 2。它已被移植到 Python 3。
从 GitHub 安装geopdf就像运行以下命令一样简单:
pip install https://github.com/GeospatialPython/geopdf-py3/archive/master.zip
以下示例重新创建了我们在第一章,“使用 Python 学习地理空间分析”,在简单 GIS部分创建的地图,作为一个地理空间 PDF。geopdf库依赖于 Python 的 ReportLab PDF 库。我们需要执行的步骤如下:
-
创建一个 PDF 绘图画布。
-
为科罗拉多州绘制一个矩形。
-
设置一个函数将地图坐标转换为屏幕坐标。
-
绘制并标注城市和人口。
-
将该州的所有角落注册为地理空间 PDF 坐标,这些坐标将整个地图进行地理参照。
Python 代码的注释解释了每一步发生了什么:
# Import the geopdf library
from geopdf import GeoCanvas
# Import the necessary Reportlab modules
from reportlab.pdfbase.pdfdoc import PDFString, PDFArray
# Create a canvas with a name for our pdf.
canvas = GeoCanvas('SimpleGIS.pdf')
# Draw a rectangle to represent the State boundary
canvas.rect(100, 400, 400, 250, stroke=1)
# DATA MODEL
# All layers will have a name, 1+ points, and population count
NAME = 0
POINTS = 1
POP = 2
# Create the state layer
state = ["COLORADO", [[-109, 37], [-109, 41], [-102, 41], [-102, 37]], 5187582]
# Cities layer list
# city = [name, [point], population]
cities = []
# Add Denver
cities.append(["DENVER", [-104.98, 39.74], 634265])
# Add Boulder
cities.append(["BOULDER", [-105.27, 40.02], 98889])
# Add Durango
cities.append(["DURANGO", [-107.88, 37.28], 17069])
# MAP GRAPHICS RENDERING
map_width = 400
map_height = 250
# State Bounding Box
# Use Python min/max function to get state bounding box
minx = 180
maxx = -180
miny = 90
maxy = -90
for x, y in state[POINTS]:
if x < minx:
minx = x
elif x > maxx:
maxx = x
if y < miny:
miny = y
elif y > maxy:
maxy = y
# Get earth distance on each axis
dist_x = maxx - minx
dist_y = maxy - miny
# Scaling ratio each axis
# to map points from world to screen
x_ratio = map_width / dist_x
y_ratio = map_height / dist_y
def convert(point):
"""Convert lat/lon to screen coordinates"""
lon = point[0]
lat = point[1]
x = map_width - ((maxx - lon) * x_ratio)
y = map_height - ((maxy - lat) * y_ratio)
# Python turtle graphics start in the middle of
# the screen so we must offset the points so they
# are centered
x = x + 100
y = y + 400
return [x, y]
# Set up our map labels
canvas.setFont("Helvetica", 20)
canvas.drawString(250, 500, "COLORADO")
# Use smaller text for cities
canvas.setFont("Helvetica", 8)
# Draw points and label the cities
for city in cities:
pixel = convert(city[POINTS])
print(pixel)
# Place a point for the city
canvas.circle(pixel[0], pixel[1], 5, stroke=1, fill=1)
# Label the city
canvas.drawString(pixel[0] + 10, pixel[1], city[NAME] + ", Population: " + str(city[POP]))
# A series of registration point pairs (pixel x,
# pixel y, x, y) to spatially enable the PDF. We only
# need to do the state boundary.
# The cities will be contained with in it.
registration = PDFArray([
PDFArray(map(PDFString, ['100', '400', '{}'.format(minx), '{}'.format(maxy)])),
PDFArray(map(PDFString, ['500', '400', '{}'.format(maxx), '{}'.format(maxy)])),
PDFArray(map(PDFString, ['100', '150', '{}'.format(minx), '{}'.format(miny)])),
PDFArray(map(PDFString, ['500', '150', '{}'.format(maxx), '{}'.format(miny)]))
])
# Add the map registration
canvas.addGeo(Registration=registration)
# Save our geopdf
canvas.save()
Rasterio
我们在本章早期介绍的 GDAL 库功能非常强大,但它并不是为 Python 设计的。rasterio库通过将 GDAL 包装在一个非常简单、干净的 Pythonic API 中,解决了这个问题,用于栅格数据操作。
此示例使用本章 GDAL 示例中的卫星图像。我们将打开图像并获取一些元数据,如下所示
>>> import rasterio
>>> ds = rasterio.open("SatImage.tif")
>>> ds.name
'SatImage.tif'
>>> ds.count
3
>>> ds.width
2592
>>> ds.height
2693
OSMnx
osmnx库结合了Open Street Map(OSM)和强大的 NetworkX 库来管理用于路由的街道网络。这个库有数十个依赖项,它将这些依赖项整合起来以执行下载、分析和可视化街道网络的复杂步骤。
您可以使用pip尝试安装osmnx:
pip install osmnx
然而,您可能会因为依赖项而遇到一些安装问题。在这种情况下,使用我们将在本章后面介绍的 Conda 系统会更容易一些。
以下示例使用osmnx从 OSM 下载城市的街道数据,从中创建街道网络,并计算一些基本统计数据:
>>> import osmnx as ox
>>> G = ox.graph_from_place('Bay Saint Louis, MS , USA', network_type='drive')
>>> stats = ox.basic_stats(G)
>>> stats["street_length_avg"]
172.1468804611654
Jupyter
当您处理地理空间或其他科学数据时,您应该了解 Jupyter 项目。Jupyter Notebook 应用程序在网页浏览器中创建和显示笔记本文档,这些文档是可读和可执行的代码和数据。它非常适合分享软件教程,并在地理空间 Python 世界中变得非常普遍。
您可以在这里找到 Jupyter Notebooks 和 Python 的良好介绍:jupyter-notebook-beginner-guide.readthedocs.io/en/latest/what_is_jupyter.html。
Conda
Conda 是一个开源的包管理系统,它使得安装和更新复杂的库变得更加容易。它与多种语言兼容,包括 Python。Conda 对于设置库和测试它们非常有用,这样我们就可以在开发环境中尝试新事物。通常,自定义配置生产环境会更好,但 Conda 是原型化新想法的绝佳方式。
您可以从conda.io/en/latest/开始使用 Conda。
摘要
在本章中,我们概述了 Python 特定的地理空间分析工具。许多这些工具都包含了绑定到我们在第三章,“地理空间技术景观”中讨论的库,以提供针对特定操作(如 GDAL 的栅格访问函数)的最佳解决方案。我们还尽可能地包括了纯 Python 库,并且在我们处理即将到来的章节时将继续包括纯 Python 算法。
在下一章中,我们将开始应用所有这些工具进行 GIS 分析。
进一步阅读
以下链接将帮助您进一步探索本章的主题。第一个链接是关于 XPath 查询语言,我们使用 Elementree 来过滤 XML 元素。第二个链接是 Python 字符串库的文档,这在本书中对于操作数据至关重要。第三,我们有lxml库,这是更强大和快速的 XML 库之一。最后,我们有 Conda,它为 Python 中的科学操作提供了一个全面、易于使用的框架,包括地理空间技术:
-
想要获取 XPath 的更多信息,请查看以下链接:
www.w3schools.com/xsl/xpath_intro.asp -
想要了解更多关于 Python
string模块的详细信息,请查看以下链接:docs.python.org/3.4/library/string.html -
LXML 的文档可以在以下链接中找到:
lxml.de/ -
你可以在以下链接中了解更多关于 Conda 的信息:
conda.io/en/latest/
第五章:Python 和地理信息系统
本章将专注于将 Python 应用于通常由地理信息系统(GIS)如 QGIS 或 ArcGIS 执行的功能。这些功能是地理空间分析的核心和灵魂。我们将尽可能减少 Python 本身之外的外部依赖,以便你拥有尽可能可重用的工具,在不同的环境中使用。在本书中,我们从编程的角度将 GIS 分析和遥感分离,这意味着在本章中,我们将主要关注矢量数据。
与本书中的其他章节一样,这里展示的项目是核心功能,它们作为构建块,你可以重新组合来解决本书之外遇到的挑战。本章的主题包括以下内容:
-
测量距离
-
坐标转换
-
重投影矢量数据
-
测量面积
-
编辑 shapefile
-
从更大的数据集中选择数据
-
创建专题地图
-
使用电子表格
-
非 GIS 数据类型的转换
-
地理编码
-
多进程
本章包含许多代码示例。除了文本外,代码注释还包括在示例中的指南。本章覆盖的范围比本书中的任何其他章节都要广。它涵盖了从测量地球到编辑数据、创建地图,到使用扩展的多进程以加快分析速度的各个方面。到本章结束时,你将是一名准备学习本书其余部分更高级技术的地理空间分析师。
技术要求
对于本章,你需要以下内容:
-
Python 3.7
-
Python UTM 库
-
Python OGR 库
-
Python Shapefile 库
-
Python Fiona 库
-
Python PNGCanvas 库
-
Python Pillow 库(Python 图像库)
-
Python Folium 库
-
Python Pymea 库
-
Python Geocoder 库
-
Python GeoPy 库
测量距离
地理空间分析的本质是发现地球上物体的关系。彼此更近的物体往往比彼此更远的物体有更强的关系。这个概念被称为托布勒地理第一定律。因此,测量距离是地理空间分析的一个关键功能。
正如我们所学的,每张地图都是地球的一个模型,它们在某种程度上都是错误的。因此,坐在电脑前测量地球上两点之间的准确距离是不可能的。即使是专业的土地测量员(他们带着传统的观测设备和非常精确的 GPS 设备到野外)也无法考虑到 A 点和 B 点之间地球表面的每一个异常。因此,为了测量距离,我们必须考虑以下问题:
-
我们在测量什么?
-
我们在测量多少?
-
我们需要多少精度?
现在,要计算距离,我们可以使用以下三种地球模型:
-
平面
-
球形
-
椭球体
在平面模型中,使用的是标准的欧几里得几何。地球被视为一个没有曲率的平面,如下面的图所示:

由于在这个模型中你工作的是直线,所以数学变得相当简单。地理坐标最常用的格式是十进制度数。然而,十进制度数坐标是在球体上作为角度的参考测量——经度和本初子午线之间,以及纬度和赤道之间。此外,经线在两极会聚于零。纬线的周长在两极也会变得更小。这些事实意味着十进制度数对于使用无限平面的欧几里得几何来说不是一个有效的坐标系。
地图投影试图简化在二维平面上处理三维椭球体的问题,无论是在纸张上还是在计算机屏幕上。正如我们在第一章中讨论的,《使用 Python 学习地理空间分析》,地图投影将地球的圆形模型展平到平面上,并为了地图的便利性而引入了扭曲。一旦这个投影到位,十进制度数被交换为具有x和y坐标的笛卡尔坐标系,我们就可以使用最简单的欧几里得几何形式——即勾股定理。
在足够大的尺度上,像地球这样的球体或椭球体看起来更像是一个平面而不是一个球体。事实上,几个世纪以来,人们都认为地球是平的!如果经度度的差异足够小,你通常可以使用欧几里得几何,然后将测量值转换为米、千米或英里。这种方法通常不推荐,但最终的决定取决于你作为分析师对精度的要求。
球形模型方法试图通过避免将地球压扁到平面上所产生的问题来更好地逼近现实。正如其名所示,该模型使用一个完美的球体来表示地球(类似于物理地球仪),这使得我们可以直接使用度数。这个模型忽略了地球实际上更像是具有不同厚度地壳的椭圆形,而不是一个完美的球体。但是,通过在球体表面工作距离,我们可以开始以更高的精度测量更长的距离。以下截图说明了这个概念:

使用地球椭球体模型,分析师们努力寻找地球表面的最佳模型。有几个椭球体模型,被称为基准。基准是一组定义地球估计形状的值,也称为大地测量系统。像任何其他地理参考系统一样,基准可以针对局部区域进行优化。最常用的基准是称为 WGS84 的基准,它设计用于全球使用。你应该知道,随着评估技术和技术的改进,WGS84 有时会更新。最近的修订发生在 2004 年。
在北美,NAD83 基准用于优化大陆上的参考。在东半球,更频繁地使用 欧洲大地测量参考系统 1989 (ETRS89)。ETRS89 被固定在 欧亚板块 的稳定部分。基于 ETRS89 的欧洲地图不受大陆漂移的影响,大陆漂移每年变化可达 2.5 厘米,因为地球的地壳在移动。
椭球体从中心到边缘没有恒定的半径。这个事实意味着在地球的球体模型中使用的公式在椭球体模型中开始出现问题。虽然这不是一个完美的近似,但它比球体模型更接近现实。
以下截图显示了一个用黑色线条表示的通用椭球模型,与用红色线条表示的地球不均匀地壳进行了对比。虽然我们不会在这些例子中使用它,但另一个模型是大地水准面模型。大地水准面是地球上最精确和最准确的模型,它基于地球表面,除了重力和旋转外,没有其他影响因素。以下图示展示了大地水准面、椭球体和球体模型,以说明它们之间的差异:

理解这些地球模型对于本书中的其他内容至关重要,因为毕竟我们是在模拟地球。
现在我们已经讨论了这些不同的地球模型以及测量它们的问题,让我们看看一些使用 Python 的解决方案。
使用勾股定理
我们将从最简单的方法开始测量,即勾股定理,也称为欧几里得距离。如果你还记得你从学校学到的几何课程,勾股定理断言以下内容:
a2 + b2 = c2
在这个断言中,变量 a、b 和 c 都是三角形的边。如果你知道另外两边,你可以解出任意一边。
在这个例子中,我们将从密西西比横轴墨卡托(MSTM)投影中的两个投影点开始。这个投影的单位是米。x轴的位置是从该州最西端定义的中央子午线测量的。y轴是从 NAD83 水平基准定义的。第一个点定义为(x1,y1),代表密西西比州的首府杰克逊。第二个点定义为(x2,y2),代表沿海城市比洛克西,如下面的插图所示:

在下面的例子中,Python 中的双星号(**)是指数的语法,我们将用它来平方距离。
我们将导入 Python 的 math 模块,以使用其名为sqrt()的平方根函数。然后,我们将计算x轴和y轴的距离。最后,我们将使用这些变量来执行欧几里得距离公式,以从x,y原点得到边界框的米距离,这将在 MSTM 投影中使用:
import math
# First point
x1 = 456456.23
y1 = 1279721.064
# Second point
x2 = 576628.34
y2 = 1071740.33
# X distance
x_dist = x1 - x2
# Y distance
y_dist = y1 - y2
# Pythagorean theorem
dist_sq = x_dist**2 + y_dist**2
distance = math.sqrt(dist_sq)
print(distance)
# 240202.66
因此,距离大约是 240,202 米,约合 240.2 公里或 150 英里。这个计算相当准确,因为这种投影是为了在密西西比州使用笛卡尔坐标来测量距离和面积而优化的。
我们还可以使用十进制度量距离,但我们必须执行一些额外的步骤。要使用度来测量,我们必须将角度转换为弧度,这考虑了坐标之间的曲面积距。我们还将以弧度为单位乘以地球半径(以米为单位),以将结果转换回弧度。
你可以在en.wikipedia.org/wiki/Radian上了解更多关于弧度的信息。
在以下代码中,我们将使用 Python 的math.radians()方法执行此转换,当我们计算x和y距离时:
import math
x1 = -90.21
y1 = 32.31
x2 = -88.95
y2 = 30.43
x_dist = math.radians(x1 - x2)
y_dist = math.radians(y1 - y2)
dist_sq = x_dist**2 + y_dist**2
dist_rad = math.sqrt(dist_sq)
dist_rad * 6371251.46
# 251664.46
好的,这次我们得到了大约 251 公里,比我们的第一次测量多了 11 公里。所以,正如你所看到的,你选择的测量算法和地球模型可以产生重大影响。使用相同的方程,我们得到了截然不同的答案,这取决于我们选择的坐标系和地球模型。
你可以在mathworld.wolfram.com/Distance.html上了解更多关于欧几里得距离的信息。
让我们来了解一下哈弗辛公式。
使用哈弗辛公式
使用勾股定理在地球(一个球体)上测量距离的问题之一是大圆距离的概念。大圆是球面上两点之间的最短距离。定义大圆的另一个重要特征是,如果沿着圆周完全绕球体一周,该圆将球体平分为两个相等的半球,如下面的维基百科插图所示:

那么,如何正确地测量曲面上的一条线呢?最流行的方法是使用哈弗辛公式,它使用三角学来计算以十进制度数定义的坐标的大圆距离。哈弗辛公式是haversine(θ) = sin²(θ/2),其中θ是球面上两点之间的中心角。再次提醒,在我们应用公式之前,我们将轴距离从度数转换为弧度,就像上一个例子一样。但这次,我们还将纬度(y-轴)坐标单独转换为弧度:
import math
x1 = -90.212452861859035
y1 = 32.316272202663704
x2 = -88.952170968942525
y2 = 30.438559624660321
x_dist = math.radians(x1 - x2)
y_dist = math.radians(y1 - y2)
y1_rad = math.radians(y1)
y2_rad = math.radians(y2)
a = math.sin(y_dist/2)**2 + math.sin(x_dist/2)**2 \
* math.cos(y1_rad) * math.cos(y2_rad)
c = 2 * math.asin(math.sqrt(a))
distance = c * 6371 # kilometers
print(distance)
# 240.63
哇!我们使用哈弗辛公式得到了 240.6 公里,而使用优化且更精确的投影方法得到了 240.2 公里。这个差异不到半公里,对于一个相距 150 英里的两个城市的距离计算来说已经很不错了。哈弗辛公式是最常用的距离测量公式,因为它在编码方面相对简单,并且在大多数情况下相当准确。它被认为精确到大约一米的范围内。
总结到目前为止我们所学的,作为分析师,你遇到的点坐标大多数都是未投影的十进制度数。因此,你的测量选项如下:
-
将其重新投影到距离精确的笛卡尔投影中并测量。
-
只需使用哈弗辛公式,看看它如何帮助你的分析。
-
使用更加精确的文森蒂公式。
对了!还有一个公式试图提供比哈弗辛公式更好的测量结果。
使用文森蒂公式
因此,我们已经检查了使用勾股定理(平坦地球模型)和哈弗辛公式(球形地球模型)进行距离测量的方法。文森蒂公式考虑了地球的椭球体模型。如果你使用的是局部椭球体,它可以精确到小于一米的程度。
在以下公式的实现中,你可以更改半长轴值和扁平率,以适应任何椭球体的定义。让我们看看在以下示例中使用文森蒂公式在 NAD83 椭球体上测量时的距离是多少:
- 首先,我们将导入
math模块,这允许我们在弧度下工作,以及我们需要的其他math函数:
import math
- 现在,我们需要设置我们的变量,包括包含距离值的变量、我们正在测量的两个点、描述地球的常数以及我们需要的一阶导数公式:
distance = None
x1 = -90.212452861859035
y1 = 32.316272202663704
x2 = -88.952170968942525
y2 = 30.438559624660321
# Ellipsoid Parameters
# Example is NAD83
a = 6378137 # semi-major axis
f = 1/298.257222101 # inverse flattening
b = abs((f*a)-a) # semi-minor axis
L = math.radians(x2-x1)
U1 = math.atan((1-f) * math.tan(math.radians(y1)))
U2 = math.atan((1-f) * math.tan(math.radians(y2)))
sinU1 = math.sin(U1)
cosU1 = math.cos(U1)
sinU2 = math.sin(U2)
cosU2 = math.cos(U2)
lam = L
- 现在开始文森蒂公式。没有简单的方法来做这件事,数学有点复杂,但它有效:
for i in range(100):
sinLam = math.sin(lam)
cosLam = math.cos(lam)
sinSigma = math.sqrt((cosU2*sinLam)**2 +
(cosU1*sinU2-sinU1*cosU2*cosLam)**2)
if (sinSigma == 0):
distance = 0 # coincident points
break
cosSigma = sinU1*sinU2 + cosU1*cosU2*cosLam
sigma = math.atan2(sinSigma, cosSigma)
sinAlpha = cosU1 * cosU2 * sinLam / sinSigma
cosSqAlpha = 1 - sinAlpha**2
cos2SigmaM = cosSigma - 2*sinU1*sinU2/cosSqAlpha
if math.isnan(cos2SigmaM):
cos2SigmaM = 0 # equatorial line
C = f/16*cosSqAlpha*(4+f*(4-3*cosSqAlpha))
LP = lam
lam = L + (1-C) * f * sinAlpha *
(sigma + C*sinSigma*(cos2SigmaM+C*cosSigma *
(-1+2*cos2SigmaM*cos2SigmaM)))
if not abs(lam-LP) 1e-12:
break
uSq = cosSqAlpha * (a**2 - b**2) / b**2
A = 1 + uSq/16384*(4096+uSq*(-768+uSq*(320-175*uSq)))
B = uSq/1024 * (256+uSq*(-128+uSq*(74-47*uSq)))
deltaSigma = B*sinSigma*(cos2SigmaM+B/4 *
(cosSigma*(-1+2*cos2SigmaM*cos2SigmaM) - B/6*cos2SigmaM*(-3+4*sinSigma*sinSigma) * (-3+4*cos2SigmaM*cos2SigmaM)))
s = b*A*(sigma-deltaSigma)
最后,在完成所有这些之后,我们得到了我们的距离:
distance = s
print(distance)
# 240237.66693880095
使用文森蒂公式,我们的测量结果是 240.1 公里,这比我们使用欧几里得距离预测的测量结果只差 100 米。令人印象深刻!虽然它比哈弗辛公式在数学上复杂得多,但你也可以看到它更加精确。
纯 Python 的 geopy 模块实现了 Vincenty 公式,并且能够通过将地点名称转换为经纬度坐标来进行地点的地理编码:geopy.readthedocs.org/en/latest/。
在这些示例中使用到的点距离赤道相对较近。当你向两极移动或处理更大的距离或极小的距离时,你所做的选择变得越来越重要。如果你只是尝试围绕一个城市画一个半径来选择营销活动推广音乐会的地点,那么几公里的误差可能是可以接受的。然而,如果你试图估计飞机在两个机场之间飞行所需的燃油量,那么你需要非常精确!
如果你想要了解更多关于测量距离和方向的问题,以及如何通过编程来解决这个问题,请访问以下网站: www.movable-type.co.uk/scripts/latlong.html。
在这个网站上,Chris Veness 对这个主题进行了详细的介绍,并提供了在线计算器,以及用 JavaScript 编写的示例,这些示例可以轻松地移植到 Python 中。我们刚刚看到的 Vincenty 公式实现就是从这个网站上的 JavaScript 移植过来的。
你可以在这里看到 Vincenty 公式的完整纯数学符号: en.wikipedia.org/wiki/Vincenty%27s_formulae。
现在我们已经知道了如何计算距离,我们需要了解如何计算线的方向,以便通过距离和位置将地球上的物体联系起来进行地理空间分析。
计算线方向
除了距离之外,你通常还想知道线段的端点之间的方位角。我们可以使用 Python 的 math 模块仅从其中一个点计算出这条线的方向:
- 首先,我们导入所需的
math函数:
from math import atan2, cos, sin, degrees
- 接下来,我们为我们的两个点设置一些变量:
lon1 = -90.21
lat1 = 32.31
lon2 = -88.95
lat2 = 30.43
- 接下来,我们将计算两点之间的角度:
angle = atan2(cos(lat1)*sin(lat2)-sin(lat1) * \
cos(lat2)*cos(lon2-lon1), sin(lon2-lon1)*cos(lat2))
- 最后,我们将计算线的方位角(以度为单位):
bearing = (degrees(angle) + 360) % 360
print(bearing)
309.3672990606595
有时,你可能会得到一个负的方位角值。为了避免这个问题,我们将 360 添加到结果中,以避免出现负数,并使用 Python 的取模运算符来防止值超过 360。
在角度计算中的 math 是逆向工程一个直角三角形,然后找出三角形的锐角。以下 URL 提供了这个公式的元素解释,以及最后的交互式示例: www.mathsisfun.com/sine-cosine-tangent.html。
我们现在已经知道了如何计算地球上特征的位置。接下来,我们将学习如何整合来自不同来源的数据,从坐标转换开始。
理解坐标转换
坐标转换允许你在不同的坐标系之间转换点坐标。当你开始处理多个数据集时,你不可避免地会得到不同坐标系和投影的数据。你可以使用一个名为utm的纯 Python 模块在两个最常用的坐标系之间进行转换,即 UTM 和地理坐标(纬度和经度)。你可以使用easy_install或从 PyPI 的pip安装它:pypi.python.org/pypi/utm。
utm模块的使用非常简单。要将 UTM 坐标转换为纬度和经度,可以使用以下代码:
import utm
y = 479747.0453210057
x = 5377685.825323031
zone = 32
band = 'U'
print(utm.to_latlon(y, x, zone, band))
# (48.55199390882121, 8.725555729071763)
UTM 区域是水平编号的。然而,垂直方向上,纬度带按英文字母顺序排列,有一些例外。例如,字母A、B、Y和Z用于标记地球的极点。字母I和O被省略,因为它们看起来太像1和0。字母N到X位于北半球,而字母C到M位于南半球。以下截图来自网站Atlas Florae Europaeae,展示了欧洲的 UTM 区域:

从纬度和经度转换甚至更简单。我们只需将纬度和经度传递给from_latlon()方法,该方法返回一个元组,包含与to_latlon()方法接受的相同参数:
import utm
utm.from_latlon(48.55199390882121, 8.725555729071763)
# (479747.04524576373, 5377691.373080335, 32, 'U')
在此 Python 实现中使用的算法在www.uwgb.edu/dutchs/UsefulData/UTMFormulas.HTM上进行了详细描述。
在 UTM 和纬度/经度之间转换只是将来自不同来源的数据集转换以便在地图上很好地叠加的表面工作。要超越基础,我们需要执行地图投影。
现在我们已经知道了如何计算线方向,让我们看看重投影是如何进行的。
理解重投影
在 GIS 中,重投影就是将数据集中的坐标从一个坐标系转换到另一个坐标系。尽管由于数据分布的更先进方法,重投影现在不太常见,但有时你需要重投影一个 shapefile。纯 Python 的utm模块适用于参考系统转换,但对于完整的重投影,我们需要 OGR Python API 的帮助。包含在osgeo模块中的 OGR API 还提供了开放空间参考模块,也称为osr,我们将使用它来进行重投影。
例如,我们将使用一个包含纽约市博物馆和画廊位置的 Lambert 正形投影点 shapefile。我们将将其重投影到 WGS84 地理(或更确切地说,取消投影)。你可以在此处下载此 zip 文件:git.io/vLbT4。
以下是最简化的脚本,用于重新投影 shapefile。几何形状被转换后写入新文件,但.dbf文件只是简单地复制到新名称,因为我们没有改变它。我们使用了标准的 Python shutil模块,简称 shell 工具,用于复制.dbf。源 shapefile 名称和目标 shapefile 名称在脚本的开头作为变量。目标投影也接近顶部,它使用 EPSG 代码设置。该脚本假设存在一个.prj投影文件,它定义了源投影。如果没有,你可以使用与目标投影相同的语法手动定义它。我们将逐步讲解投影数据集的过程。每个部分都有注释:
- 首先,我们导入我们的库:
from osgeo import ogr
from osgeo import osr
import os
import shutil
- 接下来,我们将 shapefile 名称定义为变量:
srcName = 'NYC_MUSEUMS_LAMBERT.shp'
tgtName = 'NYC_MUSEUMS_GEO.shp'
- 现在,我们使用
osr模块和 EPSG 代码4326创建我们的目标空间参考,它是 WGS84 地理坐标:
tgt_spatRef = osr.SpatialReference()
tgt_spatRef.ImportFromEPSG(4326)
- 然后,我们使用
ogr设置我们的 shapefileReader对象并获取空间参考:
driver = ogr.GetDriverByName('ESRI Shapefile')
src = driver.Open(srcName, 0)
srcLyr = src.GetLayer()
src_spatRef = srcLyr.GetSpatialRef()
- 接下来,我们检查目标 shapefile 是否已从之前的测试运行中存在,如果存在则删除它:
if os.path.exists(tgtName):
driver.DeleteDataSource(tgtName)
- 现在,我们可以开始构建我们的目标图层:
tgt = driver.CreateDataSource(tgtName)
lyrName = os.path.splitext(tgtName)[0]
# Use well-known binary format (WKB) to specify geometry
tgtLyr = tgt.CreateLayer(lyrName, geom_type=ogr.wkbPoint)
featDef = srcLyr.GetLayerDefn()
trans = osr.CoordinateTransformation(src_spatRef, tgt_spatRef)
- 接下来,我们可以遍历源 shapefile 中的要素,使用
Transform()方法重新投影它们,并将它们添加到新 shapefile 中:
srcFeat = srcLyr.GetNextFeature()
while srcFeat:
geom = srcFeat.GetGeometryRef()
geom.Transform(trans)
feature = ogr.Feature(featDef)
feature.SetGeometry(geom)
tgtLyr.CreateFeature(feature)
feature.Destroy()
srcFeat.Destroy()
srcFeat = srcLyr.GetNextFeature()
src.Destroy()
tgt.Destroy()
- 然后,我们需要创建一个包含投影信息的 shapefile
.prj文件,因为 shapefile 没有固有的方式来存储它:
# Convert geometry to Esri flavor of Well-Known Text (WKT) format
# for export to the projection (prj) file.
tgt_spatRef.MorphToESRI()
prj = open(lyrName + '.prj', 'w')
prj.write(tgt_spatRef.ExportToWkt())
prj.close()
- 最后,我们只需将
.dbf源复制到新文件名,因为属性是重投影过程的一部分:
srcDbf = os.path.splitext(srcName)[0] + '.dbf'
tgtDbf = lyrName + '.dbf'
shutil.copyfile(srcDbf, tgtDbf)
以下截图显示了 QGIS 中重新投影的点,背景为卫星影像:

如果你正在处理一组点,你可以通过编程方式重新投影它们,而不是使用 PyProj 重新投影 shapefile:jswhit.github.io/pyproj/。
除了将坐标转换为不同的投影外,你通常还需要在不同格式之间进行转换,我们将在下一部分进行探讨。
理解坐标格式转换
地图坐标传统上以度、分、秒(DMS)的形式表示,用于海上导航。然而,在 GIS(基于计算机的)中,纬度和经度以称为十进制度数的十进制数表示。度、分、秒格式仍然在使用。有时,你必须在这两种格式之间进行转换,以执行计算和输出报告。
在这个例子中,我们将创建两个函数,可以将一种格式转换为另一种格式:
- 首先,我们导入
math模块进行转换和re正则表达式模块解析坐标字符串:
import math
import re
- 我们有一个函数可以将十进制度数转换为
度、分和秒字符串:
def dd2dms(lat, lon):
"""Convert decimal degrees to degrees, minutes, seconds"""
latf, latn = math.modf(lat)
lonf, lonn = math.modf(lon)
latd = int(latn)
latm = int(latf * 60)
lats = (lat - latd - latm / 60) * 3600.00
lond = int(lonn)
lonm = int(lonf * 60)
lons = (lon - lond - lonm / 60) * 3600.00
compass = {
'lat': ('N','S'),
'lon': ('E','W')
}
lat_compass = compass['lat'][0 if latd >= 0 else 1]
lon_compass = compass['lon'][0 if lond >= 0 else 1]
return '{}º {}\' {:.2f}" {}, {}º {}\' {:.2f}"
{}'.format(abs(latd),
abs(latm), abs(lats), lat_compass, abs(lond),
abs(lonm), abs(lons), lon_compass)
- 接下来,我们有一个函数用于反向转换度数:
def dms2dd(lat, lon):
lat_deg, lat_min, \
lat_sec, lat_dir = re.split('[^\d\.A-Z]+', lat)
lon_deg, lon_min, \
lon_sec, lon_dir = re.split('[^\d\.A-Z]+', lon)
lat_dd = float(lat_deg) +\
float(lat_min)/60 + float(lat_sec)/(60*60);
lon_dd = float(lon_deg) +\
float(lon_min)/60 + float(lon_sec)/(60*60);
if lat_dir == 'S':
lat_dd *= -1
if lon_dir == 'W':
lon_dd *= -1
return (lat_dd, lon_dd);
- 现在,如果我们想将十进制度数转换为 DMS,就像使用以下代码一样简单:
print(dd2dms(35.14953, -90.04898))
# 35º 8' 58.31" N, 90º 2' 56.33" W
- 要进行相反的操作,你只需输入以下函数:
dms2dd("""29º 56' 0.00" N""", """90º 4' 12.36" W""")
(29.933333333333334, -90.0701)
注意,由于 DMS 坐标包含单引号和双引号来表示分钟和秒,我们必须使用 Python 字符串约定,在每个纬度和经度坐标上使用三引号来包含这两种引号,以便它们被正确解析。
坐标是 GIS 数据集的基本单位。它们用于构建点、线和多边形。
计算多边形的面积
在我们继续编辑 GIS 数据之前,我们还有一个计算要做。GIS 的最基本单位是一个点。两个点可以形成一条线。共享端点的多条线可以形成多段线,多段线可以形成多边形。多边形用于在地理空间操作中表示从一栋房子到整个国家的一切。
计算多边形的面积是 GIS 中最有用的操作之一,如果我们想了解特征的相对大小。但在 GIS 中,面积计算不仅限于基本几何。多边形位于地球表面,这是一个曲面的表面。多边形必须进行投影以考虑这种曲率。
幸运的是,有一个纯 Python 模块,简单地称为area,为我们处理这些复杂问题。由于它是纯 Python,你可以查看源代码来了解它是如何工作的。area模块的area()函数接受一个 GeoJSON 字符串,其中包含构成多边形的点列表,然后返回面积。以下步骤将展示如何计算多边形的面积:
- 你可以使用
pip安装area模块:
pip install area
- 首先,我们将从
area模块导入area函数:
from area import area
- 接下来,我们将创建一个名为
polygon的变量,它包含在 GeoJSON 几何形状中,用于我们的多边形:
# Our points making up a polygon
polygon = {"type":"Polygon","coordinates":[[[-89.324,30.312],[-89.326,30.31],[-89.322,30.31],[-89.321,30.311],[-89.321,30.312],[-89.324,30.312]]]}
- 现在,我们可以将多边形点字符串传递给面积函数来计算面积:
a = area(polygon)
- 返回的面积是
80235.13927976067平方米。然后我们可以使用 Python 的内置round()函数将长浮点值四舍五入到两位小数,得到80235.14:
round(a, 2)
你现在有了进行地理空间数据距离和尺寸计算的数学工具。
在下一节中,我们将查看编辑最流行的 GIS 数据格式之一——shapefiles 中的数据集。
编辑 shapefiles
Shapefiles 是 GIS 中最常见的数据格式之一,无论是用于数据交换还是进行 GIS 分析。在本节中,我们将学习如何广泛地处理这些文件。在第二章《学习地理空间数据》中,我们讨论了 shapefiles 作为一种可以与其关联许多不同文件类型的格式。对于编辑 shapefiles 以及大多数其他操作,我们只关心两种文件类型:
-
.shp文件 -
.dbf文件
.shp文件包含几何信息,而.dbf文件包含相应几何体的属性。在 shapefile 中的每个几何记录都有一个.dbf记录。记录没有编号或以任何方式标识。这意味着,当从 shapefile 中添加和删除信息时,你必须小心地删除或添加每个文件类型的记录以匹配。
如我们在第四章中讨论的,地理空间 Python 工具箱,我们可以使用以下两个库在 Python 中编辑 shapefile:
-
一个是 OGR 库的 Python 绑定。
-
另一个是 PyShp 库,它完全是用 Python 编写的。
我们将使用 PyShp 来保持本书尽可能使用纯 Python的主题。要安装 PyShp,请使用easy_install或pip。
要开始编辑 shapefile,我们将从一个包含密西西比州城市的点 shapefile 开始,你可以将其作为 ZIP 文件下载。将以下文件下载到你的工作目录并解压它:git.io/vLbU4。
我们正在处理的点可以在以下插图看到:

访问 shapefile
要对 shapefile 进行任何操作,我们需要将其作为数据源访问。要访问 shapefile,我们将使用 PyShp 打开它。在 PyShp 中,我们将添加以下代码:
import shapefile
r = shapefile.Reader('MSCities_Geo_Pts')
r
<shapefile.Reader instance at 0x00BCB760>
我们创建了一个 shapefile Reader对象实例并将其设置为r变量。请注意,当我们向Reader类传递文件名时,我们没有使用任何文件扩展名。记住,我们至少在处理两个以.shp和.dbf结尾的不同文件。因此,这些两个文件共有的基本文件名是我们真正需要的所有。
然而,你可以使用文件扩展名。PyShp 将忽略它并使用基本文件名。那么,为什么你要添加扩展名呢?大多数操作系统允许文件名中有任意数量的点号。例如,你可能有一个以下基本名称的 shapefile:myShapefile.version.1.2。
在这种情况下,PyShp 将尝试解释最后一个点号之后的字符作为文件扩展名,这将导致.2。这个问题将阻止你打开 shapefile。所以,如果你的 shapefile 在基本名称中有点,你需要在文件名中添加文件扩展名,如.shp或.dbf。
一旦你打开了 shapefile 并创建了一个Reader对象,你可以从Reader对象获取一些关于地理数据的信息。在以下示例中,我们将从我们的Reader对象获取 shapefile 的边界框、形状类型和记录数:
r.bbox
[-91.38804855553174, 30.29314882296931, -88.18631833931401,
34.96091138678437]
r.shapeType
# 1
r.numRecords
# 298
包含最小 x 值、最小 y 值、最大 x 值和最大 y 值的边界框存储在 r.bbox 属性中,并以列表形式返回。形状类型作为 shapeType 属性可用,是由官方形状文件规范定义的数字代码。在这种情况下,1 代表点形状文件,3 代表线,5 代表多边形。最后,numRecords 属性告诉我们这个形状文件中有 298 条记录。因为它是一个简单的点形状文件,我们知道有 298 个点,每个点都有自己的 .dbf 记录。
下表显示了形状文件的不同几何类型及其对应的数字代码:
| 几何类型 | 数字代码 |
|---|---|
NULL |
0 |
POINT |
1 |
POLYLINE |
3 |
POLYGON |
5 |
MULTIPOINT |
8 |
POINTZ |
11 |
POLYLINEZ |
13 |
POLYGONZ |
15 |
MULTIPOINTZ |
18 |
POINTM |
21 |
POLYLINEM |
23 |
POLYGONM |
25 |
MULTIPOINTM |
28 |
MULTIPATCH |
31 |
现在我们知道了如何访问它,让我们看看我们如何读取这些文件。
读取形状文件属性
.dbf 文件是一种简单的数据库格式,其结构类似于具有行和列的电子表格,其中每一列都有一个标签,定义了它包含的信息。我们可以通过检查 Reader 对象的 fields 属性来查看这些信息:
r.fields
# [('DeletionFlag', 'C', 1, 0), ['STATEFP10', 'C', 2, 0],
['PLACEFP10', 'C', 5, 0],
# ['PLACENS10', 'C', 8, 0], ['GEOID10', 'C', 7, 0], ['NAME10', 'C',
100, 0],
# ['NAMELSAD10', 'C', 100, 0], ['LSAD10', 'C', 2, 0], ['CLASSFP10',
'C', 2, 0],
# ['PCICBSA10', 'C', 1, 0], ['PCINECTA10', 'C', 1, 0], ['MTFCC10',
'C', 5, 0],
# ['FUNCSTAT10', 'C', 1, 0], ['ALAND10', 'N', 14, 0], ['AWATER10',
'N', 14,0],
# ['INTPTLAT10', 'C', 11, 0], ['INTPTLON10', 'C', 12, 0]]
fields 属性返回了相当多的信息。字段包含有关每个字段的信息列表,称为 字段描述符。对于每个字段,以下信息被展示:
-
字段名称:这是字段作为文本的名称,对于形状文件而言,其长度不能超过 10 个字符。
-
字段类型:这是字段的类型,可以是文本、数字、日期、浮点数或布尔值,分别表示为 C、N、D、F 和 L。形状文件规范说明它使用 dBASE III 规范的
.dbf格式,但大多数 GIS 软件似乎支持 dBASE IV。在版本 IV(4)中,数字和浮点数类型是等效的。 -
字段长度:这是数据中的字符或数字长度。
-
十进制长度:这是数字或浮点字段中的小数位数。
第一个字段描述符概述了一个隐藏的字段,它是 .dbf 文件格式规范的一部分。DeletionFlag 允许软件标记要删除的记录,而实际上并不删除它们。这样,信息仍然在文件中,但可以从显示的记录列表或搜索查询中删除。
如果我们只想获取字段名称而不是其他元数据,我们可以使用 Python 列推导式来仅返回描述符中的第一个项目(字段名称),并忽略 DeletionFlag 字段。以下示例创建了一个列推导式,它返回每个描述符中的第一个项目(字段名称),从第二个描述符开始忽略删除标志:
[item[0] for item in r.fields[1:]]
# ['STATEFP10', 'PLACEFP10', 'PLACENS10', 'GEOID10', 'NAME10', 'NAMELSAD10', 'LSAD10',
# 'CLASSFP10', 'PCICBSA10', 'PCINECTA10', 'MTFCC10', 'FUNCSTAT10', 'ALAND10',
# 'AWATER10', 'INTPTLAT10', 'INTPTLON10']
现在,我们只有字段名,这要容易阅读得多。为了清晰起见,字段名都包含数字 10,因为这是该 shapefile 的 2010 年版本,它是每个普查的一部分。这类缩写由于字段名长度限制为 10 个字符而在 shapefile .dbf 文件中很常见。
接下来,让我们检查这些字段描述的一些记录。我们可以使用 r.record() 方法查看单个记录。我们知道从第一个例子中,有 298 条记录。所以,让我们以第三条记录为例。记录是通过列表索引访问的。在 Python 中,索引从 0 开始,所以我们必须从所需的记录号中减去 1 以获得索引。对于记录 3,索引将是 2。您只需将索引传递给 record() 方法,如下面的代码所示:
r.record(2)
#['28', '16620', '02406337', '2816620', 'Crosby', 'Crosby town', '43', 'C1', 'N','N', # 'G4110', 'A', 5489412, 21336, '+31.2742552', '-091.0614840']
如您所见,字段名是独立于实际记录存储的。如果您想选择一个记录值,您需要它的索引。每个记录中城市名称的索引是 4:
r.record(2)[4]
# 'Crosby'
但是计数索引是枯燥的。通过字段名引用值要容易得多。我们可以用几种方法将字段名与特定记录的值关联起来。第一种方法是使用 Python 列表的 index() 方法,通过字段名来程序化地获取索引:
fieldNames = [item[0] for item in r.fields[1:]]
name10 = fieldNames.index('NAME10')
name10
# 4
r.record(2)[name10]
# 'Crosby'
我们还可以通过使用 Python 的内置 zip() 方法将字段名与值关联起来,该方法将两个或多个列表中的对应项匹配并合并到一个元组列表中。然后,我们可以遍历该列表,检查名称,然后获取相关的值,如下面的代码所示:
fieldNames = [item[0] for item in r.fields[1:]]
fieldNames
# ['STATEFP10', 'PLACEFP10', 'PLACENS10', 'GEOID10', 'NAME10', 'NAMELSAD10',
# 'LSAD10', 'CLASSFP10', 'PCICBSA10', 'PCINECTA10', 'MTFCC10','FUNCSTAT10',
# 'ALAND10','AWATER10', 'INTPTLAT10', 'INTPTLON10']
rec = r.record(2)
rec
# ['28', '16620', '02406337', '2816620', 'Crosby', 'Crosby town',
# '43', 'C1', 'N','N', 'G4110', 'A', 5489412, 21336, '+31.2742552', '-091.0614840']
zipRec = zip(fieldNames, rec)
list(zipRec)
# [('STATEFP10', '28'), ('PLACEFP10', '16620'), ('PLACENS10', '02406337'),
# ('GEOID10', '2816620'), ('NAME10', 'Crosby'), ('NAMELSAD10', 'Crosby town'),
# ('LSAD10', '43'), ('CLASSFP10', 'C1'), ('PCICBSA10','N'),('PCINECTA10','N'),
# ('MTFCC10', 'G4110'), ('FUNCSTAT10', 'A'), ('ALAND10', 5489412),('AWATER10', 21336),
# ('INTPTLAT10', '+31.2742552'), ('INTPTLON10', '-091.0614840')]
for z in zipRec:
if z[0] == 'NAME10': print(z[1])
# Crosby
我们还可以使用 r.records() 方法遍历 .dbf 记录。在这个例子中,我们将通过 Python 数组切片限制 records() 方法返回的列表的结果,只遍历前三个记录。正如我们之前提到的,shapefiles 不包含记录号,因此我们还将枚举记录列表,并动态创建记录号,以便输出更易于阅读。在这个例子中,我们将使用 enumerate() 方法,它将返回包含索引和记录的元组,如下面的代码所示:
for rec in enumerate(r.records()[:3]):
print(rec[0]+1, ': ', rec[1])
# 1 : ['28', '59560', '02404554', '2859560', 'Port Gibson', 'Port Gibson city', '
# 25', 'C1', 'N', 'N', 'G4110', 'A', 4550230, 0, '+31.9558031', '-090.9834329']
# 2 : ['28', '50440', '02404351', '2850440', 'Natchez', 'Natchez city', '25', 'C1',
# 'Y', 'N', 'G4110', 'A', 34175943, 1691489, '+31.5495016', '-091.3887298']
# 3 : ['28', '16620', '02406337', '2816620', 'Crosby', 'Crosby town', '43', 'C1','N',
# 'N', 'G4110', 'A', 5489412, 21336, '+31.2742552', '-091.0614840']
这种枚举技巧是大多数 GIS 软件包在表格中显示记录时使用的。许多 GIS 分析师认为 shapefiles 存储记录号,因为每个 GIS 程序都会显示一个。但是,如果您删除了一个记录,例如在 ArcGIS 或 QGIS 中删除记录号 5,并保存文件,当您再次打开它时,您会发现之前记录号 6 的现在变成了记录 5。一些空间数据库可能为记录分配一个唯一的标识符。通常,唯一的标识符很有帮助。您始终可以在 .dbf 中创建另一个字段和列,并分配您自己的数字,即使记录被删除,这个数字也会保持不变。
如果你正在处理非常大的 shapefile,PyShp 有迭代方法可以更有效地访问数据。默认的records()方法一次将所有记录读入 RAM,这对于小的.dbf文件来说是可行的,但即使只有几千条记录,管理起来也变得困难。每次你使用records()方法时,你也可以同样使用r.iterRecords()方法。这种方法只保留提供当前记录所需的最小信息量,而不是整个数据集。在这个快速示例中,我们使用iterRecords()方法来计算记录数,以验证文件头部中的计数:
counter = 0
for rec in r.iterRecords():
counter += 1
counter
# 298
现在我们已经可以读取 shapefile 的一半,即属性,我们准备查看另一半,即几何形状。
读取 shapefile 几何形状
现在,让我们看看几何形状。之前,我们查看了一些头部信息,并确定这个 shapefile 是一个点 shapefile。所以,我们知道每个记录包含一个单独的点。让我们检查第一个几何形状记录:
geom = r.shape(0)
geom.points
# [[-90.98343326763826, 31.9558035947602]]
在每个几何形状记录中,也称为shape,即使只有一个点,如本例所示,点也存储在名为points的列表中。点以x,y对的形式存储,因此如果使用该坐标系,经度在纬度之前。
shapefile 规范还允许 3D 形状。高程值沿着z-轴,通常称为z值。所以,一个 3D 点通常描述为x,y,z。在 shapefile 格式中,如果形状类型允许,z值存储在单独的z属性中。如果形状类型不允许z值,那么在 PyShp 读取记录时,该属性永远不会设置。带有z值的 shapefile 还包含度量值或m值,这些值很少使用,且在本示例中未使用。
一个度量是一个用户分配的值,可能与一个形状相关联。一个例子就是在特定位置记录的温度。还有另一类形状类型,允许为每个形状添加m值,但不能添加z值。这类形状类型被称为M 形状类型。就像z值一样,如果数据存在,则创建m属性;否则,则不会创建。你通常不会遇到带有z值的 shapefile,也很少会遇到带有m值设置的 shapefile。但有时你会遇到,所以了解它们是好的。就像我们的字段和记录.dbf示例一样,如果你不喜欢将z和m值存储在单独的列表中,你可以从点列表中使用zip()方法将它们合并。zip方法可以接受多个列表作为参数,这些参数由逗号分隔,正如我们在之前循环记录时演示的那样,将字段名称和属性连接起来。
当您使用 PyShp 创建Reader对象时,它是只读的。您可以在Reader对象中更改任何值,但它们不会被写入原始 shapefile。在下一个子节中,我们将看到我们如何在原始 shapefile 中进行更改。
修改 shapefile
要创建一个 shapefile,您还需要创建一个Writer对象。您可以在Reader或Writer对象中更改值;它们只是动态的 Python 数据类型。但您必须在某个时候,将Reader中的值复制到Writer中。PyShp 自动处理所有标题信息,例如边界框和记录计数。您只需关注几何和属性。您会发现这种方法比我们之前使用的 OGR 示例要简单得多。然而,它也仅限于 UTM 投影。
为了演示这个概念,我们将读取一个包含以度为单位单位的点的 shapefile,并将其转换为Writer对象中的 UTM 参考系统,然后保存它。我们将使用 PyShp 和本章之前讨论的 UTM 模块。我们将使用的 shapefile 是纽约市博物馆的 shapefile,我们将其重投影到 WGS84 地理坐标系。您也可以直接下载 ZIP 文件,该文件可在git.io/vLd8Y找到。
在以下示例中,我们将读取 shapefile,为转换后的 shapefile 创建一个 writer,复制字段,然后是记录,最后在保存转换后的 shapefile 之前将每个点转换为几何记录。以下是代码:
import shapefile
import utm
r = shapefile.Reader('NYC_MUSEUMS_GEO')
w = shapefile.Writer(r.shapeType)
w.fields = list(r.fields)
w.records.extend(r.records())
for s in r.iterShapes():
lon,lat = s.points[0]
y,x,zone,band = utm.from_latlon(lat,lon)
w.point(x,y)
w.save('NYC_MUSEUMS_UTM')
如果您要打印第一个 shape 的第一个点,您将看到以下内容:
print(w.shapes()[0].points[0])
# [4506346.393408813, 583315.4566450359, 0, 0]
点以包含四个数字的列表形式返回。前两个是x和y值,而最后两个是占位符,在这种情况下分别用于高程和测量值,这些值在写入这些类型的 shapefile 时使用。此外,我们没有编写 PRJ 投影文件,就像我们在先前的重投影示例中所做的那样。以下是一个使用spatialreference.org/中的 EPSG 代码创建 PRJ 文件的方法。先前的示例中的zone变量告诉我们我们正在使用 UTM Zone 18,即 EPSG 代码 26918。以下代码将创建一个prj文件:
from urllib.request import urlopen
prj = urlopen('http://spatialreference.org/ref/epsg/26918/esriwkt/')
with open('NYC\_MUSEUMS\_UTM', 'w') as f:
f.write(str(prj.read()))
作为另一个示例,我们可以在 shapefile 中添加一个新功能。在这个例子中,我们将向表示热带风暴的 shapefile 中添加第二个多边形。您可以在此处下载此示例的压缩 shapefile:git.io/vLdlA。
我们将读取 shapefile,将其复制到Writer对象中,添加新的多边形,并使用以下代码以相同的文件名将其写回:
import shapefile
file_name = "ep202009.026_5day_pgn.shp"
r = shapefile.Reader(file_name)
with shapefile.Writer("test", r.shapeType) as w:
w.fields = list(r.fields)
for rec in r.records():
w.record(*list(rec))
for s in r.shapes():
w._shapeparts(parts=[s.points], shapeType=s.shapeType)
w.poly([[[-104, 24], [-104, 25], [-103, 25], [-103, 24], [-104,
24]]])
w.record("STANLEY", "TD", "091022/1500", "27", "21", "48", "ep")
这就是我们如何在原始 shapefile 中进行更改的方法。现在,让我们看看我们如何在 shapefile 中添加新字段。
添加字段
在形状文件上执行的一个非常常见的操作是向它们添加额外的字段。这个操作很简单,但有一个重要的元素需要记住。当你添加一个字段时,你也必须遍历记录并为该列创建一个空单元格或添加一个值。作为一个例子,让我们向纽约市博物馆的 UTM 版本形状文件添加一个参考纬度和经度列:
- 首先,我们将打开形状文件并创建一个新的
Writer对象:
import shapefile
r = shapefile.Reader('NYC_MUSEUMS_UTM')
with shapefile.Writer("test", r.shapeType) as w:
- 接下来,我们将字段作为长度为
8的浮点类型添加,整个字段的精度最高为5位小数:
w.fields = list(r.fields)
w.field('LAT','F',8,5)
w.field('LON','F',8,5)
- 接下来,我们将打开地理版本的形状文件并从每个记录中获取坐标。我们将这些坐标添加到 UTM 版本
.dbf中的相应属性记录中:
for i in range(len(r.shapes())):
lon, lat = r.shape(i).points[0]
w.point(lon, lat)
w.record(*list(r.record(i)), lat, lon)
在下一个子节中,我们将看到如何合并多个形状文件。
合并形状文件
将多个相同类型的相关形状文件聚合到一个更大的形状文件中是另一种非常有用的技术。你可能是在一个团队中工作,该团队将感兴趣的区域划分成几个部分,然后在一天结束时组装数据。或者,你可能从一系列现场传感器(如气象站)中聚合数据。
对于这个示例,我们将使用一套县建筑足迹,该县分别维护在四个不同的象限(西北、东北、西南和东南)。你可以将这些形状文件作为一个单独的 ZIP 文件下载,链接为git.io/vLbUE。
当你解压这些文件时,你会看到它们按象限命名。以下脚本使用 PyShp 将它们合并成一个形状文件:
import glob
import shapefile
files = glob.glob('footprints_*shp')
with shapefile.Writer("Merged") as w:
r = None
for f in files:
r = shapefile.Reader(f)
if not w.fields:
w.fields = list(r.fields)
for rec in r.records():
w.record(*list(rec))
for s in r.shapes():
w._shapeparts(parts=[s.points], shapeType=s.shapeType)
如你所见,合并一系列形状文件非常直接。然而,我们没有进行任何合理性检查以确保所有形状文件都是同一类型,如果你打算将此脚本用于重复的自动化过程,而不是仅仅是一次性过程,你可能需要这样做。
关于这个示例的另一个注意事项是我们如何调用Writer对象。在其他示例中,我们使用一个数字代码来定义形状类型。你可以直接定义那个数字(例如,对于点形状文件为 1)或调用 PyShp 的一个常量。常量是大写字母表示的形状文件类型。例如,多边形如下所示:
shapefile.POLYGON
在这个情况下,该常数的值是 5。当从Reader对象复制数据到Writer对象时,你会注意到形状类型定义只是简单地引用,如下面的示例所示:
r = shapefile.Reader('myShape')
w = shapefile.Writer("myShape", r.shapeType)
这种方法使你的脚本更加健壮,因为如果以后更改脚本或数据集,脚本中有一个更少的变量需要更改。在合并示例中,当我们调用Writer时,我们没有Reader对象可用的好处。
我们可以打开列表中的第一个 shapefile 并检查其类型,但这会增加几行代码。更简单的方法是省略形状类型。如果Writer的形状类型没有保存,PyShp 将忽略它,直到你保存 shapefile。那时,它将检查单个几何记录的标题并据此确定。
虽然你可以在特殊情况下使用这种方法,但当你能明确定义形状类型时,为了清晰和确保安全,防止任何异常情况错误,最好明确定义形状类型。以下插图是此数据集的样本,以便你更好地了解数据的外观,因为我们将在接下来的使用中更多地使用它:

现在,让我们看看如何使用.dbfpy文件来完成这个操作。
使用 dbfpy 合并 shapefiles
PyShp 的.dbf部分有时会遇到与某些软件生成的.dbf文件相关的问题。幸运的是,PyShp 允许你分别操作不同的 shapefile 类型。有一个更健壮的.dbf库,名为dbfpy3,我们在第四章“地理空间 Python 工具箱”中讨论过。你可以使用 PyShp 来处理.shp和.shx文件,而.dbfpy处理更复杂的.dbf文件。你可以从这里下载模块:github.com/GeospatialPython/dbfpy3/archive/master.zip。
这种方法需要更多的代码,但它通常会在 PyShp 单独处理.dbf问题时失败的地方成功。本例使用与上一个示例相同的 shapefiles。在下面的示例中,我们将仅使用其属性合并一个 shapefile:
- 首先,我们导入所需的库,使用 glob 模块获取 shapefiles 列表,并使用 PyShp 创建一个 shapefile
Writer对象:
import glob
import shapefile
from dbfpy3 import dbf
shp_files = glob.glob('footprints_*.shp')
w = shapefile.Writer(shp="merged.shp", shx="merged.shx")
- 现在,我们将只打开
.shp文件并复制几何数据到 writer。稍后我们将使用dbfpy3模块来获取属性,以演示分别处理 shapefile 组件:
# Loop through ONLY the shp files and copy their shapes
# to a Writer object. We avoid opening the dbf files
# to prevent any field-parsing errors.
for f in shp_files:
print("Shp: {}".format(f))
r = shapefile.Reader(f)
r = shapefile.Reader(shp=shpf)
for s in r.shapes():
w.poly([s.points])
print("Num. shapes: {}".format(len(w.shapes())))
- 一旦所有几何数据都复制到 writer 中,我们就可以保存
.shp文件,并让 PyShp 为几何创建索引文件:
# Save only the shp and shx index file to the new
# merged shapefile.
w.close()
- 接下来,我们可以使用
glob模块获取.dbf文件列表:
# Now we come back with dbfpy and merge the dbf files
dbf\_files = glob.glob('\*.dbf')
- 接下来,我们将使用列表中的第一个
.dbf文件作为模板来获取字段数据,并使用它来设置 shapefile writer 的属性:
# Use the first dbf file as a template
template = dbf\_files.pop(0)
merged\_dbf\_name = 'merged.dbf'
# Copy the entire template dbf file to the merged file
merged\_dbf = open(merged\_dbf\_name, 'wb')
temp = open(template, 'rb')
merged\_dbf.write(temp.read())
merged\_dbf.close()
temp.close()
- 然后,我们简单地遍历
.dbf文件并将记录复制到Writer:中
# Now read each record from the remaining dbf files
# and use the contents to create a new record in
# the merged dbf file.
db = dbf.Dbf(merged\_dbf\_name)
for f in dbf\_files:
print('Dbf: {}'.format(f))
dba = dbf.Dbf(f)
for rec in dba:
db\_rec = db.newRecord()
for k, v in list(rec.asDict().items()):
db\_rec[k] = v
db\_rec.store()
db.close()
现在我们知道了如何合并 shapefiles,让我们来看看如何分割它们。
分割 shapefiles
有时,你可能还需要分割较大的 shapefiles,以便更容易地关注感兴趣的子集。这种分割,或子集化,可以是空间上的,也可以是按属性进行的,具体取决于对数据的哪个方面感兴趣。
空间子集化
提取数据集的一部分的一种方法是通过使用空间属性,如大小。在以下示例中,我们将子集我们合并的东南象限文件。我们将通过面积过滤建筑足迹多边形,并将面积不超过 100 平方米(约 1,000 平方英尺)的任何建筑导出到新的 shapefile。我们将使用footpints_se shapefile 进行此操作。
PyShp 有一个接受坐标列表并返回正或负面积的签名面积方法。我们将使用utm模块将坐标转换为米。通常,正或负面积表示多边形点的顺序是顺时针还是逆时针,分别。但在这里,点的顺序并不重要,所以我们将使用abs()函数的绝对值,如下所示,当我们获取面积值时:
import shapefile
import utm
r = shapefile.Reader('footprints\_se')
w = shapefile.Writer(r.shapeType)
w.fields = list(r.fields)
for sr in r.shapeRecords():
utmPoints = []
for p in sr.shape.points:
x,y,band,zone = utm.from_latlon(p[1],p[0])
utmPoints.append([x,y])
area = abs(shapefile.signed_area(utmPoints))
if area <= 100:
w._shapes.append(sr.shape)
w.records.append(sr.record)
w.save('footprints\_185')
让我们看看原始 shapefile 和子集 shapefile 之间记录数量的差异:
r = shapefile.Reader('footprints\_se')
subset = shapefile.Reader('footprints\_185')
print(r.numRecords)
# 26447
print(subset.numRecords)
# 13331
现在我们有一些实质性的构建块,用于使用矢量数据和属性进行地理空间分析。
执行选择
之前的子集示例是选择数据的一种方法。还有许多其他方法可以用于进一步分析的数据子集。在本节中,我们将检查选择对高效数据处理至关重要的数据子集,以将大型数据集的大小减少到给定数据集的兴趣区域。
点在多边形中的公式
我们在第一章中简要讨论了点在多边形中的公式,使用 Python 学习地理空间分析,作为常见类型的地理空间操作。你会发现它是最有用的公式之一。该公式相对简单。
以下函数使用光线投射方法执行此检查。该方法从测试点绘制一条线穿过多边形,并计算它穿过多边形边界的次数。如果计数为偶数,则点位于多边形外部。如果为奇数,则它在内部。这种特定的实现还检查该点是否位于多边形的边缘:
def point_in_poly(x,y,poly):
# check if point is a vertex
if (x,y) in poly: return True
# check if point is on a boundary
for i in range(len(poly)):
p1 = None
p2 = None
if i==0:
p1 = poly[0]
p2 = poly[1]
else:
p1 = poly[i-1]
p2 = poly[i]
if p1[1] == p2[1] and p1[1] == y and x min(p1[0], \
p2[0]) and x < max(p1[0], p2[0]):
return True
n = len(poly)
inside = False
p1x,p1y = poly[0]
for i in range(n+1):
p2x,p2y = poly[i % n]
if y min(p1y,p2y):
if y <= max(p1y,p2y):
if x <= max(p1x,p2x):
if p1y != p2y:
xints = (y-p1y)*(p2x-p1x)/(p2y-p1y)+p1x
if p1x == p2x or x <= xints:
inside = not inside
p1x,p1y = p2x,p2y
if inside: return True
return False
现在,让我们使用point_in_poly()函数测试智利的点:
# Test a point for inclusion
myPolygon = [(-70.593016,-33.416032), (-70.589604,-33.415370),
(-70.589046,-33.417340), (-70.592351,-33.417949),
(-70.593016,-33.416032)]
# Point to test
lon = -70.592000
lat = -33.416000
print(point_in_poly(lon, lat, myPolygon))
# True
这表明点位于内部。让我们也验证边缘点将被检测到:
# test an edge point
lon = -70.593016
lat = -33.416032
print(point_in_poly(lon, lat, myPolygon))
# True
你会发现这个函数有新的用途。它绝对是你工具箱中应该保留的一个。
包围盒选择
包围盒是能够完全包含一个特征的最小矩形。我们可以将其用作从更大的数据集中高效地子集一个或多个单个特征的有效方法。我们将通过一个示例来查看如何使用简单的包围盒来隔离一组复杂的特征并将其保存为新的 shapefile。在这个例子中,我们将从美国大陆的主要道路 shapefile 中提取波多黎各岛上的道路。您可以从这里下载 shapefile: github.com/GeospatialPython/Learn/raw/master/roads.zip。
浮点坐标比较可能很昂贵,但由于我们使用的是矩形而不是不规则多边形,这段代码对于大多数操作来说已经足够高效:
import shapefile
r = shapefile.Reader('roadtrl020')
w = shapefile.Writer(r.shapeType)
w.fields = list(r.fields)
xmin = -67.5
xmax = -65.0
ymin = 17.8
ymax = 18.6
for road in r.iterShapeRecords():
geom = road.shape
rec = road.record
sxmin, symin, sxmax, symax = geom.bbox
if sxmin < xmin: continue
elif sxmax xmax: continue
elif symin < ymin: continue
elif symax ymax: continue
w._shapes.append(geom)
w.records.append(rec)
w.save('Puerto_Rico_Roads')
现在我们已经使用几何形状选择了要素,让我们换一种方式,通过使用属性来操作。
属性选择
我们现在已经看到了两种不同的方法来子集化更大的数据集,基于空间关系得到一个更小的数据集。但我们也可以使用属性字段来选择数据。因此,让我们快速检查一下使用属性表来子集化矢量数据的方法。在这个例子中,我们将使用一个多边形形状文件,它包含密密麻麻的城市地区在密西西比州内。你可以从这个链接下载这个压缩的形状文件 git.io/vLbU9。
这个脚本实际上非常简单。它创建了 Reader 和 Writer 对象,复制 .dbf 字段,遍历记录以匹配属性,然后将它们添加到 Writer。我们将选择人口少于 5000 的城市地区:
import shapefile
# Create a reader instance
r = shapefile.Reader('MS_UrbanAnC10')
# Create a writer instance
w = shapefile.Writer(r.shapeType)
# Copy the fields to the writer
w.fields = list(r.fields)
# Grab the geometry and records from all features
# with the correct population
selection = []
for rec in enumerate(r.records()):
if rec[1][14] < 5000:
selection.append(rec)
# Add the geometry and records to the writer
for rec in selection:
w._shapes.append(r.shape(rec[0]))
w.records.append(rec[1])
# Save the new shapefile
w.save('MS_Urban_Subset')
属性选择通常很快。空间选择由于浮点计算而计算成本高昂。尽可能确保你首先无法使用属性选择来子集化。以下插图显示了包含所有城市区域的起始形状文件,左侧有一个州边界,以及之前的属性选择后,右侧人口少于 5,000 的人的城市区域:

让我们看看使用 fiona 的相同示例,它利用了 OGR 库。我们将使用嵌套的 with 语句来减少正确打开和关闭文件所需的代码量:
import fiona
with fiona.open('MS_UrbanAnC10.shp') as sf:
filtered = filter(lambda f: f['properties']['POP'] < 5000, sf)
# Shapefile file format driver
drv = sf.driver
# Coordinate Reference System
crs = sf.crs
# Dbf schema
schm = sf.schema
subset = 'MS_Urban_Fiona_Subset.shp'
with fiona.open(subset, 'w',
driver=drv,
crs=crs,
schema=schm) as w:
for rec in filtered:
w.write(rec)
现在,我们已经知道如何组合离散数据集以及将更大的数据集拆分。我们还能做什么?我们可以在数据集内部聚合要素。
聚合几何
GIS 矢量数据集通常由点、线或多边形要素组成。GIS 的一个原则是,地理位置上更接近的事物比地理位置上更远的事物关系更密切。当你有一组相关要素时,通常,对于你试图完成的分析来说,细节太多。将它们概括以加快处理速度或简化地图是有用的。这种操作称为聚合。聚合的一个常见例子是将一组地方政治边界合并成更大的政治边界,例如将县合并成州,或将州合并成国家,或国家合并成洲。
在这个例子中,我们将做的是将包括美国密西西比州所有县的数据集转换成一个代表整个州的单个多边形。Python 的 Shapely 库非常适合这种操作;然而,它只能操作几何形状,不能读取或写入数据文件。为了读取和写入数据文件,我们将使用 Fiona 库。如果您还没有安装 Shapely 或 Fiona,请使用pip安装它们。您可以从这里下载县数据集:git.io/fjt3b。
以下插图显示了县数据集的外观:

以下步骤将向您展示如何将单个县多边形合并成一个多边形:
-
在以下代码中,我们导入所需的库,包括
shapely库的不同部分。 -
然后,我们将打开县 GeoJSON 文件。
-
接下来,我们将复制源文件的架构,它定义了数据集的所有元数据。
-
然后,我们需要修改元数据副本以更改属性,以便定义一个用于州名的单个属性。我们还需要将几何类型从MultiPolygon更改为Polygon。
-
然后,我们将打开我们的输出数据集 GeoJSON 文件,命名为
combined.geojson。 -
接下来,我们将提取所有多边形和属性,并将所有多边形合并成一个。
-
最后,我们将使用新属性将合并的多边形写出来。
-
我们将导入我们的库,包括
OrderDict,以便我们可以控制 shapefile 属性:
# Used OrderedDict to control the order
# of data attributes
from collections import OrderedDict
# Import the shapely geometry classes and methods.
# The "mapping" method returns a GeoJSON representation
# of a geometry.
from shapely.geometry import shape, mapping, Polygon
# Import the shapely union function which combines
# geometries
from shapely.ops import unary_union
# Import Fiona to read and write datasets
import fiona
- 我们打开我们的 GeoJSON 文件并复制元数据:
# Open the counties dataset
with fiona.open('ms_counties.geojson') as src:
# copy the metadata
schema = src.meta.copy()
# Create a new field type for our
# state dataset
fields = {"State": "str:80"}
- 然后,我们创建我们的新字段:
# Create a new property for our dataset
# using the new field
prop = OrderedDict([("State", "Mississippi")])
# Change the metadata geometry type to Polygon
schema['geometry'] = 'Polygon'
schema['schema']['geometry'] = 'Polygon'
- 现在,我们可以将新字段添加到元数据中:
# Add the new field
schema['properties'] = fields
schema['schema']['properties'] = fields
- 接下来,我们可以打开合并的 GeoJSON 文件并写出我们的结果:
# Open the output GeoJSON dataset
with fiona.open('combined.geojson', 'w', **schema) as dst:
# Extract the properties and geometry
# from the counties dataset
props, geom = zip(*[(f['properties'],shape(f['geometry'])) for
f in src])
# Write the new state dataset out while
# combining the polygons into a
# single polygon and add the new property
dst.write({'geometry': mapping(\
Polygon(unary_union(geom).exterior)),
'properties': prop})
输出数据集将类似于以下插图:

现在我们已经了解了关于读取、编辑和写入 GIS 数据的一切,我们可以在接下来的章节中开始可视化它。
创建用于可视化的图像
现在,我们正从计算和数据编辑转向我们可以看到的东西!我们将从创建不同类型的地图开始。在第一章,使用 Python 学习地理空间分析,我们使用 Python 附带的自带 Tkinter 模块可视化了我们的 SimpleGIS 程序。在第四章,地理空间 Python 工具箱,我们检查了创建图像的几种其他方法。现在,我们将通过创建两种特定的主题地图来更深入地研究这些工具。第一种是点密度图,第二种是面状图。
首先,让我们从点密度图开始。
点密度计算
点密度图显示了给定区域内主题的集中情况。如果一个区域被划分为包含统计信息的多边形,你可以使用在该区域内使用固定比例随机分布的点来模拟该信息。这种类型的地图通常用于人口密度图。
在第一章,“使用 Python 学习地理空间分析”,中的猫图是一个点密度图。让我们从头开始使用纯 Python 创建一个点密度图。纯 Python 允许你使用更轻量级的库,这些库通常更容易安装,并且更便携。在这个例子中,我们将使用美国人口普查局沿美国墨西哥湾沿岸的普查区形状文件,其中包含人口数据。我们还将使用点在多边形内算法来确保随机分布的点位于适当的人口普查区内。最后,我们将使用PNGCanvas模块来输出我们的图像。
PNGCanvas模块非常出色且速度快。然而,它没有填充简单矩形以外的多边形的能力。你可以实现一个填充算法,但在纯 Python 中它非常慢。然而,对于快速轮廓和点绘图,它做得很好。
你还会看到world2screen()方法,这与我们在第一章,“使用 Python 学习地理空间分析”中的 SimpleGIS 中使用的坐标到映射算法类似。在这个例子中,我们将读取形状文件并将其作为图像写回:
- 首先,我们导入我们需要的库,包括
pngcanvas,以绘制地图图像:
import shapefile
import random
import pngcanvas
- 接下来,我们定义我们的点在多边形内函数,我们之前已经使用过。在这个例子中,我们将使用它来在位置内随机分布人口值:
def point_in_poly(x,y,poly):
'''Boolean: is a point inside a polygon?'''
# check if point is a vertex
if (x,y) in poly: return True
# check if point is on a boundary
for i in range(len(poly)):
p1 = None
p2 = None
if i==0:
p1 = poly[0]
p2 = poly[1]
else:
p1 = poly[i-1]
p2 = poly[i]
if p1[1] == p2[1] and p1[1] == y and \
x min(p1[0], p2[0]) and x < max(p1[0], p2[0]):
return True
n = len(poly)
inside = False
p1x,p1y = poly[0]
for i in range(n+1):
p2x,p2y = poly[i % n]
if y min(p1y,p2y):
if y <= max(p1y,p2y):
if x <= max(p1x,p2x):
if p1y != p2y:
xints = (y-p1y)*(p2x-p1x)/(p2y-p1y)+p1x
if p1x == p2x or x <= xints:
inside = not inside
p1x,p1y = p2x,p2y
if inside: return True
else: return False
- 现在,我们需要一个函数来将我们的地理空间坐标缩放到地图图像:
def world2screen(bbox, w, h, x, y):
'''convert geospatial coordinates to pixels'''
minx,miny,maxx,maxy = bbox
xdist = maxx - minx
ydist = maxy - miny
xratio = w/xdist
yratio = h/ydist
px = int(w - ((maxx - x) * xratio))
py = int((maxy - y) * yratio)
return (px,py)
- 接下来,我们读取形状文件并设置输出地图图像的大小:
# Open the census shapefile
inShp = shapefile.Reader('GIS_CensusTract_poly')
# Set the output image size
iwidth = 600
iheight = 400
- 接下来,我们需要确定人口字段的索引,以便我们可以获取每个区域的人口计数:
# Get the index of the population field
pop_index = None
dots = []
for i,f in enumerate(inShp.fields):
if f[0] == 'POPULAT11':
# Account for deletion flag
pop_index = i-1
- 然后,我们计算人口密度值。我们希望在地图上为每 100 人创建一个点:
# Calculate the density and plot points
for sr in inShp.shapeRecords():
population = sr.record[pop_index]
# Density ratio - 1 dot per 100 people
density = population / 100
found = 0
- 我们将遍历每个多边形,随机分布点以创建密度图:
# Randomly distribute points until we
# have the correct density
while found < density:
minx, miny, maxx, maxy = sr.shape.bbox
x = random.uniform(minx,maxx)
y = random.uniform(miny,maxy)
if point_in_poly(x,y,sr.shape.points):
dots.append((x,y))
found += 1
- 现在我们已经准备好创建我们的输出图像:
# Set up the PNG output image
c = pngcanvas.PNGCanvas(iwidth,iheight)
# Draw the red dots
c.color = (255,0,0,0xff)
for d in dots:
# We use the *d notation to exand the (x,y) tuple
x,y = world2screen(inShp.bbox, iwidth, iheight, *d)
c.filled_rectangle(x-1,y-1,x+1,y+1)
- 我们已经创建了点。现在,我们需要创建人口普查区的轮廓:
# Draw the census tracts
c.color = (0,0,0,0xff)
for s in inShp.iterShapes():
pixels = []
for p in s.points:
pixel = world2screen(inShp.bbox, iwidth, iheight, *p)
pixels.append(pixel)
c.polyline(pixels)
- 最后,我们将保存输出图像:
# Save the image
with open('DotDensity.png','wb') as img:
img.write(c.dump())
此脚本输出人口普查区的轮廓,以及密度点,以非常有效地展示人口集中情况:

现在,让我们看看地图的第二种类型:渐变图。
渐变图
水色图使用阴影、着色或符号来显示一个区域内平均的值或数量。它们使我们能够将大量数据作为总结来可视化。如果相关数据跨越多个多边形,这种方法很有用。例如,在按国家划分的全球人口密度地图中,许多国家有断开的多边形(例如,夏威夷是美国的一个岛国)。
在本例中,我们将使用我们在第三章中讨论的Python Imaging Library(PIL),地理空间技术景观。PIL 不是纯 Python 编写的,但它是专门为 Python 设计的。我们将重新创建我们之前的点密度示例,作为水色图。我们将根据每平方公里的人数(人口)计算每个普查区的密度比率,并使用该值调整颜色。深色表示人口密集,浅色表示人口稀疏。按照以下步骤操作:
- 首先,我们将导入我们的库:
import math
import shapefile
try:
import Image
import ImageDraw
except:
from PIL import Image, ImageDraw
- 然后,我们需要我们的地理坐标到图像坐标转换函数:
def world2screen(bbox, w, h, x, y):
'''convert geospatial coordinates to pixels'''
minx,miny,maxx,maxy = bbox
xdist = maxx - minx
ydist = maxy - miny
xratio = w/xdist
yratio = h/ydist
px = int(w - ((maxx - x) * xratio))
py = int((maxy - y) * yratio)
return (px,py)
- 现在,我们打开我们的形状文件并设置输出图像大小:
# Open our shapefile
inShp = shapefile.Reader('GIS_CensusTract_poly')
iwidth = 600
iheight = 400
- 然后,我们设置 PIL 来绘制我们的地图图像:
# PIL Image
img = Image.new('RGB', (iwidth,iheight), (255,255,255))
# PIL Draw module for polygon fills
draw = ImageDraw.Draw(img)
- 就像我们之前的例子一样,我们需要获取人口字段的索引:
# Get the population AND area index
pop_index = None
area_index = None
# Shade the census tracts
for i,f in enumerate(inShp.fields):
if f[0] == 'POPULAT11':
# Account for deletion flag
pop_index = i-1
elif f[0] == 'AREASQKM':
area_index = i-1
- 现在,我们可以绘制多边形,根据人口密度着色,并保存图像:
# Draw the polygons
for sr in inShp.shapeRecords():
density = sr.record[pop_index]/sr.record[area_index]
# The 'weight' is a scaled value to adjust the color
# intensity based on population
weight = min(math.sqrt(density/80.0), 1.0) * 50
R = int(205 - weight)
G = int(215 - weight)
B = int(245 - weight)
pixels = []
for x,y in sr.shape.points:
(px,py) = world2screen(inShp.bbox, iwidth, iheight, x, y)
pixels.append((px,py))
draw.polygon(pixels, outline=(255,255,255), fill=(R,G,B))
img.save('choropleth.png')
此脚本使用相对密度绘制以下图表。您可以使用 R、G 和 B 变量调整颜色:

现在我们能够从形状文件中展示统计数据,我们可以看看比形状文件更常见的统计数据源:电子表格。
使用电子表格
如 Microsoft Office Excel 和 Open Office Calc 之类的电子表格便宜(甚至免费)、无处不在、易于使用,非常适合记录结构化数据。出于这些原因,电子表格被广泛用于收集数据以输入 GIS 格式。作为一名分析师,您会发现您经常需要与电子表格打交道。
在前面的章节中,我们讨论了 CSV 格式,它是一种与电子表格具有相同基本行和列数据结构的文本文件。对于 CSV 文件,您使用 Python 的内置csv模块。但大多数时候,人们不会费心将真正的电子表格导出为通用的 CSV 文件。这就是纯 Python 的xlrd模块发挥作用的地方。xlrd这个名字是Excel Reader的缩写,可以从 PyPI 获取。还有一个配套的模块,即xlwt(Excel Writer)模块,用于编写电子表格。这两个模块使得读写 Excel 电子表格变得轻而易举。结合 PyShp,您可以轻松地在电子表格和形状文件之间转换。本例演示了将电子表格转换为形状文件的过程。我们将使用纽约市博物馆点数据的电子表格版本,该版本可在git.io/Jemi9找到。
电子表格包含属性数据,后面跟着一个x列(经度)和一个y列(纬度)。要将其导出为形状文件,我们将执行以下步骤:
-
打开电子表格。
-
创建一个形状文件
Writer对象。 -
将电子表格的第一行作为
dbf列。 -
遍历电子表格的每一行,并将属性复制到
dbf。 -
从电子表格的x和y列创建一个点。
脚本如下:
import xlrd
import shapefile
# Open the spreadsheet reader
xls = xlrd.open_workbook('NYC_MUSEUMS_GEO.xls')
sheet = xls.sheet_by_index(0)
# Open the shapefile writer
w = shapefile.Writer(shapefile.POINT)
# Move data from spreadsheet to shapefile
for i in range(sheet.ncols):
# Read the first header row
w.field(str(sheet.cell(0,i).value), 'C', 40)
for i in range(1, sheet.nrows):
values = []
for j in range(sheet.ncols):
values.append(sheet.cell(i,j).value)
w.record(*values)
# Pull latitude/longitude from the last two columns
w.point(float(values[-2]),float(values[-1]))
w.save('NYC_MUSEUMS_XLS2SHP')
将形状文件转换为电子表格是一个不太常见的操作,尽管并不困难。要将形状文件转换为电子表格,您需要确保使用添加字段示例,该示例来自本章编辑形状文件部分,您有一个x和y列。您将遍历形状,并将x、y值添加到这些列中。然后,您将读取字段名称和dbf中的列值到一个xlwt电子表格对象或 CSV 文件中,使用csv模块。坐标列在以下屏幕截图中有标签:

在下一节中,我们将使用电子表格作为输入数据源。
创建热图
热图用于使用显示密度的栅格图像来显示数据的地理聚类。聚类还可以通过使用数据中的一个字段来权衡,不仅显示地理密度,还显示强度因子。在这个例子中,我们将使用包含在 CSV 数据集中的熊目击数据来创建密西西比州不同地区的熊目击频率热图。这个数据集非常简单,我们将把 CSV 文件当作文本文件处理,这是 CSV 文件的一个很好的特性。
您可以在此处下载数据集:git.io/fjtGL。
输出将是一个简单的 HTML 网络地图,您可以在任何网络浏览器中打开。该网络地图将基于优秀的 Leaflet JavaScript 库。在此基础上,我们将使用 Python Folium 库,这使得我们能够轻松创建 Leaflet 网络地图,以生成 HTML 页面:
import os
import folium
from folium.plugins import HeatMap
f = open('bear_sightings.csv', 'r')
lines = f.readlines()
lines.pop(0)
data = []
bears = [list(map(float, l.strip().split(','))) for l in lines]
m = folium.Map([32.75, -89.52], tiles='stamentonerbackground', zoom_start=7, max_zoom=7, min_zoom=7)
HeatMap(bears, max_zoom=16, radius=22, min_opacity=1, blur=30).add_to(m)
m.save('heatmap.html')
此脚本将创建一个名为heatmap.html的文件。在任何网络浏览器中打开它,您将看到类似的图像:

接下来,我们将学习如何使用 GPS 生成数据来收集如前述热图中的现场数据。
使用 GPS 数据
目前最常见的 GPS 数据类型是 Garmin GPX 格式。我们在第四章,地理空间 Python 工具箱中介绍了这种 XML 格式,它已成为非官方的行业标准。因为它是一种 XML 格式,所以所有 XML 的良好文档规则都适用于它。然而,还有一种比 XML 和 GPX 更早的 GPS 数据类型,称为国家海洋电子协会(NMEA)。这些数据是设计为流式传输的 ASCII 文本句子。
你偶尔会遇到这种格式,尽管它较老且较为晦涩,但它仍然非常活跃,尤其是在通过自动识别系统(AIS)通信船舶位置时,AIS 跟踪全球的船舶。但通常,你有一个纯 Python 的好选择。pynmea模块可在 PyPI 上找到。以下代码是 NMEA 语句的一个小样本:
$GPRMC,012417.859,V,1856.599,N,15145.602,W,12.0,7.27,020713,,E\*4F
$GPGGA,012418.859,1856.599,N,15145.602,W,0,00,,,M,,M,,\*54
$GPGLL,1856.599,N,15145.602,W,012419.859,V\*35
$GPVTG,7.27,T,,M,12.0,N,22.3,K\*52
$GPRMC,012421.859,V,6337.596,N,12330.817,W,66.2,23.41,020713,,E\*74
要从 PyPI 安装pynmea模块并下载完整的样本文件,你可以查看以下 URL:git.io/vLbTv。然后,你可以运行以下样本,它将解析 NMEA 语句到对象。NMEA 语句包含大量信息:
from pynmea.streamer import NMEAStream
nmeaFile = open('nmea.txt')
nmea_stream = NMEAStream(stream_obj=nmeaFile)
next_data = nmea_stream.get_objects()
nmea_objects = []
while next_data:
nmea_objects += next_data
next_data = nmea_stream.get_objects()
# The NMEA stream is parsed!
# Let's loop through the
# Python object types:
for nmea_ob in nmea_objects:
if hasattr(nmea_ob, 'lat'):
print('Lat/Lon: (%s, %s)' % (nmea_ob.lat, nmea_ob.lon))
纬度和经度以称为度分十分之一度的格式存储。例如,这个随机的坐标 4533.35,是 45 度和 33.35 分。一分之一的 0.35 是 21 秒。在另一个例子中,16708.033 是 167 度和 8.033 分。一分之一的 0.033 大约是 2 秒。你可以在aprs.gids.nl/nmea/找到更多关于 NMEA 格式的信息。
GPS 数据是一个重要的位置数据源,但我们可以使用街道地址来描述地球上的一个点。在地球上定位街道地址的方法称为地理编码。
地理编码
地理编码是将街道地址转换为纬度和经度的过程。这个操作对于车载导航系统和在线驾驶方向网站至关重要。Python 有两个优秀的地理编码库,名为geocoder和geopy。这两个库都利用在线地理编码服务,允许你以编程方式地理编码地址。geopy 库甚至允许你进行反向地理编码,将纬度和经度匹配到最近的地址:
- 首先,让我们用一个简单的例子来演示
geocoder库,它默认使用谷歌地图作为其引擎:
import geocoder
g = geocoder.google('1403 Washington Ave, New Orleans, LA 70130')
print(g.geojson)
# {'type': 'Feature', 'geometry': {'type': 'Point', 'coordinates': [-90.08421849999999, 29.9287839]},
'bbox': {'northeast': [29.9301328802915, -90.0828695197085], 'southwest': [29.9274349197085, -90.0855674802915]},
'properties': {'quality': 'street_address', 'lat': 29.9287839, 'city': 'New Orleans',
'provider': 'google', 'geometry': {'type': 'Point', 'coordinates': [-90.08421849999999, 29.9287839]},
'lng': -90.08421849999999, 'method': 'geocode', 'encoding': 'utf-8', 'confidence': 9, 'address': '1403 Washington Ave,
New Orleans, LA 70130, USA', 'ok': True, 'neighborhood': 'Garden District', 'county': 'Orleans Parish',
'accuracy': 'ROOFTOP', 'street': 'Washington Ave', 'location': '1403 Washington Ave, New Orleans, LA 70130',
'bbox': {'northeast': [29.9301328802915, -90.0828695197085], 'southwest': [29.9274349197085, -90.0855674802915]},
'status': 'OK', 'country': 'US', 'state': 'LA', 'housenumber': '1403', 'postal': '70130'}}
print(g.wkt)
# 'POINT(-90.08421849999999 29.9287839)'
在这里,我们打印出该地址的 GeoJSON 记录,其中包含谷歌数据库中所有已知信息。然后,我们以 WKT 字符串的形式打印出返回的纬度和经度,这可以用作其他操作的输入,例如检查地址是否位于洪水平原多边形内。该库的文档还展示了如何切换到其他在线地理编码服务,如 Bing 或 Yahoo。其中一些服务需要 API 密钥,并且可能有请求限制。
- 现在,让我们看看
geopy库。在这个例子中,我们将使用OpenStreetMap数据库进行地理编码。一旦我们将地址匹配到位置,我们将反过来进行反向地理编码:
from geopy.geocoders import Nominatim
g = Nominatim()
location = g.geocode('88360 Diamondhead Dr E, Diamondhead, MS 39525')
rev = g.reverse('{},{}'.format(location.latitude, location.longitude))
print(rev)
# NVision Solutions Inc., 88360, Diamondhead Drive East, Diamondhead, Hancock County, Mississippi, 39520,
# United States of America
print(location.raw)
# {'class': 'office', 'type': 'yes', 'lat': '30.3961962', 'licence': 'Data © OpenStreetMap contributors,
# ODbL 1.0\. http://www.openstreetmap.org/copyright', 'display\_name': 'NVision Solutions Inc.,
# 88360, Diamondhead Drive East, Diamondhead, Hancock County, Mississippi, 39520, United States of America',
# 'lon': '-89.3462139', 'boundingbox': ['30.3961462', '30.3962462', '-89.3462639', '-89.3461639'],
# 'osm\_id': '2470309304', 'osm\_type': 'node', 'place\_id': '25470846', 'importance': 0.421}
既然我们已经知道了多种不同的地理编码方法,让我们看看如何加快这个过程。如果你有数千个地址需要地理编码,这可能需要一段时间。使用多进程,你可以将可能需要几天的时间缩短到几个小时。
多进程
地理空间数据集非常大。处理它们可能需要时间,这可能需要数小时,有时甚至需要数天。但有一种方法可以加快某些操作的处理速度。Python 内置的 multiprocessing 模块可以在您的计算机上生成多个进程,以利用所有可用的处理器。
与 multiprocessing 模块配合得非常好的操作之一是地理编码。在这个例子中,我们将对城市列表进行地理编码,并将该处理分散到您机器上的所有处理器。我们将使用之前相同的地理编码技术,但这次,我们将添加 multiprocessing 模块以增加速度和可扩展性的潜力。以下代码将在多个处理器上同时地理编码城市列表:
- 首先,我们导入所需的模块:
# Import our geocoding module
from geopy.geocoders import Nominatim
# Import the multiprocessing module
import multiprocessing as mp
- 接下来,我们创建我们的地理编码器对象:
# Create our geocoder
g = Nominatim()
- 现在,我们需要一个函数来地理编码单个地址:
# Create a function to geocode an individual address
def gcode(address):
location = g.geocode(address)
print("Geocoding: {}".format(address))
return location
- 接下来,我们创建要处理的城市的列表:
# Our list of cities to process
cities = ["New Orleans, LA", "Biloxi, MS", "Memphis, TN",
"Atlanta, GA", "Little Rock, AR", "Destin, FL"]
- 然后,我们根据可用的处理器数量设置处理器池:
# Create our processor pool counting all of the processors
# on the machine.
pool = mp.Pool(processes=mp.cpu_count())
- 接下来,我们将城市列表映射到地理编码函数,通过处理器池进行:
# Map our cities list to the geocoding function
# and allow the processor pool to split it
# across processors
results = pool.map(gcode, cities)
- 然后,我们可以打印结果:
# Now print the results
print(results)
# [Location(New Orleans, Orleans Parish, Louisiana, USA, (29.9499323, -90.0701156, 0.0)),
# Location(Biloxi, Harrison County, Mississippi, USA, (30.374673, -88.8459433348286, 0.0)),
# Location(Memphis, Shelby County, Tennessee, USA, (35.1490215, -90.0516285, 0.0)),
# Location(Atlanta, Fulton County, Georgia, USA, (33.7490987, -84.3901849, 0.0)),
# Location(Little Rock, Arkansas, USA, (34.7464809, -92.2895948, 0.0)),
# Location(Destin, Okaloosa County, Florida, USA, (30.3935337, -86.4957834, 0.0))]
这种技术可能非常强大,但并非所有类型的处理都可以以这种方式执行。您使用的处理类型必须支持可以分解为离散计算的运算。但当您可以将问题分解,就像我们在本例中所做的那样,结果将快得多。
摘要
本章涵盖了 GIS 分析的关键组件。我们探讨了使用不同方法在地球曲面上测量的挑战。我们研究了坐标转换的基本原理和全重投影,使用了 OGR、PyShp 的 utm 模块和 Fiona,后者简化了 OGR。我们编辑了 shapefile 并执行了空间和属性选择。我们仅使用 Python 从头创建主题地图。我们还从电子表格中导入数据。然后,我们从 NMEA 流中解析 GPS 数据。最后,我们使用地理编码将街道地址转换为位置,反之亦然。
作为地理空间分析师,您可能熟悉 GIS 和遥感,但大多数分析师专注于一个领域或另一个领域。这就是为什么本书将这两个领域分开成单独的章节——这样我们就可以专注于它们之间的差异。正如我们在引言中提到的,本章的技术是所有地理空间分析的基础,并将为您提供学习该领域任何方面的工具。
在 第六章 Python 和遥感 中,我们将探讨遥感。在 GIS 中,我们已经能够使用纯 Python 模块来探索这个领域。在遥感中,由于数据的大小和复杂性,我们将更多地依赖于用 C 编写的编译模块的绑定。
第六章:Python 和遥感
在本章中,我们将讨论遥感。遥感是关于收集关于地球的信息集合,而不需要与它进行物理接触。通常这意味着需要使用卫星或航空图像、光探测与测距(LIDAR),它测量飞机到地球的激光脉冲,或者合成孔径雷达。遥感也可以指处理收集到的数据,这就是我们在本章中使用该术语的方式。随着更多卫星的发射和数据分布变得更加容易,遥感正以越来越令人兴奋的方式发展。卫星和航空图像的高可用性,以及每年发射的有趣新型传感器,正在改变遥感在理解我们世界中所扮演的角色。
在遥感领域,我们逐个遍历图像中的每个像素,并执行某种形式的查询或数学过程。可以将图像视为一个大型数值数组。在遥感中,这些数组可以相当大,大小从数十兆字节到数吉字节不等。虽然 Python 运行速度快,但只有基于 C 的库才能提供在可接受速度下遍历数组的速度。
我们将使用Python 图像库(PIL)进行图像处理,以及 NumPy,它提供多维数组数学。虽然这些库是用 C 编写的以提高速度,但它们是为 Python 设计的,并提供 Pythonic API。
在本章中,我们将涵盖以下主题:
-
交换图像波段
-
创建图像直方图
-
执行直方图拉伸
-
剪辑和分类图像
-
从图像中提取特征
-
变化检测
首先,我们将从基本的图像处理开始,然后在此基础上逐步构建,直至自动变化检测。这些技术将通过添加处理卫星数据和其他遥感产品到我们的工具箱中,来补充前面的章节。
技术要求
-
Python 3.6 或更高版本
-
内存:最低 6 GB(Windows),8 GB(macOS),推荐 8 GB
-
存储:最低 7200 RPM SATA,可用空间 20 GB;推荐 SSD,可用空间 40 GB
-
处理器:最低配置 Intel Core i3 2.5 GHz;推荐配置 Intel Core i5
交换图像波段
我们的眼睛只能看到可见光谱中的颜色,这些颜色是红色、绿色和蓝色(RGB)的组合。空中和空间传感器可以收集可见光谱之外的能量波长。为了查看这些数据,我们将代表不同波长光反射率的图像在 RGB 通道中移动,以制作彩色图像。
这些图像通常最终会变成奇特和外星般的颜色组合,这可能会使视觉分析变得困难。以下是一个典型的卫星图像示例,展示了位于墨西哥湾沿岸密西西比州 NASA 斯坦尼斯太空中心的 Landsat 7 卫星场景,该中心是遥感以及地理空间分析领域的领先中心:

大部分植被看起来是红色的,水几乎看起来是黑色的。这是一类假彩色图像,意味着图像的颜色不是基于 RGB 光。然而,我们可以更改波段的顺序或替换某些波段,以创建另一种看起来更像我们习惯看到世界的假彩色图像。为此,你首先需要从这个链接下载此图像的 ZIP 文件:git.io/vqs41。
我们在第四章“地理空间 Python 工具箱”的“安装 GDAL 和 NumPy”部分中安装了具有 Python 绑定的 GDAL 库。GDAL 库包含一个名为gdal_array的模块,该模块可以将遥感图像加载到 NumPy 数组中,并从 NumPy 数组中保存遥感图像,以便于操作。GDAL 本身是一个数据访问库,并不提供太多的处理功能。因此,在本章中,我们将主要依赖 NumPy 来实际更改图像。
在这个例子中,我们将使用gdal_array将图像加载到 NumPy 数组中,然后立即将其保存到一个新的 GeoTiff 文件中。然而,在保存时,我们将使用 NumPy 的高级数组切片功能来更改波段的顺序。在 NumPy 中,图像是多维数组,其顺序为波段、高度和宽度。这意味着具有三个波段的图像将是一个长度为 3 的数组,包含图像的波段、高度和宽度数组。需要注意的是,NumPy 引用数组位置的方式是y,x(行,列),而不是我们在电子表格和其他软件中使用的常规x, y(列,行)格式。让我们开始吧:
- 首先,我们将导入
gdal_array:
from gdal import gdal_array
- 接下来,我们将加载一个名为
FalseColor.tif的图像到numpy数组中:
# name of our source image
src = "FalseColor.tif"
# load the source image into an array
arr = gdal_array.LoadFile(src)
- 接下来,我们将通过切片数组、重新排列顺序并将它保存回来重新排序图像波段:
# swap bands 1 and 2 for a natural color image.
# We will use numpy "advanced slicing" to reorder the bands.
# Using the source image
output = gdal_array.SaveArray(arr[[1, 0, 2], :], "swap.tif",
format="GTiff", prototype=src)
# Dereference output to avoid corrupted file on some platforms
output = None
在SaveArray方法中,最后一个参数被称为原型。此参数允许您指定另一个 GDAL 图像,从中复制空间参考信息和一些其他图像参数。如果没有此参数,我们最终会得到一个没有地理参考信息的图像,这种图像不能在 GIS 中使用。在这种情况下,我们指定输入图像文件名,因为图像除了波段顺序外都是相同的。在这个方法中,你可以看出 Python GDAL API 是 C 库的包装器,并不像 Python 设计的库那样 Pythonic。例如,一个纯 Python 库会编写SaveArray()方法为save_array(),以遵循 Python 标准。
这个例子产生的结果是swap.tif图像,这是一个视觉效果更好的图像,有绿色植被和蓝色水域:

这张图片只有一个问题:它有点暗,看不清楚。让我们看看在下一节中能否找出原因。
创建直方图
直方图显示了数据集中数据分布的统计频率。在遥感的情况下,数据集是一个图像。数据分布是像素在0到255范围内的频率,这是在计算机上存储图像信息所使用的 8 字节数的范围。
在 RGB 图像中,颜色用 3 位数组表示,其中(0,0,0)表示黑色,(255,255,255)表示白色。我们可以用 y 轴上的每个值的频率和 x 轴上 256 个可能的像素值范围来绘制图像的直方图。
记得在第一章,使用 Python 学习地理空间分析,在创建最简单的 Python GIS部分,当我们使用 Python 包含的 Turtle 图形引擎创建一个简单的 GIS 时?嗯,我们也可以用它轻松地绘制直方图。
直方图通常是一个一次性产品,用于快速脚本。此外,直方图通常以条形图的形式显示,条形的宽度代表分组数据箱的大小。但是,在图像中,每个bin只有一个值,所以我们将创建一个线形图。我们将使用本例中的直方图函数,并为每个相应的波段创建红色、绿色和蓝色线条。
本例的绘图部分默认将y轴值缩放到图像中找到的最大 RGB 频率。技术上,y轴代表最大频率,即图像中的像素数,如果图像是单一颜色的话。我们将再次使用turtle模块,但这个例子可以轻松地转换成任何图形输出模块。让我们看看我们在前面的例子中创建的swap.tif图像:
- 首先,我们导入所需的库,包括
turtle图形库:
from gdal import gdal_array
import turtle as t
- 现在,我们创建一个
histogram函数,它可以接受一个数组并将数字排序到直方图的各个箱子中:
def histogram(a, bins=list(range(0, 256))):
fa = a.flat
n = gdal_array.numpy.searchsorted(gdal_array.numpy.sort(fa), bins)
n = gdal_array.numpy.concatenate([n, [len(fa)]])
hist = n[1:]-n[:-1]
return hist
- 最后,我们有我们的
turtle图形函数,它接受直方图并绘制它:
def draw_histogram(hist, scale=True):
- 使用以下代码绘制图形轴:
t.color("black")
axes = ((-355, -200), (355, -200), (-355, -200), (-355, 250))
t.up()
for p in axes:
t.goto(p)
t.down()
t.up()
- 然后,我们可以给它们标注:
t.goto(0, -250)
t.write("VALUE", font=("Arial, ", 12, "bold"))
t.up()
t.goto(-400, 280)
t.write("FREQUENCY", font=("Arial, ", 12, "bold"))
x = -355
y = -200
t.up()
- 现在,我们在 x 轴上添加刻度,这样我们就可以看到线条的值:
for i in range(1, 11):
x = x+65
t.goto(x, y)
t.down()
t.goto(x, y-10)
t.up()
t.goto(x, y-25)
t.write("{}".format((i*25)), align="center")
- 我们会对 y 轴做同样的处理:
x = -355
y = -200
t.up()
pixels = sum(hist[0])
if scale:
max = 0
for h in hist:
hmax = h.max()
if hmax > max:
max = hmax
pixels = max
label = int(pixels/10)
for i in range(1, 11):
y = y+45
t.goto(x, y)
t.down()
t.goto(x-10, y)
t.up()
t.goto(x-15, y-6)
t.write("{}".format((i*label)), align="right")
- 我们可以开始绘制我们的直方图线条:
x_ratio = 709.0 / 256
y_ratio = 450.0 / pixels
colors = ["red", "green", "blue"]
for j in range(len(hist)):
h = hist[j]
x = -354
y = -199
t.up()
t.goto(x, y)
t.down()
t.color(colors[j])
for i in range(256):
x = i * x_ratio
y = h[i] * y_ratio
x = x - (709/2)
y = y + -199
t.goto((x, y))
- 最后,我们可以加载我们的图像,并使用之前定义的函数绘制其直方图:
im = "swap.tif"
histograms = []
arr = gdal_array.LoadFile(im)
for b in arr:
histograms.append(histogram(b))
draw_histogram(histograms)
t.pen(shown=False)
t.done()
这是运行前面的代码示例后swap.tif直方图的样子:

如您所见,所有三个波段都紧密地聚集在图表的左侧,并且所有值都小于125左右。随着这些值接近零,图像变暗,这并不奇怪。
为了好玩,让我们再次运行脚本,当我们调用draw_histogram()函数时,我们将添加scale=False选项,以了解图像的大小并提供绝对刻度。我们将更改以下行:
draw_histogram(histograms)
这将变为以下内容:
draw_histogram(histograms, scale=False)
这种变化将产生以下直方图图:

如您所见,很难看到值分布的细节。然而,如果您正在比较来自同一源图像的多个不同产品的多个直方图,这种绝对刻度方法是有用的。
因此,现在我们了解了使用直方图统计地查看图像的基本方法,那么我们如何使图像更亮呢?让我们在下一节中查看。
执行直方图拉伸
直方图拉伸操作确实如其名称所示。它在整个刻度上重新分配像素值。通过这样做,我们在高亮度级别有更多的值,图像变得更亮。因此,在这个例子中,我们将重用我们的直方图函数,但我们将添加另一个名为stretch()的函数,它接受一个图像数组,创建直方图,然后为每个波段扩展值范围。我们将在swap.tif上运行这些函数,并将结果保存到名为stretched.tif的图像中:
import gdal_array
import operator
from functools import reduce
def histogram(a, bins=list(range(0, 256))):
fa = a.flat
n = gdal_array.numpy.searchsorted(gdal_array.numpy.sort(fa), bins)
n = gdal_array.numpy.concatenate([n, [len(fa)]])
hist = n[1:]-n[:-1]
return hist
def stretch(a):
"""
Performs a histogram stretch on a gdal_array array image.
"""
hist = histogram(a)
lut = []
for b in range(0, len(hist), 256):
# step size
step = reduce(operator.add, hist[b:b+256]) / 255
# create equalization look-up table
n = 0
for i in range(256):
lut.append(n / step)
n = n + hist[i+b]
gdal_array.numpy.take(lut, a, out=a)
return asrc = "swap.tif"
arr = gdal_array.LoadFile(src)
stretched = stretch(arr)
output = gdal_array.SaveArray(arr, "stretched.tif", format="GTiff", prototype=src)
output = None
stretch算法将产生以下图像。看看它变得多么明亮和视觉上吸引人:

我们可以通过在im变量中更改文件名到stretched.tif来在我们的turtle图形直方图脚本上运行我们的swap.tif:
im = "stretched.tif"
运行前面的代码将给出以下直方图:

如您所见,现在三个波段都均匀分布了。它们相互之间的相对分布是相同的,但在图像中,它们现在分布在整个频谱上。
现在我们能够更改图像以获得更好的展示效果,让我们看看如何裁剪它们以检查特定感兴趣的区域。
裁剪图像
分析员很少对整个卫星场景感兴趣,这可以轻松覆盖数百平方英里。考虑到卫星数据的大小,我们非常希望将图像的大小减少到仅我们感兴趣的区域。实现这种减少的最佳方式是将图像裁剪到定义我们研究区域的边界。我们可以使用 shapefiles(或其他矢量数据)作为我们的边界定义,并基本上删除所有该边界之外的数据。
以下图像包含我们的stretched.tif图像,以及一个县边界文件叠加在上面,在Quantum GIS(QGIS)中可视化:

要裁剪图像,我们需要遵循以下步骤:
-
使用
gdal_array将图像加载到数组中。 -
使用 PyShp 创建一个 shapefile 读取器。
-
将 shapefile 栅格化到地理参考图像(将其从矢量转换为栅格)。
-
将 shapefile 图像转换为二进制掩码或过滤器,以仅获取 shapefile 边界内我们想要的图像像素。
-
通过掩码过滤卫星图像。
-
丢弃掩码外的卫星图像数据。
-
将裁剪后的卫星图像保存为
clip.tif。
我们在第四章“地理空间 Python 工具箱”中安装了 PyShp,所以你应该已经通过 PyPi 安装了它。我们还将在此脚本中添加一些有用的新实用函数。第一个是world2pixel(),它使用 GDAL GeoTransform 对象为我们执行世界坐标到图像坐标的转换。
这仍然是我们在这本书中一直使用的相同过程,但它与 GDAL 的集成更好。
我们还添加了imageToArray()函数,它将 PIL 图像转换为 NumPy 数组。县边界 shapefile 是我们在前几章中使用的hancock.shp边界,但如果你需要,也可以从这里下载:git.io/vqsRH。
我们使用 PIL,因为它是将 shapefile 作为掩码图像光栅化的最简单方式,以过滤掉 shapefile 边界之外的像素。让我们开始吧:
- 首先,我们将加载所需的库:
import operator
from osgeo import gdal, gdal_array, osr
import shapefile
- 现在,我们将加载 PIL。在不同的平台上可能需要以不同的方式安装,因此我们必须检查这种差异:
try:
import Image
import ImageDraw
except:
from PIL import Image, ImageDraw
- 现在,我们将设置输入图像、shapefile 和输出图像的变量:
# Raster image to clip
raster = "stretched.tif"
# Polygon shapefile used to clip
shp = "hancock"
# Name of clipped raster file(s)
output = "clip"
- 接下来,创建一个函数,它简单地将图像转换为
numpy数组,这样我们就可以将创建的掩码图像转换为 NumPy 数组,并在基于 NumPy 的裁剪过程中使用它:
def imageToArray(i):
"""
Converts a Python Imaging Library array to a gdal_array image.
"""
a = gdal_array.numpy.fromstring(i.tobytes(), 'b')
a.shape = i.im.size[1], i.im.size[0]
return a
- 接下来,我们需要一个函数将地理空间坐标转换为图像像素,这将允许我们使用来自裁剪 shapefile 的坐标来限制要保存的图像像素:
def world2Pixel(geoMatrix, x, y):
"""
Uses a gdal geomatrix (gdal.GetGeoTransform()) to calculate
the pixel location of a geospatial coordinate
"""
ulX = geoMatrix[0]
ulY = geoMatrix[3]
xDist = geoMatrix[1]
yDist = geoMatrix[5]
rtnX = geoMatrix[2]
rtnY = geoMatrix[4]
pixel = int((x - ulX) / xDist)
line = int((ulY - y) / abs(yDist))
return (pixel, line)
- 现在,我们可以将源图像加载到
numpy数组中:
# Load the source data as a gdal_array array
srcArray = gdal_array.LoadFile(raster)
- 我们还将以 gdal 图像的形式加载源图像,因为
gdal_array没有给我们转换坐标到像素所需的地理变换信息:
# Also load as a gdal image to get geotransform (world file) info
srcImage = gdal.Open(raster)
geoTrans = srcImage.GetGeoTransform()
- 现在,我们将使用 Python shapefile 库打开我们的 shapefile:
# Use pyshp to open the shapefile
r = shapefile.Reader("{}.shp".format(shp))
- 接下来,我们将根据源图像将 shapefile 边界框坐标转换为图像坐标:
# Convert the layer extent to image pixel coordinates
minX, minY, maxX, maxY = r.bbox
ulX, ulY = world2Pixel(geoTrans, minX, maxY)
lrX, lrY = world2Pixel(geoTrans, maxX, minY)
- 然后,我们可以根据 shapefile 的范围计算输出图像的大小,并仅取源图像的相应部分:
# Calculate the pixel size of the new image
pxWidth = int(lrX - ulX)
pxHeight = int(lrY - ulY)
clip = srcArray[:, ulY:lrY, ulX:lrX]
- 接下来,我们将为输出图像创建新的几何矩阵数据:
# Create a new geomatrix for the image
# to contain georeferencing data
geoTrans = list(geoTrans)
geoTrans[0] = minX
geoTrans[3] = maxY
- 现在,我们可以从 shapefile 创建一个简单的黑白掩码图像,它将定义我们想要从源图像中提取的像素:
# Map points to pixels for drawing the county boundary
# on a blank 8-bit, black and white, mask image.
pixels = []
for p in r.shape(0).points:
pixels.append(world2Pixel(geoTrans, p[0], p[1]))
rasterPoly = Image.new("L", (pxWidth, pxHeight), 1)
# Create a blank image in PIL to draw the polygon.
rasterize = ImageDraw.Draw(rasterPoly)
rasterize.polygon(pixels, 0)
- 接下来,我们将掩码图像转换为
numpy数组:
# Convert the PIL image to a NumPy array
mask = imageToArray(rasterPoly)
- 最后,我们准备好使用掩码数组在
numpy中裁剪源数组并将其保存为新的 geotiff 图像:
# Clip the image using the mask
clip = gdal_array.numpy.choose(mask, (clip, 0)).astype(
gdal_array.numpy.uint8)
# Save ndvi as tiff
gdal_array.SaveArray(clip, "{}.tif".format(output),
format="GTiff", prototype=raster)
此脚本生成以下裁剪图像:

在县边界之外保留的黑色区域实际上被称为NoData值,这意味着在该位置没有信息,并且大多数地理空间软件会忽略这些值。因为图像是矩形的,所以NoData值对于不完全填充图像的数据是常见的。
您现在已经走过了全球地理空间分析师每天用来准备多光谱卫星和航空图像以用于 GIS 的整个工作流程。在下一节中,我们将探讨我们如何实际上分析图像作为信息。
图像分类
自动化遥感(ARS)很少在可见光谱中进行。ARS 处理图像时无需任何人工输入。在可见光谱之外最常用的波长是红外和近红外。
以下插图是一张热成像图(波段 10),来自最近一次的 Landsat 8 飞越美国墨西哥湾沿岸,从路易斯安那州的纽奥尔良到阿拉巴马州的莫比尔。图像中的主要自然特征已被标注,以便您定位:

因为图像中的每个像素都有一个反射率值,所以它是信息,而不仅仅是颜色。反射率类型可以确切地告诉我们一个特征是什么,而不是我们通过观察来猜测。Python 可以看到这些值,并以与我们直观地通过分组相关像素值相同的方式挑选出特征。我们可以根据像素之间的关系对像素进行着色,以简化图像并查看相关特征。这种技术称为分类。
分类可以从基于直方图推导出的某些值分布算法的相对简单分组,到涉及训练数据集甚至计算机学习和人工智能的复杂方法。最简单的形式被称为无监督分类,其中除了图像本身之外没有提供任何其他输入。涉及某种训练数据以引导计算机的方法称为监督分类。需要注意的是,分类技术在许多领域都有应用,从寻找患者体内扫描中癌细胞的外科医生,到在赌场使用面部识别软件在安全录像中自动识别已知骗子在二十一点桌上的情况。
为了介绍遥感分类,我们将仅使用直方图将具有相似颜色和强度的像素分组,看看我们得到什么。首先,您需要从这里下载 Landsat 8 场景:git.io/vByJu。
与之前示例中的histogram()函数不同,我们将使用 NumPy 中包含的版本,该版本允许您轻松指定箱数,并返回两个数组,包含频率以及箱值范围。我们将使用包含范围的第二个数组作为图像的类定义。lut或查找表是一个任意调色板,用于将颜色分配给 20 个无监督类别。您可以使用任何颜色。让我们看看以下步骤:
- 首先,我们导入我们的库:
import gdal
from gdal import gdal_array, osr
- 接下来,我们为输入和输出图像设置一些变量:
# Input file name (thermal image)
src = "thermal.tif"
# Output file name
tgt = "classified.jpg"
- 将图像加载到
numpy数组中进行处理:
# Load the image into numpy using gdal
srcArr = gdal_array.LoadFile(src)
- 现在,我们将使用 20 个组或
bins来创建我们图像的直方图,这些组将用于分类:
# Split the histogram into 20 bins as our classes
classes = gdal_array.numpy.histogram(srcArr, bins=20)[1]
- 然后,我们将创建一个查找表,该表将定义我们类别的颜色范围,以便我们可以可视化它们:
# Color look-up table (LUT) - must be len(classes)+1.
# Specified as R, G, B tuples
lut = [[255, 0, 0], [191, 48, 48], [166, 0, 0], [255, 64, 64], [255,
115, 115], [255, 116, 0], [191, 113, 48], [255, 178, 115], [0,
153, 153], [29, 115, 115], [0, 99, 99], [166, 75, 0], [0, 204,
0], [51, 204, 204], [255, 150, 64], [92, 204, 204], [38, 153,
38], [0, 133, 0], [57, 230, 57], [103, 230, 103], [184, 138, 0]]
- 现在我们已经完成了设置,我们可以进行分类:
# Starting value for classification
start = 1
# Set up the RGB color JPEG output image
rgb = gdal_array.numpy.zeros((3, srcArr.shape[0],
srcArr.shape[1], ), gdal_array.numpy.float32)
# Process all classes and assign colors
for i in range(len(classes)):
mask = gdal_array.numpy.logical_and(start <= srcArr, srcArr <=
classes[i])
for j in range(len(lut[i])):
rgb[j] = gdal_array.numpy.choose(mask, (rgb[j], lut[i][j]))
start = classes[i]+1
- 最后,我们可以保存我们的分类图像:
# Save the image
output = gdal_array.SaveArray(rgb.astype(gdal_array.numpy.uint8), tgt, format="JPEG")
output = None
以下图像是我们的分类输出,我们刚刚将其保存为 JPEG 格式:

在保存为图像时,我们没有指定原型参数,因此它没有地理参考信息,尽管我们本可以轻松地将输出保存为 GeoTIFF 格式。
对于一个非常简单的无监督分类来说,这个结果并不坏。岛屿和海岸平原以不同的绿色色调出现。云被隔离为橙色和深蓝色。我们在内陆有一些混淆,因为陆地特征的颜色与墨西哥湾相同。我们可以通过手动定义类范围而不是仅仅使用直方图来进一步细化这个过程。
现在我们有了分离图像中特征的能力,我们可以尝试提取特征作为矢量数据,以便包含在 GIS 中。
从图像中提取特征
对图像进行分类的能力使我们转向另一个遥感能力。在你过去几章中处理形状文件之后,你是否曾经想过它们从何而来?像形状文件这样的矢量 GIS 数据通常是从遥感图像中提取的,例如我们之前看到的例子。
提取通常涉及分析师在图像中的每个对象周围点击并绘制特征以保存为数据。但是,有了良好的遥感数据和适当的预处理,从图像中自动提取特征是可能的。
对于这个例子,我们将从我们的 Landsat 8 热图像中取一个子集,以隔离墨西哥湾的一组屏障岛。岛屿呈现白色,因为沙子很热,而较冷的水则呈现黑色(你可以从这里下载这张图片:git.io/vqarj):

我们使用这个例子的目标是自动将图像中的三个岛屿提取为形状文件。但在我们能够做到这一点之前,我们需要屏蔽掉我们不感兴趣的数据。例如,水具有广泛的像素值范围,岛屿本身也是如此。如果我们只想提取岛屿本身,我们需要将所有像素值推入仅两个组中,使图像变为黑白。这种技术称为阈值化。图像中的岛屿与背景中的水有足够的对比度,阈值化应该可以很好地隔离它们。
在下面的脚本中,我们将图像读入一个数组,然后使用仅两个分箱对图像进行直方图化。然后,我们将使用黑色和白色为两个分箱着色。这个脚本只是我们分类脚本的修改版,输出非常有限。让我们看看以下步骤:
- 首先,我们导入我们需要的库:
from gdal import gdal_array
- 接下来,我们定义输入和输出图像的变量:
# Input file name (thermal image)
src = "islands.tif"
# Output file name
tgt = "islands_classified.tiff"
- 然后,我们可以加载图像:
# Load the image into numpy using gdal
srcArr = gdal_array.LoadFile(src)
- 现在,我们可以设置我们的简单分类方案:
# Split the histogram into 20 bins as our classes
classes = gdal_array.numpy.histogram(srcArr, bins=2)[1]
lut = [[255, 0, 0], [0, 0, 0], [255, 255, 255]]
- 接下来,我们对图像进行分类:
# Starting value for classification
start = 1
# Set up the output image
rgb = gdal_array.numpy.zeros((3, srcArr.shape[0], srcArr.shape[1], ),
gdal_array.numpy.float32)
# Process all classes and assign colors
for i in range(len(classes)):
mask = gdal_array.numpy.logical_and(start <= srcArr, srcArr <=
classes[i])
for j in range(len(lut[i])):
rgb[j] = gdal_array.numpy.choose(mask, (rgb[j], lut[i][j]))
start = classes[i]+1
- 最后,我们保存图像:
# Save the image
gdal_array.SaveArray(rgb.astype(gdal_array.numpy.uint8),
tgt, format="GTIFF", prototype=src)
输出看起来很棒,如下面的图像所示:

岛屿被清楚地隔离,因此我们的提取脚本将能够将它们识别为多边形并保存到形状文件中。GDAL 库有一个名为 Polygonize() 的方法,它正好做这件事。它将图像中所有孤立像素集分组,并将它们保存为要素数据集。在这个脚本中,我们将使用的一个有趣的技术是使用我们的输入图像作为掩码。
Polygonize() 方法允许您指定一个掩码,它将使用黑色作为过滤器,以防止水被提取为多边形,我们最终将只得到岛屿。在脚本中需要注意的另一个区域是,我们复制了源图像的地理参考信息到我们的形状文件,以正确地定位它。让我们看看以下步骤:
- 首先,我们导入我们的库:
import gdal
from gdal import ogr, osr
- 接下来,我们设置输入和输出图像以及形状文件的变量:
# Thresholded input raster name
src = "islands_classified.tiff"
# Output shapefile name
tgt = "extract.shp"
# OGR layer name
tgtLayer = "extract"
- 让我们打开我们的输入图像并获取第一个也是唯一的一个波段:
# Open the input raster
srcDS = gdal.Open(src)
# Grab the first band
band = srcDS.GetRasterBand(1)
- 然后,我们将告诉
gdal使用该波段作为掩码:
# Force gdal to use the band as a mask
mask = band
- 现在,我们准备好设置我们的形状文件:
# Set up the output shapefile
driver = ogr.GetDriverByName("ESRI Shapefile")
shp = driver.CreateDataSource(tgt)
- 然后,我们需要从源图像复制我们的空间参考信息到形状文件,以便在地球上定位它:
# Copy the spatial reference
srs = osr.SpatialReference()
srs.ImportFromWkt(srcDS.GetProjectionRef())
layer = shp.CreateLayer(tgtLayer, srs=srs)
- 现在,我们可以设置我们的形状文件属性:
# Set up the dbf file
fd = ogr.FieldDefn("DN", ogr.OFTInteger)
layer.CreateField(fd)
dst_field = 0
- 最后,我们可以提取我们的多边形:
# Automatically extract features from an image!
extract = gdal.Polygonize(band, mask, layer, dst_field, [], None)
输出的形状文件简单地命名为 extract.shp。如您从第四章中可能记得的,地理空间 Python 工具箱,我们使用 PyShp 和 PNG Canvas 创建了一个快速纯 Python 脚本,用于可视化形状文件。我们将把这个脚本带回这里,以便我们可以查看我们的形状文件,但我们会给它添加一些额外的功能。最大的岛屿有一个小潟湖,在多边形中显示为一个洞。为了正确渲染它,我们必须处理形状文件记录中的部分。
使用该脚本的前一个示例没有这样做,所以我们将在这个步骤中添加这个部分,我们将通过形状文件特征循环:
- 首先,我们需要导入我们将需要的库:
import shapefile
import pngcanvas
- 接下来,我们从形状文件中获取空间信息,这将允许我们将坐标映射到像素:
r = shapefile.Reader("extract.shp")
xdist = r.bbox[2] - r.bbox[0]
ydist = r.bbox[3] - r.bbox[1]
iwidth = 800
iheight = 600
xratio = iwidth/xdist
yratio = iheight/ydist
- 现在,我们将创建一个列表来保存我们的多边形:
polygons = []
- 然后,我们将遍历形状文件并收集我们的多边形:
for shape in r.shapes():
for i in range(len(shape.parts)):
pixels = []
pt = None
if i < len(shape.parts)-1:
pt = shape.points[shape.parts[i]:shape.parts[i+1]]
else:
pt = shape.points[shape.parts[i]:]
- 接下来,我们将每个点映射到图像像素:
for x, y in pt:
px = int(iwidth - ((r.bbox[2] - x) * xratio))
py = int((r.bbox[3] - y) * yratio)
pixels.append([px, py])
polygons.append(pixels)
- 接下来,我们使用
PNGCanvas中的多边形像素信息绘制图像:
c = pngcanvas.PNGCanvas(iwidth, iheight)
for p in polygons:
c.polyline(p)
- 最后,我们保存图像:
with open("extract.png", "wb") as f:
f.write(c.dump())
f.close()
以下图像显示了我们的自动提取的岛屿特征:

执行此类工作的商业软件可能轻易花费数万美元。虽然这些软件包非常稳健,但看到您仅使用简单的 Python 脚本和一些开源软件包就能走多远仍然很有趣且令人鼓舞。在许多情况下,您可以完成所需的所有工作。
最西端的岛屿包含多边形孔洞,如下面的图像所示,并放大了该区域:

如果您想看看如果我们没有处理多边形孔洞会发生什么,只需运行第四章中“地理空间 Python 工具箱”的脚本版本,并与这个相同的 shapefile 进行比较即可。潟湖不容易看到,但如果你使用另一个脚本,你会找到它。
自动特征提取是地理空间分析中的圣杯,因为手动提取特征需要高昂的成本和繁琐的努力。特征提取的关键是正确的图像分类。自动特征提取与水体、岛屿、道路、农田、建筑和其他具有与背景高对比度像素值的特征配合得很好。
您现在已经很好地掌握了使用 GDAL、NumPy 和 PIL 处理遥感数据的方法。现在是时候继续到我们最复杂的例子:变化检测了。
理解变化检测
变化检测是从两个不同日期的同一区域自动识别差异的过程,这两个图像是精确地理配准的。这实际上只是图像分类的另一种形式。就像我们之前的分类示例一样,它可以从这里使用的简单技术到提供惊人精确和准确结果的复杂算法。
对于这个例子,我们将使用来自沿海地区的一对图像。这些图像显示了在一场大飓风前后的人口密集区域,因此存在显著差异,其中许多差异很容易通过视觉识别,这使得这些样本非常适合学习变化检测。我们的技术是简单地使用 NumPy 从第二张图像中减去第一张图像以获得简单的图像差异。这是一个有效且常用的技术。
优点是它全面且非常可靠。这个过于简单的算法的缺点是它没有隔离变化类型。许多变化对于分析来说并不重要,例如海洋上的波浪。在这个例子中,我们将有效地屏蔽水面以避免这种干扰,并仅关注差异图像直方图右侧的高反射率值。
您可以从git.io/vqa6h下载基线图像。
您可以从git.io/vqaic下载已更改的图像。
注意这些图像相当大——分别为 24 MB 和 64 MB!
基准图像是全色图像,而变化图像是假彩色图像。全色图像是由捕获所有可见光的传感器创建的,通常是高分辨率传感器而不是捕获包含限制波长的波段的多元光谱传感器。
通常,你会使用两个相同的波段组合,但这些样本将适用于我们的目的。我们可以用来评估变化检测的视觉标记包括图像东南象限的一座桥梁,它从半岛延伸到图像的边缘。这座桥梁在原始图像中清晰可见,但被飓风减少到桩。另一个标记是西北象限的一艘船,它在变化后的图像中表现为一条白色轨迹,但在原始图像中并不存在。
中性标记是水和穿过城镇并连接到桥梁的州际公路,这是一个容易看到的混凝土特征,在两个图像之间没有显著变化。以下是基于线的图像截图:

要自己近距离查看这些图像,你应该使用 QGIS 或 OpenEV(FWTools),如第三章中“地理空间技术景观”部分的Quantum GIS and OpenEv所述,以便轻松查看。以下图像是变化后的图像:

因此,让我们进行变化检测:
- 首先,我们加载我们的库:
import gdal
from gdal import gdal_array
import numpy as np
- 现在,我们设置输入和输出图像的变量:
# "Before" image
im1 = "before.tif"
# "After" image
im2 = "after.tif"
- 接下来,我们使用
gdal_array将这两张图像读入 NumPy 数组:
# Load before and after into arrays
ar1 = gdal_array.LoadFile(im1).astype(np.int8)
ar2 = gdal_array.LoadFile(im2)[1].astype(np.int8)
- 现在,我们从变化后的图像中减去原始图像(差值=变化后-变化前):
# Perform a simple array difference on the images
diff = ar2 - ar1
- 然后,我们将图像分为五类:
# Set up our classification scheme to try
# and isolate significant changes
classes = np.histogram(diff, bins=5)[1]
- 接下来,我们将颜色表设置为使用黑色来屏蔽低级别。我们这样做是为了过滤水和道路,因为它们在图像中较暗:
# The color black is repeated to mask insignificant changes
lut = [[0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 255, 0], [255, 0, 0]]
- 然后,我们为类别分配颜色:
# Starting value for classification
start = 1
# Set up the output image
rgb = np.zeros((3, diff.shape[0], diff.shape[1], ), np.int8)
# Process all classes and assign colors
for i in range(len(classes)):
mask = np.logical_and(start <= diff, diff <= classes[i])
for j in range(len(lut[i])):
rgb[j] = np.choose(mask, (rgb[j], lut[i][j]))
start = classes[i]+1
- 最后,我们保存我们的图像:
# Save the output image
output = gdal_array.SaveArray(rgb, "change.tif", format="GTiff", prototype=im2)
output = None
这是我们的初始差异图像的样子:

主要来说,绿色类别代表添加了某些东西的区域。红色则表示较暗的值,可能是移除了某些东西。我们可以看到,在西北象限的船迹是绿色的。我们还可以看到植被有很多变化,这是由于季节差异而预期的。桥梁是一个异常,因为暴露的桩比原始桥梁较暗的表面更亮,这使得它们变成了绿色而不是红色。
混凝土是变化检测中的一个重要指标,因为它在阳光下非常明亮,通常是新发展的标志。相反,如果一座建筑被拆除并且混凝土被移除,这种差异也容易识别。因此,我们在这里使用的简单差异算法并不完美,但可以通过阈值、掩膜、更好的类别定义和其他技术进行大幅改进。
要真正欣赏我们的变化检测产品,你可以在 QGIS 中将它叠加到原始或后续图像上,并将颜色设置为黑色透明,如图所示:

可能地,你可以将这种变化检测分析结合特征提取示例,以提取作为矢量数据的变化,这些数据可以在 GIS 中高效地进行分析。
摘要
在本章中,我们涵盖了遥感的基础,包括波段交换、直方图、图像分类、特征提取和变化检测。与其他章节一样,我们尽可能地接近纯 Python,在处理速度上做出妥协的地方,我们尽可能地限制软件库,以保持简单。然而,如果你安装了本章的工具,你实际上拥有一个完整的遥感软件包,其局限性仅在于你学习的愿望。
本章中的技术是所有遥感处理的基础,将使你能够构建更复杂的操作。
在下一章中,我们将研究高程数据。高程数据既不属于 GIS 也不属于遥感,因为它具有这两种处理方式的元素。
进一步阅读
GDAL 的作者提供了一系列 Python 示例,涵盖了多个可能对你感兴趣的高级主题。你可以在github.com/OSGeo/gdal/tree/master/gdal/swig/python/samples找到它们。
第七章:Python 和高程数据
高程数据是地理空间数据中最迷人的类型之一。它代表了许多不同类型的数据源和格式。它可以显示矢量和栅格数据的属性,从而产生独特的数据产品。高程数据可用于地形可视化、土地覆盖分类、水文建模、交通路线规划、特征提取以及许多其他用途。
您不能使用栅格和矢量数据执行所有这些选项,但由于高程数据是三维的,因为它包含x、y和z坐标,您通常可以从这些数据中获得比其他任何类型更多的信息。
在本章中,我们将涵盖以下主题:
-
使用 ASCII 网格高程数据文件进行简单的高程处理
-
创建阴影地形图
-
创建高程等高线
-
栅格化激光雷达数据
-
创建 3D 网格
在本章中,您将学习如何以栅格和矢量格式读取和写入高程数据。我们还将创建一些衍生产品。
访问 ASCII 网格文件
对于本章的大部分内容,我们将使用 ASCII 网格文件,或称 ASCIIGRID。这些文件是一种通常与高程数据相关的栅格数据。这种网格格式以文本形式存储数据,在等大小的正方形行和列中,具有简单的标题。每一行/列中的每个单元格存储一个单一的数值,它可以代表地形的一些特征,如高程、坡度或流向。其简单性使其成为一种易于使用且平台无关的栅格格式。该格式在第二章的ASCII 网格部分中描述,学习地理空间数据。
在整本书中,我们一直依赖 GDAL,在一定程度上甚至依赖 PIL 来读取和写入地理空间栅格数据,包括gdalnumeric模块,这样我们就可以将栅格数据加载到 NumPy 数组中。ASCII 网格允许我们仅使用 Python 或甚至 NumPy 来读取和写入栅格,因为它是一种简单的纯文本格式。
作为提醒,一些高程数据集使用图像格式来存储高程数据。大多数图像格式仅支持介于 0 到 255 之间的 8 位值;然而,一些格式,包括 TIFF,可以存储更大的值。
地理空间软件通常可以显示这些数据集;然而,传统的图像软件和库通常不能。为了简化,在本章中,我们将主要坚持使用 ASCII 网格格式进行数据,它既适合人类阅读也适合机器阅读,并且得到了广泛的支持。
读取网格
NumPy 具有使用其loadtxt()方法直接读取 ASCII 网格格式的功能,该方法旨在从文本文件中读取数组。前六行是标题,它不是数组的一部分。以下是一些网格标题的示例:
ncols 250
nrows 250
xllcorner 277750.0
yllcorner 6122250.0
cellsize 1.0
NODATA_value -9999
让我们看看前面代码中的每一行包含什么:
-
第 1 行包含网格中的列数,这与x轴同义。
-
第 2 行表示y轴作为行数。
-
第 3 行代表左下角的x坐标,这是以米为单位的x值最小值。
-
第 4 行是网格左下角对应的y值最小值。
-
第 5 行是栅格的单元格大小或分辨率。由于单元格是正方形,只需要一个大小值,而不是大多数地理空间栅格中单独的x和y分辨率值。
-
第 6 行是
NODATA_value,这是一个分配给任何未提供值的单元格的数字。
地理空间软件在计算中忽略这些单元格,并且通常允许为它设置特殊的显示设置,例如将其设置为黑色或透明。-9999值是一个在行业中常用的无数据占位符值,在软件中易于检测,但可以任意选择。例如,负值的标高(即水深)可能在-9999米处有有效数据,并且可能选择9999或其他值。只要这个值在标题中定义,大多数软件都不会有问题。在某些示例中,我们将使用数字零;然而,零也可以是一个有效的数据值。
numpy.loadtxt()方法包括一个名为skiprows的参数,它允许您指定在读取数组值之前要跳过的文件中的行数。
要尝试这种技术,您可以从git.io/vYapU下载一个名为myGrid.asc的示例网格文件。
因此,对于myGrid.asc,我们将使用以下代码:
myArray = numpy.loadtxt("myGrid.asc", skiprows=6)
这行代码导致myArray变量包含一个由 ASCIIGRID 格式的myGrid.asc文件派生的numpy数组。AS 文件扩展名由 ASCIIGRID 格式使用。这段代码运行得很好,但有一个问题。NumPy 允许我们跳过标题,但不能保留它。我们需要保留它,以便我们有数据的空间参考。我们还将使用它来保存此网格或创建一个新的网格。
为了解决这个问题,我们将使用 Python 内置的linecache模块来获取标题。我们可以打开文件,遍历行,将每一行存储在一个变量中,然后关闭文件。然而,linecache将解决方案简化为单行。以下行将文件的第一行读取到名为line1的变量中:
import linecache
line1 = linecache.getline("myGrid.asc", 1)
在本章的示例中,我们将使用这种技术创建一个简单的标题处理器,它可以在几行代码中将这些标题解析到 Python 变量中。现在我们知道了如何读取网格,让我们学习如何写入它们。
编写网格
在 NumPy 中编写网格与读取它们一样简单。我们使用相应的numpy.savetxt()函数将网格保存到文本文件。唯一的难点是在将数组写入文件之前,我们必须构建并添加六行标题信息。这个过程对于不同版本的 NumPy 略有不同。在任何情况下,您首先将标题作为字符串构建。如果您使用的是 NumPy 1.7 或更高版本,savetxt()方法有一个可选的参数称为 header,允许您指定一个字符串作为参数。您可以使用以下命令从命令行快速检查您的 NumPy 版本:
python -c "import numpy;print(numpy.__version__)"
1.8.2
向后兼容的方法是打开一个文件,写入标题,然后写入数组。以下是一个示例,说明如何使用 1.7 版本的 NumPy 将名为myArray的数组保存到名为myGrid.asc的 ASCIIGRID 文件中:
header = "ncols {}\n".format(myArray.shape[1])
header += "nrows {}\n".format(myArray.shape[0])
header += "xllcorner 277750.0\n"
header += "yllcorner 6122250.0\n"
header += "cellsize 1.0\n"
header += "NODATA_value -9999"
numpy.savetxt("myGrid.asc", myArray, header=header, fmt="%1.2f")
我们利用 Python 格式字符串,它允许您在字符串中放置占位符以格式化要插入的 Python 对象。{}格式变量将您引用的对象转换为字符串。在这种情况下,我们引用数组中的列数和行数。
在 NumPy 中,一个数组有两个属性:
-
大小:它返回数组中值的整数。
-
形状:它返回一个元组,分别包含行数和列数。
因此,在先前的示例中,我们使用形状属性元组将行数和列数添加到我们的 ASCII 网格的标题中。请注意,我们还为每一行添加了尾随换行符(\\n)。除非我们在脚本中更改了它们,否则没有理由更改x和y值、单元格大小或无数据值。
savetxt()方法还有一个fmt参数,它允许您使用 Python 格式字符串来指定如何写入数组值。在这种情况下,%1.2f值指定至少有一个数字且不超过两位小数的浮点数。NumPy 在 1.6 之前的向后兼容版本以相同的方式构建标题字符串,但首先创建文件句柄:
with open("myGrid.asc", "w") as f:
f.write(header)
numpy.savetxt(f, str(myArray), fmt="%1.2f")
如您将在接下来的示例中看到,仅使用 NumPy 就能生成有效的地理空间数据文件的能力非常强大。在接下来的几个示例中,我们将使用加拿大不列颠哥伦比亚省温哥华附近的一个山区 ASCIIGRID 数字高程模型(DEM)。
您可以在以下网址下载此样本的 ZIP 文件:git.io/vYwUX。
以下图像是使用 QGIS 和颜色渐变对原始 DEM 进行着色,使得低海拔值呈现深蓝色,而高海拔值呈现亮红色:

虽然我们可以从概念上理解这些数据,但这并不是一种直观的数据可视化方式。让我们看看通过创建阴影地形图是否能做得更好。
创建阴影地形图
阴影高程图以这种方式着色海拔,使得地形看起来像是在低角度光线下投射的,这创造了明亮的亮点和阴影。这种美学风格创造了一种几乎像照片一样的错觉,这很容易理解,以便我们可以理解地形的差异。重要的是要注意,这种风格实际上是一种错觉,因为光线在太阳角度方面往往物理上不准确,而海拔通常被夸大以增加对比度。
在这个例子中,我们将使用我们之前引用的 ASCII DEM 来创建另一个网格,该网格代表 NumPy 中地形阴影高程版本。这种地形非常动态,所以我们不需要夸大海拔;然而,脚本中有一个名为z的变量,可以从 1.0 增加到放大海拔。
在我们定义了所有变量,包括输入和输出文件名之后,我们将看到基于linecache模块的标题解析器,它还使用 Python 列表推导式来循环和解析然后从列表中分割成六个变量的行。我们还创建了一个名为ycell的y单元格大小,按照惯例,它只是单元格大小的倒数。如果我们不这样做,生成的网格将会转置。
注意,我们为坡度和方位角网格定义了文件名,这两个中间产品被组合起来创建最终产品。这些中间网格也被输出。它们也可以作为其他类型产品的输入。
此脚本使用 3x3 窗口方法扫描图像,并平滑这些小网格中的中心值,以有效地处理图像。它是在您计算机的内存限制内完成的。然而,因为我们使用 NumPy,我们可以通过矩阵一次处理整个数组,而不是使用一系列嵌套循环。这种技术基于一位名叫 Michal Migurski 的开发者的出色工作,他实现了 Matthew Perry 的 C++实现的巧妙 NumPy 版本,这成为了 GDAL 套件中 DEM 工具的基础。
在计算了坡度和方位角之后,它们被用来输出阴影高程图。坡度是山丘或山脉的陡峭程度,而方位角是网格单元面向的方向,以 0 到 360 度之间的度数指定。最后,所有内容都通过 NumPy 保存到磁盘上。在savetxt()方法中,我们指定一个由四个整数组成的格式字符串,因为峰值海拔高度是几千米:
- 首先,我们将导入
linecache模块来解析标题和numpy模块来进行处理:
from linecache import getline
import numpy as np
- 接下来,我们将设置所有将定义阴影高程处理方式的变量名称:
# File name of ASCII digital elevation model
source = "dem.asc"
# File name of the slope grid
slopegrid = "slope.asc"
# File name of the aspect grid
aspectgrid = "aspect.asc"
# Output file name for shaded relief
shadegrid = "relief.asc"
# Shaded elevation parameters
# Sun direction
azimuth = 315.0
# Sun angle
altitude = 45.0
# Elevation exageration
z = 1.0
# Resolution
scale = 1.0
# No data value for output
NODATA = -9999
# Needed for numpy conversions
deg2rad = 3.141592653589793 / 180.0
rad2deg = 180.0 / 3.141592653589793
- 现在我们已经设置了变量,我们可以解析标题:
# Parse the header using a loop and
# the built-in linecache module
hdr = [getline(source, i) for i in range(1, 7)]
values = [float(h.split(" ")[-1].strip()) for h in hdr]
cols, rows, lx, ly, cell, nd = values
xres = cell
yres = cell * -1
- 接下来,我们可以通过跳过标题部分使用
numpy加载实际数据:
# Load the dem into a numpy array
arr = np.loadtxt(source, skiprows=6)
- 我们将逐行逐列遍历数据,以处理它。请注意,然而,我们将跳过包含无数据值的边缘。我们将随着进行将数据分成更小的 3 x 3 像素网格,因为对于每个网格单元,我们需要看到它周围的单元:
# Exclude 2 pixels around the edges which are usually NODATA.
# Also set up structure for 3 x 3 windows to process the slope
# throughout the grid
window = []
for row in range(3):
for col in range(3):
window.append(arr[row:(row + arr.shape[0] - 2),
col:(col + arr.shape[1] - 2)])
# Process each 3x3 window in both the x and y directions
x = ((z * window[0] + z * window[3] + z * window[3] + z *
window[6]) -
(z * window[2] + z * window[5] + z * window[5] + z *
window[8])) / \
(8.0 * xres * scale)
y = ((z * window[6] + z * window[7] + z * window[7] + z *
window[8]) -
(z * window[0] + z * window[1] + z * window[1] + z *
window[2])) / \
(8.0 * yres * scale)
- 对于每个 3 x 3 的小窗口,我们将计算
slope、aspect,然后是shaded高程值:
# Calculate slope
slope = 90.0 - np.arctan(np.sqrt(x * x + y * y)) * rad2deg
# Calculate aspect
aspect = np.arctan2(x, y)
# Calculate the shaded relief
shaded = np.sin(altitude * deg2rad) * np.sin(slope * deg2rad) + \
np.cos(altitude * deg2rad) * np.cos(slope * deg2rad) * \
np.cos((azimuth - 90.0) * deg2rad - aspect)
- 接下来,我们需要将每个值缩放到 0-255 之间,以便它可以作为一个图像来查看:
# Scale values from 0-1 to 0-255
shaded = shaded * 255
- 现在,我们必须重建我们的标题,因为我们忽略了无数据值的外边缘,并且我们的数据集更小了:
# Rebuild the new header
header = "ncols {}\n".format(shaded.shape[1])
header += "nrows {}\n".format(shaded.shape[0])
header += "xllcorner {}\n".format(lx + (cell * (cols -
shaded.shape[1])))
header += "yllcorner {}\n".format(ly + (cell * (rows -
shaded.shape[0])))
header += "cellsize {}\n".format(cell)
header += "NODATA_value {}\n".format(NODATA)
- 接下来,我们将任何无数据值设置为我们在变量开始时设置的选择的无数据值:
# Set no-data values
for pane in window:
slope[pane == nd] = NODATA
aspect[pane == nd] = NODATA
shaded[pane == nd] = NODATA
- 我们将分别保存坡度和方向网格,以便我们可以在以后查看并理解阴影高程是如何创建的:
# Open the output file, add the header, save the slope grid
with open(slopegrid, "wb") as f:
f.write(bytes(header, "UTF-8")
np.savetxt(f, slope, fmt="%4i")
# Open the output file, add the header, save the aspectgrid
with open(aspectgrid, "wb") as f:
f.write(bytes(header, "UTF-8")
np.savetxt(f, aspect, fmt="%4i")
# Open the output file, add the header, save the relief grid
with open(shadegrid, "wb") as f:
f.write(bytes(header, 'UTF-8'))
np.savetxt(f, shaded, fmt="%4i")
如果我们将输出的阴影高程网格加载到 QGIS 中,并将样式指定为拉伸图像到最小和最大值,我们将看到以下图像:

如果 QGIS 要求您指定投影,数据是 EPSG:3157。您还可以在第四章的“安装 GDAL”部分中讨论的 FWTools OpenEV 应用程序中打开图像,该应用程序将自动拉伸图像以实现最佳查看。
如您所见,前面的图像比我们最初检查的伪彩色表示更容易理解。接下来,让我们看看用于创建阴影高程的坡度栅格:

坡度显示了数据集中从高点到低点的所有方向上的高程逐渐下降。坡度是许多类型的水文模型的有用输入:

方向显示了从单元格到其邻居的下降变化的最大速率。如果您将方向图像与阴影高程图像进行比较,您将看到方向图像中的红色和灰色值对应于阴影高程中的阴影。因此,坡度主要负责将 DEM 转换为地形高程,而方向负责阴影。
现在我们能够以有用的方式显示数据,让我们看看我们是否也可以从中创建其他数据。
创建高程等高线
等高线是数据集中相同高程的等值线。等高线通常以间隔步进,以创建一种直观的方式来表示高程数据,无论是视觉上还是数值上,都使用资源高效的矢量数据集。现在,让我们看看另一种使用等高线更好地可视化高程的方法。
输入用于在 DEM 中生成等高线,输出是一个 shapefile。用于生成等高线的算法(Marching Squares:en.wikipedia.org/wiki/Marching_squares)相当复杂,并且使用 NumPy 的线性代数实现非常困难。在这种情况下,我们的解决方案是回退到 GDAL 库,该库通过 Python API 提供了等高线方法。实际上,这个脚本的大部分内容只是设置输出 shapefile 所需的 OGR 库代码。实际的等高线生成是一个名为 gdal.ContourGenerate() 的方法调用。在这个调用之前,有一些注释定义了方法参数。其中最重要的如下:
-
contourInterval:这是等高线之间的距离,以数据集单位表示。 -
contourBase:这是等高线的起始海拔。 -
fixedLevelCount:这指定了等高线的固定数量,而不是距离。 -
idField:这是必需的 shapefiledbf字段的名称,通常称为 ID。 -
elevField:这是必需的 shapefiledbf字段名称,用于海拔值,并在地图标注中很有用。
您应该从第四章的 安装 GDAL 部分安装 GDAL 和 OGR。我们将实施以下步骤:
-
首先,我们将定义输入 DEM 文件名。
-
然后,我们将输出 shapefile 的名称。
-
接下来,我们将使用 OGR 创建 shapefile 数据源。
-
然后,我们将获取 OGR 图层。
-
接下来,我们将打开 DEM。
-
最后,我们将在 OGR 图层上生成等高线。
让我们看看前面步骤的代码表示:
- 首先,我们加载
gdal和ogr库来处理数据:
import gdal
import ogr
- 然后我们将设置一个用于文件名的变量:
# Elevation DEM
source = "dem.asc"
- 接下来,我们将使用 OGR 创建我们输出 shapefile 的开始部分:
# Output shapefile
target = "contour"
ogr_driver = ogr.GetDriverByName("ESRI Shapefile")
ogr_ds = ogr_driver.CreateDataSource(target + ".shp")
ogr_lyr = ogr_ds.CreateLayer(target,
# wkbLineString25D is the type code for geometry with a z
# elevation value.
geom_type=ogr.wkbLineString25D)
field_defn = ogr.FieldDefn("ID" ogr.OFTInteger)
ogr_lyr.CreateField(field_defn)
field_defn = ogr.FieldDefn("ELEV" ogr.OFTReal)
ogr_lyr.CreateField(field_defn)
- 然后,我们将创建一些等高线:
# gdal.ContourGenerate() arguments
# Band srcBand,
# double contourInterval,
# double contourBase,
# double[] fixedLevelCount,
# int useNoData,
# double noDataValue,
# Layer dstLayer,
# int idField,
# int elevField
ds = gdal.Open(source)
# EPGS:3157
gdal.ContourGenerate(ds.GetRasterBand(1), 400, 10, [], 0, 0, ogr_lyr, 0, 1))
ogr_ds = None
- 现在,让我们使用我们在第四章的 PNGCanvas 部分中介绍的
pngcanvas来绘制我们刚刚创建的等高线 shapefile。请参阅第四章,地理空间 Python 工具箱:
import shapefile
import pngcanvas
# Open the contours
r = shapefile.Reader("contour.shp")
# Setup the world to pixels conversion
xdist = r.bbox[2] - r.bbox[0]
ydist = r.bbox[3] - r.bbox[1]
iwidth = 800
iheight = 600
xratio = iwidth/xdist
yratio = iheight/ydist
contours = []
# Loop through all shapes
for shape in r.shapes():
# Loop through all parts
for i in range(len(shape.parts)):
pixels = []
pt = None
if i < len(shape.parts) - 1:
pt = shape.points[shape.parts[i]:shape.parts[i+1]]
else:
pt = shape.points[shape.parts[i]:]
for x, y in pt:
px = int(iwidth - ((r.bbox[2] - x) * xratio))
py = int((r.bbox[3] - y) * yratio)
pixels.append([px, py])
contours.append(pixels)
# Set up the output canvas
canvas = pngcanvas.PNGCanvas(iwidth, iheight)
# PNGCanvas accepts rgba byte arrays for colors
red = [0xff, 0, 0, 0xff]
canvas.color = red
# Loop through the polygons and draw them
for c in contours:
canvas.polyline(c)
# Save the image
with open("contours.png", "wb") as f:
f.write(canvas.dump())
我们最终会得到以下图像:

如果我们将我们的阴影高程 ASCIIGRID 和 shapefile 带入 GIS,例如 QGIS,我们可以创建一个简单的地形图,如下所示。您可以使用在脚本中指定的海拔(即 ELEV)dbf 字段来标注等高线的高度:

在这些 NumPy 网格示例中使用的技术为各种高程产品提供了构建块。接下来,我们将处理一种最复杂的高程数据类型:激光雷达数据。
处理激光雷达数据
LIDAR代表光探测与测距。它与基于雷达的图像类似,但使用有限的光束,每秒击中地面数十万次,以收集大量非常精细的(x, y, z)位置,以及时间和强度。强度值是 LIDAR 与其他数据类型真正区分开来的地方。例如,建筑物的沥青屋顶可能与附近树木的顶部处于相同的海拔高度,但强度值将不同。就像遥感一样,多光谱卫星图像中的辐射值允许我们建立分类库。LIDAR 数据的强度值允许我们对 LIDAR 数据进行分类和着色。
LIDAR 的高体积和精度实际上使其难以使用。LIDAR 数据集被称为点云,因为数据集的形状通常不规则,因为数据是三维的,并且有离群点。没有很多软件包能够有效地可视化点云。
此外,有限点的非规则形状很难交互,即使我们使用适当的软件。
由于这些原因,LIDAR 数据上最常用的操作之一是将数据投影并重新采样到规则网格。我们将使用一个小型的 LIDAR 数据集来完成这项工作。此数据集未压缩时大约为 7 MB,包含超过 600,000 个点。数据捕捉了一些易于识别的特征,如建筑物、树木和停车场中的汽车。您可以从git.io/vOERW下载压缩后的数据集。
文件格式是 LIDAR 特有的非常常见的二进制格式,称为LAS,代表激光。将此文件解压缩到您的工作目录。为了读取此格式,我们将使用一个纯 Python 库,称为laspy。您可以使用以下命令安装 Python 版本 3.7:
pip install http://git.io/vOER9
安装了laspy后,我们就准备好从 LIDAR 创建网格了。
从 LIDAR 数据创建网格
此脚本相当直接。我们遍历 LIDAR 数据中的(x, y)点位置,并将它们投影到我们的一个平方米大小的网格上。由于 LIDAR 数据的精度,我们最终会在单个单元格中结束多个点。我们将这些点平均以创建一个共同的海拔值。我们还要处理的一个问题是数据丢失。每次重新采样数据时,您都会丢失信息。
在这种情况下,我们最终会在栅格中间出现NODATA空洞。为了处理这个问题,我们将使用周围单元格的平均值填充这些空洞,这是一种插值的形式。我们只需要两个模块,这两个模块都可在 PyPI 上找到,如下面的代码所示:
from laspy.file import File
import numpy as np
# Source LAS file
source = "lidar.las"
# Output ASCII DEM file
target = "lidar.asc"
# Grid cell size (data units)
cell = 1.0
# No data value for output DEM
NODATA = 0
# Open LIDAR LAS file
las = File(source, mode="r")
# xyz min and max
min = las.header.min
max = las.header.max
# Get the x axis distance in meters
xdist = max[0] - min[0]
# Get the y axis distance in meters
ydist = max[1] - min[1]
# Number of columns for our grid
cols = int(xdist) / cell
# Number of rows for our grid
rows = int(ydist) / cell
cols += 1
rows += 1
# Track how many elevation
# values we aggregate
count = np.zeros((rows, cols)).astype(np.float32)
# Aggregate elevation values
zsum = np.zeros((rows, cols)).astype(np.float32)
# Y resolution is negative
ycell = -1 * cell
# Project x, y values to grid
projx = (las.x - min[0]) / cell
projy = (las.y - min[1]) / ycell
# Cast to integers and clip for use as index
ix = projx.astype(np.int32)
iy = projy.astype(np.int32)
# Loop through x, y, z arrays, add to grid shape,
# and aggregate values for averaging
for x, y, z in np.nditer([ix, iy, las.z]):
count[y, x] += 1
zsum[y, x] += z
# Change 0 values to 1 to avoid numpy warnings,
# and NaN values in array
nonzero = np.where(count > 0, count, 1)
# Average our z values
zavg = zsum / nonzero
# Interpolate 0 values in array to avoid any
# holes in the grid
mean = np.ones((rows, cols)) * np.mean(zavg)
left = np.roll(zavg, -1, 1)
lavg = np.where(left > 0, left, mean)
right = np.roll(zavg, 1, 1)
ravg = np.where(right > 0, right, mean)
interpolate = (lavg + ravg) / 2
fill = np.where(zavg > 0, zavg, interpolate)
# Create our ASCII DEM header
header = "ncols {}\n".format(fill.shape[1])
header += "nrows {}\n".format(fill.shape[0])
header += "xllcorner {}\n".format(min[0])
header += "yllcorner {}\n".format(min[1])
header += "cellsize {}\n".format(cell)
header += "NODATA_value {}\n".format(NODATA)
# Open the output file, add the header, save the array
with open(target, "wb") as f:
f.write(bytes(header, 'UTF-8'))
# The fmt string ensures we output floats
# that have at least one number but only
# two decimal places
np.savetxt(f, fill, fmt="%1.2f")
我们脚本的输出结果是一个 ASCIIGRID,当在 OpenEV 中查看时,看起来如下所示。高海拔地区较亮,而低海拔地区较暗。即使以这种形式,您也可以看到建筑物、树木和汽车:

如果我们分配一个热图颜色渐变,颜色会给你一个更清晰的地面高度差异感:

那么,如果我们运行这个输出 DEM 通过我们之前提到的阴影立体图脚本会发生什么?直边建筑和斜坡山之间的差异很大。如果您将阴影立体图脚本中的输入和输出名称更改为处理 LIDAR DEM,我们得到以下坡度结果:

山地地形柔和的起伏坡度在图像中简化为主要特征的轮廓。在视向图像中,变化非常剧烈,且距离很短,因此输出图像看起来非常混乱,如下面的截图所示:

尽管这些图像与较粗糙但相对平滑的山地版本之间存在差异,但我们仍然得到了一个非常漂亮的阴影立体图,从视觉上看类似于黑白照片:

现在我们知道了如何处理 LIDAR 数据,让我们学习如何使用 Python 可视化它。
使用 PIL 可视化 LIDAR 数据
本章之前的部分 DEM 图像使用 QGIS 和 OpenEV 进行了可视化。我们还可以通过引入我们在前几章中没有使用的 Python 图像库(PIL)的一些新功能,在 Python 中创建输出图像。
在本例中,我们将使用 PIL.ImageOps 模块,该模块包含直方图均衡化和自动对比度增强的功能。我们将使用 PIL 的 fromarray() 方法从 numpy 导入数据。让我们看看如何通过以下代码帮助我们将输出与本章中展示的桌面 GIS 程序的输出尽可能接近:
import numpy as np
try:
import Image
import ImageOps
except ImportError:
from PIL import Image, ImageOps
# Source gridded LIDAR DEM file
source = "lidar.asc"
# Output image file
target = "lidar.bmp"
# Load the ASCII DEM into a numpy array
arr = np.loadtxt(source, skiprows=6)
# Convert array to numpy image
im = Image.fromarray(arr).convert("RGB")
# Enhance the image:
# equalize and increase contrast
im = ImageOps.equalize(im)
im = ImageOps.autocontrast(im)
# Save the image
im.save(target)
如您所见,在以下图像中,增强后的阴影立体图比之前的版本有更清晰的立体感:

现在,让我们给我们的阴影立体图上色。我们将使用内置的 Python colorsys 模块进行颜色空间转换。通常,我们指定颜色为 RGB 值。然而,为了创建热图方案的色阶,我们将使用 HSV(代表 色调、饱和度和亮度)值来生成我们的颜色。
HSV 的优点是您可以调整 H 值,使其在色轮上的度数在 0 到 360 之间。使用单个色调值允许您使用线性渐变方程,这比处理三个单独的 RGB 值的组合要容易得多。以下图像来自在线杂志 Qt Quarterly,展示了 HSV 颜色模型:

colorsys 模块允许你在 HSV 和 RGB 值之间切换。该模块返回 RGB 值的百分比,然后必须将每个颜色的百分比映射到 0-255 的刻度。
在以下代码中,我们将把 ASCII DEM 转换为 PIL 图像,构建我们的调色板,将调色板应用于灰度图像,并保存图像:
import numpy as np
try:
import Image
import ImageOps
except:
from PIL import Image, ImageOps
import colorsys
# Source LIDAR DEM file
source = "lidar.asc"
# Output image file
target = "lidar.bmp"
# Load the ASCII DEM into a numpy array
arr = np.loadtxt(source, skiprows=6)
# Convert the numpy array to a PIL image.
# Use black and white mode so we can stack
# three bands for the color image.
im = Image.fromarray(arr).convert('L')
# Enhance the image
im = ImageOps.equalize(im)
im = ImageOps.autocontrast(im)
# Begin building our color ramp
palette = []
# Hue, Saturation, Value
# color space starting with yellow.
h = .67
s = 1
v = 1
# We'll step through colors from:
# blue-green-yellow-orange-red.
# Blue=low elevation, Red=high-elevation
step = h / 256.0
# Build the palette
for i in range(256):
rp, gp, bp = colorsys.hsv_to_rgb(h, s, v)
r = int(rp * 255)
g = int(gp * 255)
b = int(bp * 255)
palette.extend([r, g, b])
h -= step
# Apply the palette to the image
im.putpalette(palette)
# Save the image
im.save(target)
上述代码生成以下图像,高海拔以暖色调表示,低海拔以冷色调表示:

在这张图像中,我们实际上得到了比默认 QGIS 版本更多的变化。我们可以通过一个平滑算法来改善这张图像,该算法会在颜色相遇的地方混合颜色并使图像视觉上更柔和。如您所见,我们的颜色渐变范围从冷色调到暖色调,随着高程变化而增加。
创建不规则三角网
以下是我们迄今为止最复杂的示例。不规则三角网(TIN)是点数据集在点向量表面上的矢量表示,这些点以三角形的形式连接。一个算法确定哪些点是准确表示地形所绝对必要的,而不是像栅格那样,栅格在给定区域内存储固定数量的单元格,并且可能在相邻单元格中重复高程值,这些值可能更有效地存储为多边形。
TIN 比栅格更有效地在飞行中进行重采样,栅格在使用 GIS 中的 TIN 时需要更少的计算机内存和处理能力。最常见的 TIN 类型是基于Delaunay 三角剖分,它包括所有点而没有冗余三角形。
Delaunay 三角剖分非常复杂。我们将使用由 Bill Simons 编写的纯 Python 库,作为 Steve Fortune 的 Delaunay 三角剖分算法的一部分,名为voronoi.py,来计算我们的 LIDAR 数据中的三角形。您可以从git.io/vOEuJ下载脚本到您的当前工作目录或site-packages目录。
此脚本读取 LAS 文件,生成三角形,遍历它们,并输出一个 shapefile。对于此示例,我们将使用我们 LIDAR 数据的裁剪版本以减少处理区域。如果我们运行包含 600,000 多个点的整个数据集,脚本将运行数小时并生成超过五十万个三角形。您可以从以下 URL 下载裁剪的 LIDAR 数据集作为 ZIP 文件:git.io/vOE62。
由于以下示例的计算密集型特性,脚本运行时会打印出几个状态消息,该示例可能需要几分钟才能完成。我们将以PolygonZ 类型存储三角形,这允许顶点具有z高程值。解压缩 LAS 文件并运行以下代码以生成名为mesh.shp的 shapefile:
- 首先,我们导入我们的库:
import pickle
import os
import time
import math
import numpy as np
import shapefile
from laspy.file import File
# voronoi.py for Python 3: pip install http://git.io/vOEuJ
import voronoi
- 接下来,我们定义我们的 LIDAR 文件的位置和名称、我们的目标输出文件以及我们的 pickle 文件:
# Source LAS file
source = "clippedLAS.las"
# Output shapefile
target = "mesh"
# Triangles pickle archive
archive = "triangles.p"
- 现在,我们将创建一个由
voronoi模块需要的点类:
class Point:
"""Point class required by the voronoi module"""
def __init__(self, x, y):
self.px = x
self.py = y
def x(self):
return self.px
def y(self):
return self.py
- 接下来,我们将创建一个三角形数组来跟踪为网格创建的三角形:
# The triangle array holds tuples
# 3 point indices used to retrieve the points.
# Load it from a pickle
# file or use the voronoi module
# to create the triangles.
triangles = None
- 接下来,我们需要打开我们的 LIDAR 文件并提取点:
# Open LIDAR LAS file
las = File(source, mode="r")
else:
# Open LIDAR LAS file
las = File(source, mode="r")
points = []
print("Assembling points...")
# Pull points from LAS file
for x, y in np.nditer((las.x, las.y)):
points.append(Point(x, y))
print("Composing triangles...")
- 现在,我们可以对点进行 Delaunay 计算来构建三角形:
# Delaunay Triangulation
triangles = voronoi.computeDelaunayTriangulation(points)
- 如果我们再次运行这个脚本,我们将三角形存入 pickle 存档以节省时间:
# Save the triangles to save time if we write more than
# one shapefile.
f = open(archive, "wb")
pickle.dump(triangles, f, protocol=2)
f.close()
- 接下来,我们可以创建一个 shapefile
Writer对象,通过设置必要的字段来开始创建我们的输出 shapefile:
print("Creating shapefile...")
# PolygonZ shapefile (x, y, z, m)
w = shapefile.Writer(target, shapefile.POLYGONZ)
w.field("X1", "C", "40")
w.field("X2", "C", "40")
w.field("X3", "C", "40")
w.field("Y1", "C", "40")
w.field("Y2", "C", "40")
w.field("Y3", "C", "40")
w.field("Z1", "C", "40")
w.field("Z2", "C", "40")
w.field("Z3", "C", "40")
tris = len(triangles)
- 然后,我们遍历三角形并创建网格:
# Loop through shapes and
# track progress every 10 percent
last_percent = 0
for i in range(tris):
t = triangles[i]
percent = int((i/(tris*1.0))*100.0)
if percent % 10.0 == 0 and percent > last_percent:
last_percent = percent
print("{} % done - Shape {}/{} at {}".format(percent,
i, tris, time.asctime()))
part = []
x1 = las.x[t[0]]
y1 = las.y[t[0]]
z1 = las.z[t[0]]
x2 = las.x[t[1]]
y2 = las.y[t[1]]
z2 = las.z[t[1]]
x3 = las.x[t[2]]
y3 = las.y[t[2]]
z3 = las.z[t[2]]
- 接下来,我们可以消除任何极端长的线段,这些是库的错误计算:
# Check segments for large triangles
# along the convex hull which is a common
# artifact in Delaunay triangulation
max = 3
if math.sqrt((x2-x1)**2+(y2-y1)**2) > max:
continue
if math.sqrt((x3-x2)**2+(y3-y2)**2) > max:
continue
if math.sqrt((x3-x1)**2+(y3-y1)**2) > max:
continue
part.append([x1, y1, z1, 0])
part.append([x2, y2, z2, 0])
part.append([x3, y3, z3, 0])
w.poly(parts=[part])
w.record(x1, x2, x3, y1, y2, y3, z1, z2, z3)
print("Saving shapefile...")
- 最后,我们可以保存输出 shapefile:
w.close()
print("Done.")
以下图像显示了 TIN 在着色 LIDAR 数据上的放大版本:

网格从点云中提供了一种高效、连续的表面,这通常比处理点云本身更容易。
摘要
高程数据通常可以提供完整的分析数据集和派生产品,无需其他数据。在本章中,你学习了如何仅使用 NumPy 读取和写入 ASCII 网格。你还学习了如何创建阴影地形图、坡度网格和方位网格。我们使用 GDAL 库的一个不为人知的功能——等高线,通过 Python 创建高程等高线。
接下来,我们将 LIDAR 数据转换成易于操作的 ASCII 网格。我们尝试了不同的方法来使用 PIL 可视化 LIDAR 数据。最后,我们将 LIDAR 点云转换成多边形的 3D 形状文件,从而创建一个 3D 表面或 TIN。这些是用于交通规划、建设规划、水文排水建模、地质勘探等的地形分析工具。
在下一章中,我们将结合前三章的内容,进行一些高级建模,并实际创建一些信息产品。
进一步阅读
你可以在以下链接找到一些关于 Python 和高程数据的额外教程: www.earthdatascience.org/tutorials/python/elevation/。
第三部分:实用地理空间处理技术
本节属于高级水平,它将需要你之前学到的所有技能。它从学习如何创建地理空间模型来回答特定问题开始。此外,它将展示一些构建地理空间模型的技术,以及如何利用可视化概念来预测未来。接下来,我们将转向访问和处理实时数据。在本节的最后,我们将结合之前章节所学的内容,实现一个基于 GPS 数据和地理标记照片创建户外跑步或徒步报告的系统。
本节包括以下章节:
-
第八章,高级地理空间 Python 建模
-
第九章,实时数据
-
第十章,整合所有内容
第八章:高级地理空间 Python 建模
在本章中,我们将基于我们已经学习的数据处理概念来创建一些全规模的信息产品。之前介绍的数据处理方法很少能单独提供答案。您将这些数据处理方法结合起来,从多个处理后的数据集中构建一个地理空间模型。地理空间模型是现实世界某个方面的简化表示,有助于我们回答一个或多个关于项目或问题的疑问。在本章中,我们将介绍一些在农业、应急管理、物流和其他行业中常用的重要地理空间算法。
我们将创建以下产品:
-
作物健康图
-
洪水淹没模型
-
彩色等高线
-
地形路由图
-
街道路由图
-
包含地理定位照片的 shapefile
虽然这些产品是针对特定任务的,但用于创建它们的算法在地理空间分析中得到了广泛应用。在本章中,我们将涵盖以下主题:
-
创建归一化植被指数(NVDI)
-
创建洪水淹没模型
-
创建彩色等高线
-
执行最低成本路径分析
-
将路线转换为 shapefile
-
沿街道路由
-
地理定位照片
-
计算卫星图像云覆盖率
本章中的示例比前几章更长、更复杂。因此,有更多的代码注释来使程序更容易理解。我们还将在这些建议中使用更多函数。在前几章中,为了清晰起见,通常避免使用函数,但这些示例足够复杂,某些函数可以使代码更容易阅读。这些示例是您作为地理空间分析师在工作中会使用的实际过程。
技术要求
对于本章,需要满足以下要求:
-
版本:Python 3.6 或更高版本
-
RAM:最小 6 GB(Windows),8 GB(macOS);推荐 8 GB
-
存储:最小 7200 RPM SATA,可用空间 20 GB,推荐使用具有 40 GB 可用空间的 SSD。
-
处理器:最小 Intel Core i3 2.5 GHz,推荐 Intel Core i5。
创建归一化植被指数
我们的第一个示例将是一个归一化植被指数(NVDI)。NDVIs 用于显示感兴趣区域内植物的相对健康状况。NDVI 算法使用卫星或航空影像通过突出植物中的叶绿素密度来显示相对健康状况。NDVIs 仅使用红色和近红外波段。NDVI 的公式如下:
NDVI = (Infrared – Red) / (Infrared + Red)
本分析的目标是首先生成一个包含红外和红波段的彩色图像,最终使用七个类别生成伪彩色图像,这些类别将健康的植物着色为深绿色,不太健康的植物着色为浅绿色,裸土为棕色。
由于健康指数是相对的,因此定位感兴趣区域非常重要。您可以对整个地球进行相对指数计算,但像撒哈拉沙漠这样的低植被极端地区和像亚马逊雨林这样的密集森林地区会扭曲中等植被范围的结果。然而,尽管如此,气候科学家通常会创建全球 NDVI 来研究全球趋势。尽管如此,最常见的应用是针对管理区域,例如森林或农田,就像这个例子一样。
我们将从密西西比三角洲的一个单一农田的分析开始。为此,我们将从一个相当大的区域的多光谱图像开始,并使用 shapefile 来隔离单个农田。以下截图中的图像是我们的大区域,感兴趣的区域用黄色突出显示:

您可以从git.io/v3fS9下载此图像和农田的 shapefile 作为 ZIP 文件。
在这个例子中,我们将使用 GDAL、OGR、gdal_array/numpy和Python 图像库(PIL)来裁剪和处理数据。在本章的其他示例中,我们只需使用简单的 ASCII 网格和 NumPy。由于我们将使用 ASCII 高程网格,因此不需要 GDAL。在所有示例中,脚本都遵循以下约定:
-
导入库。
-
定义函数。
-
定义全局变量,例如文件名。
-
执行分析。
-
保存输出。
我们对作物健康示例的方法分为两个脚本。第一个脚本创建索引图像,这是一个灰度图像。第二个脚本对索引进行分类并输出彩色图像。在这个第一个脚本中,我们将执行以下步骤来创建索引图像:
-
读取红外波段。
-
读取农田边界 shapefile。
-
将 shapefile 栅格化成图像。
-
将形状文件图像转换为 NumPy 数组。
-
使用 NumPy 数组将红波段裁剪到农田。
-
对红外波段也执行相同的操作。
-
使用波段数组在 NumPy 中执行 NDVI 算法。
-
使用
gdal_array将结果索引算法保存到 GeoTIFF 文件中。
我们将分节讨论此脚本,以便更容易理解。代码注释也会告诉你每一步正在发生什么。
设置框架
设置框架将帮助我们导入所需的模块,并设置我们将用于先前指令步骤 1 到 5 的函数。imageToArray()函数将 PIL 图像转换为 NumPy 数组,并依赖于gdal_array和 PIL 模块。world2Pixel()函数将地理坐标转换为目标图像的像素坐标。此函数使用gdal模块提供的地理参考信息。copy_geo()函数将源图像的地理参考信息复制到目标数组,但考虑到我们在裁剪图像时创建的偏移。这些函数相当通用,可以在各种不同的遥感过程中发挥作用,而不仅仅是本例:
- 首先,我们导入我们的库:
import gdal
from osgeo import gdal
from osgeo import gdal_array
from osgeo import ogr
try:
import Image
import ImageDraw
except ImportError:
from PIL import Image, ImageDraw
- 然后,我们需要一个函数将图像转换为
numpy数组:
def imageToArray(i):
"""
Converts a Python Imaging Library
array to a gdal_array image.
"""
a = gdal_array.numpy.fromstring(i.tobytes(), 'b')
a.shape = i.im.size[1], i.im.size[0]
return a
- 现在,我们将设置一个函数来将坐标转换为图像像素:
def world2Pixel(geoMatrix, x, y):
"""
Uses a gdal geomatrix (gdal.GetGeoTransform())
to calculate the pixel location of a
geospatial coordinate
"""
ulX = geoMatrix[0]
ulY = geoMatrix[3]
xDist = geoMatrix[1]
yDist = geoMatrix[5]
rtnX = geoMatrix[2]
rtnY = geoMatrix[4]
pixel = int((x - ulX) / xDist)
line = int((ulY - y) / abs(yDist))
return (pixel, line)
- 最后,我们将创建一个函数来复制图像的地理元数据:
def copy_geo(array, prototype=None, xoffset=0, yoffset=0):
"""Copy geotransfrom from prototype dataset to array but account
for x, y offset of clipped array."""
ds = gdal_array.OpenArray(array)
prototype = gdal.Open(prototype)
gdal_array.CopyDatasetInfo(prototype, ds,
xoff=xoffset, yoff=yoffset)
return ds
下一步是加载数据,我们将在下一节中检查。
加载数据
在本节中,我们使用gdal_array加载农田的源图像,将其直接转换为 NumPy 数组。我们还定义了输出图像的名称,它将是ndvi.tif。本节中一个有趣的部分是,我们使用gdal模块而不是gdal_array再次加载源图像。
第二次调用是为了捕获通过gdal而不是gdal_array提供的图像的地理参考数据。幸运的是,gdal仅在需要时加载栅格数据,因此这种方法避免了将整个数据集两次加载到内存中。一旦我们有了多维 NumPy 数组的数据,我们就将红色和红外波段分离出来,因为它们都将用于 NDVI 方程:
# Multispectral image used
# to create the NDVI. Must
# have red and infrared
# bands
source = "farm.tif"
# Output geotiff file name
target = "ndvi.tif"
# Load the source data as a gdal_array array
srcArray = gdal_array.LoadFile(source)
# Also load as a gdal image to
# get geotransform info
srcImage = gdal.Open(source)
geoTrans = srcImage.GetGeoTransform()
# Red and infrared (or near infrared) bands
r = srcArray[1]
ir = srcArray[2]
现在我们已经加载了数据,我们可以将我们的 shapefile 转换为栅格数据。
将 shapefile 栅格化
本节开始裁剪的过程。然而,第一步是将我们打算分析的特定区域的边界 shapefile 栅格化。该区域位于更大的field.tif卫星图像内。换句话说,我们将它从矢量数据转换为栅格数据。但我们还希望在转换时填充多边形,以便它可以作为图像掩码使用。掩码中的像素将与红色和红外数组中的像素相关联。
任何在掩码之外的像素将被转换为NODATA像素,这样它们就不会作为 NDVI 的一部分进行处理。为了进行这种相关性,我们需要将实心多边形转换为 NumPy 数组,就像栅格波段一样。这种方法将确保我们的 NDVI 计算仅限于农田。
将 shapefile 多边形转换为填充多边形作为 NumPy 数组的简单方法是将它作为 PIL 图像中的多边形绘制出来,填充该多边形,然后使用 PIL 和 NumPy 中现有的方法将其转换为 NumPy 数组,这些方法允许进行该转换。
在这个例子中,我们使用ogr模块来读取 shapefile,因为我们已经有了 GDAL。但,我们也可以同样容易地使用 PyShp 来读取 shapefile。如果我们的农田图像可用作 ASCII 网格,我们可以完全避免使用gdal、gdal_array和ogr模块:
- 首先,我们打开我们的 shapefile 并选择唯一的图层:
# Clip a field out of the bands using a
# field boundary shapefile
# Create an OGR layer from a Field boundary shapefile
field = ogr.Open("field.shp")
# Must define a "layer" to keep OGR happy
lyr = field.GetLayer("field")
- 只有一个多边形,所以我们将获取该特征:
# Only one polygon in this shapefile
poly = lyr.GetNextFeature()
- 现在我们将图层范围转换为图像像素坐标:
# Convert the layer extent to image pixel coordinates
minX, maxX, minY, maxY = lyr.GetExtent()
ulX, ulY = world2Pixel(geoTrans, minX, maxY)
lrX, lrY = world2Pixel(geoTrans, maxX, minY)
- 然后,我们计算新图像的像素大小:
# Calculate the pixel size of the new image
pxWidth = int(lrX - ulX)
pxHeight = int(lrY - ulY)
- 接下来,我们创建一个正确大小的空白图像:
# Create a blank image of the correct size
# that will serve as our mask
clipped = gdal_array.numpy.zeros((3, pxHeight, pxWidth),
gdal_array.numpy.uint8)
- 现在,我们准备好使用边界框裁剪红光和红外波段:
# Clip red and infrared to new bounds.
rClip = r[ulY:lrY, ulX:lrX]
irClip = ir[ulY:lrY, ulX:lrX]
- 接下来,我们为图像创建地理参考信息:
# Create a new geomatrix for the image
geoTrans = list(geoTrans)
geoTrans[0] = minX
geoTrans[3] = maxY
- 然后,我们可以准备将点映射到像素以创建我们的掩码图像:
# Map points to pixels for drawing
# the field boundary on a blank
# 8-bit, black and white, mask image.
points = []
pixels = []
# Grab the polygon geometry
geom = poly.GetGeometryRef()
pts = geom.GetGeometryRef(0)
- 我们遍历所有点特征并存储它们的x和y值:
# Loop through geometry and turn
# the points into an easy-to-manage
# Python list
for p in range(pts.GetPointCount()):
points.append((pts.GetX(p), pts.GetY(p)))
- 现在,我们将点转换为像素位置:
# Loop through the points and map to pixels.
# Append the pixels to a pixel list
for p in points:
pixels.append(world2Pixel(geoTrans, p[0], p[1]))
- 接下来,我们创建一个新的图像,该图像将作为我们的掩码图像:
# Create the raster polygon image as a black and white 'L' mode
# and filled as white. White=1
rasterPoly = Image.new("L", (pxWidth, pxHeight), 1)
- 现在我们可以将我们的多边形栅格化:
# Create a PIL drawing object
rasterize = ImageDraw.Draw(rasterPoly)
# Dump the pixels to the image
# as a polygon. Black=0
rasterize.polygon(pixels, 0)
- 最后,我们可以将我们的掩码转换为
numpy数组:
# Hand the image back to gdal/gdal_array
# so we can use it as an array mask
mask = imageToArray(rasterPoly)
现在我们已经将 shapefile 转换为掩码图像,我们可以裁剪波段。
裁剪波段
现在我们有了我们的图像掩码,我们可以将红光和红外波段裁剪到掩码的边界。为此过程,我们使用 NumPy 的choose()方法,该方法将掩码单元格与栅格波段单元格相关联并返回该值,或返回0。结果是一个新的数组,裁剪到掩码,但带有来自栅格波段的关联值:
# Clip the red band using the mask
rClip = gdal_array.numpy.choose(mask,
(rClip, 0)).astype(gdal_array.numpy.uint8)
# Clip the infrared band using the mask
irClip = gdal_array.numpy.choose(mask,
(irClip, 0)).astype(gdal_array.numpy.uint8)
现在我们只得到了我们想要的数据,因此我们可以应用我们的 NDVI 相对植被健康公式。
使用 NDVI 公式
我们创建 NDVI 的最终过程是执行方程式红外 - 红光/红外 + 红光。我们执行的第一步是消除 NumPy 中可能发生的任何非数字(NaN)值,也称为NaN。在我们保存输出之前,我们将任何 NaN 值转换为0。我们将输出保存为ndvi.tif,这将是下一个脚本的输入,以便对 NDVI 进行分类和着色如下:
- 首先,我们将忽略来自
numpy的任何警告,因为我们将在边缘附近遇到一些错误:
# We don't care about numpy warnings
# due to NaN values from clipping
gdal_array.numpy.seterr(all="ignore")
- 现在,我们可以执行我们的 NDVI 公式:
# NDVI equation: (infrared - red) / (infrared + red)
# *1.0 converts values to floats,
# +1.0 prevents ZeroDivisionErrors
ndvi = 1.0 * ((irClip - rClip) / (irClip + rClip + 1.0))
- 如果有任何 NaN 值,我们将它们转换为零:
# Convert any NaN values to 0 from the final product
ndvi = gdal_array.numpy.nan_to_num(ndvi)
- 最后,我们保存我们的完成 NDVI 图像:
# Save the ndvi as a GeoTIFF and copy/adjust
# the georeferencing info
gtiff = gdal.GetDriverByName( 'GTiff' )
gtiff.CreateCopy(target, copy_geo(ndvi, prototype=source, xoffset=ulX, yoffset=ulY))
gtiff = None
以下图是本例的输出。您需要使用地理空间查看器(如 QGIS 或 OpenEV)查看它。在大多数图像编辑器中无法打开该图像。灰色越浅,该字段内的植物越健康:

现在我们知道了如何使用 NDVI 公式,让我们看看如何对其进行分类。
分类 NDVI
我们现在有一个有效的索引,但它不容易理解,因为它是一个灰度图像。如果我们以直观的方式给图像上色,那么即使是孩子也能识别出更健康的植物。在下一节 额外函数 中,我们读取这个灰度索引,并使用七个类别将其从棕色分类到深绿色。分类和图像处理例程,如直方图和拉伸函数,几乎与我们在第六章 Python 和遥感 中 创建直方图 部分使用的相同,但这次我们以更具体的方式应用它们。
这个示例的输出将是一个 GeoTIFF 文件,但这次它将是一个彩色 RGB 图像。
额外函数
我们不需要之前 NDVI 脚本中的任何函数,但我们需要添加一个用于创建和拉伸直方图的函数。这两个函数都使用 NumPy 数组。在这个脚本中,我们将 gdal_array 的引用缩短为 gd,因为它是一个长名称,并且我们需要在整个脚本中使用它。
让我们看看以下步骤:
- 首先,我们导入我们需要的库:
import gdal_array as gd
import operator
from functools import reduce
- 接下来,我们需要创建一个
histogram函数,我们将需要它来进行直方图拉伸:
def histogram(a, bins=list(range(256))):
"""
Histogram function for multi-dimensional array.
a = array
bins = range of numbers to match
"""
# Flatten, sort, then split our arrays for the histogram.
fa = a.flat
n = gd.numpy.searchsorted(gd.numpy.sort(fa), bins)
n = gd.numpy.concatenate([n, [len(fa)]])
hist = n[1:]-n[:-1]
return hist
- 现在,我们创建我们的直方图
stretch函数:
def stretch(a):
"""
Performs a histogram stretch on a gdal_array array image.
"""
hist = histogram(a)
lut = []
for b in range(0, len(hist), 256):
# step size – create equal interval bins.
step = reduce(operator.add, hist[b:b+256]) / 255
# create equalization lookup table
n = 0
for i in range(256):
lut.append(n / step)
n = n + hist[i+b]
gd.numpy.take(lut, a, out=a)
return a
现在我们有了我们的实用函数,我们可以处理 NDVI。
加载 NDVI
接下来,我们将 NDVI 脚本的输出重新加载到 NumPy 数组中。我们还将定义我们的输出图像名称为 ndvi_color.tif,并创建一个零填充的多维数组作为彩色 NDVI 图像的红、绿、蓝波段的占位符。以下代码将加载 NDVI TIFF 图像到 numpy 数组中:
# NDVI output from ndvi script
source = "ndvi.tif"
# Target file name for classified
# image image
target = "ndvi_color.tif"
# Load the image into an array
ndvi = gd.LoadFile(source).astype(gd.numpy.uint8)
现在我们已经将图像作为数组加载,我们可以拉伸它。
准备 NDVI
我们需要对 NDVI 执行直方图拉伸,以确保图像覆盖了将赋予最终产品意义的类范围:
# Peform a histogram stretch so we are able to
# use all of the classes
ndvi = stretch(ndvi)
# Create a blank 3-band image the same size as the ndvi
rgb = gd.numpy.zeros((3, len(ndvi), len(ndvi[0])), gd.numpy.uint8)
现在我们已经拉伸了图像,我们可以开始分类过程。
创建类别
在这部分,我们为 NDVI 类设置范围,这些范围从 0 到 255。我们将使用七个类别。你可以通过向类别列表中添加或删除值来更改类别的数量。接下来,我们创建一个 查找表,或 LUT,以为每个类别分配颜色。颜色的数量必须与类别的数量相匹配。
颜色定义为 RGB 值。start 变量定义了第一个类的开始。在这种情况下,0 是一个 nodata 值,我们在之前的脚本中指定了它,因此我们从 1 开始分类。然后我们遍历类,提取范围,并使用颜色分配将 RGB 值添加到我们的占位符数组中。最后,我们将着色图像保存为 GeoTIFF 文件:
# Class list with ndvi upper range values.
# Note the lower and upper values are listed on the ends
classes = [58, 73, 110, 147, 184, 220, 255]
# Color look-up table (lut)
# The lut must match the number of classes
# Specified as R, G, B tuples from dark brown to dark green
lut = [[120, 69, 25], [255, 178, 74], [255, 237, 166], [173, 232, 94],
[135, 181, 64], [3, 156, 0], [1, 100, 0]]
# Starting value of the first class
start = 1
现在我们可以对图像进行分类:
# For each class value range, grab values within range,
# then filter values through the mask.
for i in range(len(classes)):
mask = gd.numpy.logical_and(start <= ndvi,
ndvi <= classes[i])
for j in range(len(lut[i])):
rgb[j] = gd.numpy.choose(mask, (rgb[j], lut[i][j]))
start = classes[i]+1
最后,我们可以保存我们的分类 GeoTIFF 文件:
# Save a geotiff image of the colorized ndvi.
output=gd.SaveArray(rgb, target, format="GTiff", prototype=source)
output = None
这里是我们输出的图像:

这是本例的最终产品。农民可以使用这些数据来确定如何有效地灌溉和喷洒化学物质,如肥料和杀虫剂,以更精准、更有效、更环保的方式进行。实际上,这些类别甚至可以被转换成矢量 shapefile,然后加载到田间喷雾器的 GPS 驱动计算机上。这样,当喷雾器在田间移动时,或者在某些情况下,甚至可以携带喷雾附件的飞机在田间上空飞行,都会自动在正确的位置喷洒正确的化学物质数量。
注意,尽管我们已经将数据裁剪到田地中,但图像仍然是正方形。黑色区域是已转换为黑色的 nodata 值。在显示软件中,您可以设置 nodata 颜色为透明,而不会影响图像的其余部分。
尽管我们创建了一个非常具体的产品,即分类 NDVI,但这个脚本的框架可以被修改以实现许多遥感分析算法。有不同类型的 NDVI,但通过相对较小的修改,您可以将这个脚本变成一个工具,用于寻找海洋中的有害藻华,或者在森林中间的烟雾,这表明森林火灾。
本书试图尽可能减少 GDAL 的使用,以便专注于仅使用纯 Python 和可以从 PyPI 轻松安装的工具所能完成的工作。然而,记住关于使用 GDAL 及其相关实用程序执行类似任务的信息量很大。有关通过 GDAL 的命令行实用程序裁剪栅格的另一个教程,请参阅joeyklee.github.io/broc-cli-geo/guide/XX_raster_cropping_and_clipping.html。
现在我们已经处理了陆地,让我们处理水,以创建洪水淹没模型。
创建洪水淹没模型
在下一个例子中,我们将开始进入水文的世界。洪水是常见且破坏性极强的自然灾害之一,几乎影响着全球的每一个人口。地理空间模型是估计洪水影响并在发生前减轻这种影响的有力工具。我们经常在新闻中听到河流正在达到洪水位,但如果我们不能理解其影响,那么这些信息就是无意义的。
水文洪水模型开发成本高昂,可能非常复杂。这些模型对于工程师在建设防洪系统时至关重要。然而,第一响应者和潜在的洪水受害者只对即将发生的洪水的影响感兴趣。
我们可以使用一个非常简单且易于理解的工具来了解一个区域的洪水影响,这个工具被称为洪水淹没模型。该模型从一个单点开始,以洪水盆地在该特定洪水阶段可以容纳的最大水量来淹没一个区域。通常,这种分析是一个最坏情况。数百个其他因素都会影响到计算从河流顶部的洪水阶段进入盆地的水量。但我们仍然可以从这个简单的第一阶模型中学到很多东西。
如同在第一章的高程数据部分所述,使用 Python 学习地理空间分析,航天飞机雷达地形任务(SRTM)数据集提供了一个几乎全球性的 DEM,你可以用于这些类型的模型。有关 SRTM 数据的更多信息,请参阅www2.jpl.nasa.gov/srtm/。
您可以从git.io/v3fSg下载 EPSG:4326 的 ASCII 网格数据,以及包含点作为.zip文件的 shapefile。这个 shapefile 仅作参考,在此模型中没有任何作用。以下图像是一个数字高程模型(DEM),其中源点以黄色星号显示在德克萨斯州休斯顿附近。在现实世界的分析中,这个点可能是一个流量计,你将会有关于河流水位的资料:

我们在本例中介绍的这个算法被称为洪水填充算法。这个算法在计算机科学领域是众所周知的,并被用于经典的电脑游戏扫雷中,当用户点击一个方块时,用于清除板上的空方块。它也是图形程序(如Adobe Photoshop)中众所周知的油漆桶工具所使用的方法,用于用不同颜色填充相邻像素的同一区域。
实现此算法的方法有很多。其中最古老且最常见的方法是递归地遍历图像中的每个像素。递归的问题在于你最终会多次处理像素,并创建不必要的额外工作。递归洪水填充的资源使用量很容易在即使是中等大小的图像上使程序崩溃。
此脚本使用基于四向队列的洪水填充,可能会多次访问一个单元格,但确保我们只处理一个单元格一次。队列仅通过使用 Python 内置的集合类型来包含唯一的未处理单元格,该类型只持有唯一值。我们使用两个集合:fill,包含我们需要填充的单元格,和filled,包含已处理的单元格。
此示例执行以下步骤:
-
从 ASCII DEM 中提取标题信息。
-
将 DEM 作为
numpy数组打开。 -
将我们的起点定义为数组中的行和列。
-
声明洪水高程值。
-
仅过滤地形到所需的 elevations 值及其以下。
-
处理过滤后的数组。
-
创建一个 1, 0, 0 数组(即二进制数组),其中淹没的像素为 1。
-
将洪水淹没数组保存为 ASCII 网格。
这个例子在较慢的机器上可能需要一两分钟才能运行;我们将在整个脚本中使用print语句作为跟踪进度的简单方法。我们再次将此脚本分解为解释,以便清晰。
现在我们有了数据,我们可以开始我们的洪水填充函数。
洪水填充函数
在这个例子中,我们使用 ASCII 网格,这意味着这个模型的引擎完全在 NumPy 中。我们首先定义floodFill()函数,这是这个模型的核心和灵魂。这篇维基百科关于洪水填充算法的文章提供了不同方法的优秀概述:en.wikipedia.org/wiki/Flood_fill。
洪水填充算法从一个给定的单元格开始,开始检查相邻单元格的相似性。相似性因素可能是颜色,或者在我们的情况下是海拔。如果相邻单元格的海拔与当前单元格相同或更低,则该单元格被标记为检查其邻居,直到整个网格被检查。NumPy 不是设计用来以这种方式遍历数组的,但它总体上在处理多维数组方面仍然很高效。我们逐个单元格地通过并检查其北、南、东和西的邻居。任何可以淹没的单元格都被添加到填充集合中,它们的邻居也被添加到填充集合中以供算法检查。
如前所述,如果您尝试将相同的值添加到集合中两次,它将忽略重复条目并保持唯一的列表。通过在数组中使用集合,我们有效地检查单元格一次,因为填充集合包含唯一的单元格。以下代码实现了我们的floodFill函数:
- 首先,我们导入我们的库:
import numpy as np
from linecache import getline
- 接下来,我们创建我们的
floodFill函数:
def floodFill(c, r, mask):
"""
Crawls a mask array containing
only 1 and 0 values from the
starting point (c=column,
r=row - a.k.a. x, y) and returns
an array with all 1 values
connected to the starting cell.
This algorithm performs a 4-way
check non-recursively.
"""
- 接下来,我们创建集合来跟踪我们已经覆盖的单元格:
# cells already filled
filled = set()
# cells to fill
fill = set()
fill.add((c, r))
width = mask.shape[1]-1
height = mask.shape[0]-1
- 然后我们创建我们的淹没数组:
# Our output inundation array
flood = np.zeros_like(mask, dtype=np.int8)
- 现在我们可以遍历单元格并填充它们,或者不填充:
# Loop through and modify the cells which
# need to be checked.
while fill:
# Grab a cell
x, y = fill.pop()
- 如果土地高于洪水水位,则跳过它:
if y == height or x == width or x < 0 or y < 0:
# Don't fill
continue
- 如果土地海拔等于或低于洪水水位,则填充它:
if mask[y][x] == 1:
# Do fill
flood[y][x] = 1
filled.add((x, y))
- 现在,我们检查周围的相邻单元格以查看它们是否需要填充,当我们用完单元格时,我们返回填充的矩阵:
# Check neighbors for 1 values
west = (x-1, y)
east = (x+1, y)
north = (x, y-1)
south = (x, y+1)
if west not in filled:
fill.add(west)
if east not in filled:
fill.add(east)
if north not in filled:
fill.add(north)
if south not in filled:
fill.add(south)
return flood
现在我们已经设置了floodFill函数,我们可以创建一个洪水。
预测洪水淹没
在脚本剩余部分,我们从 ASCII 网格加载我们的地形数据,定义我们的输出网格文件名,并在地形数据上执行算法。洪水填充算法的种子是一个任意点,即sx和sy位于低海拔区域。在实际应用中,这些点可能是一个已知位置,例如河流流量计或大坝的裂缝。在最后一步,我们保存输出网格。
需要执行以下步骤:
- 首先,我们设置我们的
source和target数据名称:
source = "terrain.asc"
target = "flood.asc"
- 接下来,我们打开源:
print("Opening image...")
img = np.loadtxt(source, skiprows=6)
print("Image opened")
- 我们将创建一个低于
70米的掩码数组:
# Mask elevations lower than 70 meters.
wet = np.where(img < 70, 1, 0)
print("Image masked")
- 现在,我们将解析标题中的地理空间信息:
# Parse the header using a loop and
# the built-in linecache module
hdr = [getline(source, i) for i in range(1, 7)]
values = [float(h.split(" ")[-1].strip()) for h in hdr]
cols, rows, lx, ly, cell, nd = values
xres = cell
yres = cell * -1
- 现在,我们将建立一个位于河床中的起点:
# Starting point for the
# flood inundation in pixel coordinates
sx = 2582
sy = 2057
- 现在,我们触发我们的
floodFill函数:
print("Beginning flood fill")
fld = floodFill(sx, sy, wet)
print("Finished flood fill")
header = ""
for i in range(6):
header += hdr[i]
- 最后,我们可以保存我们的洪水淹没模型输出:
print("Saving grid")
# Open the output file, add the hdr, save the array
with open(target, "wb") as f:
f.write(bytes(header, 'UTF-8'))
np.savetxt(f, fld, fmt="%1i")
print("Done!")
下面的截图显示了在分类版本的 DEM 上洪水淹没的输出,低海拔值用棕色表示,中值用绿色表示,高值用灰色和白色表示:

包含所有低于 70 米区域的洪水栅格用蓝色表示。这张图片是用 QGIS 创建的,但它也可以在 ArcGIS 中以 EPSG:4326 格式显示。您也可以使用 GDAL 将洪水栅格网格保存为 8 位 TIFF 文件或 JPEG 文件,就像 NDVI 示例一样,以便在标准图形程序中查看。
下面的截图中的图像几乎相同,只是显示黄色的是从过滤掩码中导出的淹没,这是通过为数组生成一个名为wet的文件来完成的,而不是fld,以显示非连续区域,这些区域不是洪水的一部分。这些区域与源点不相连,因此在洪水事件中不太可能被触及:

通过改变海拔值,您可以创建额外的洪水淹没栅格。我们从一个 70 米的海拔值开始。如果我们将其值增加到 90,我们可以扩大洪水范围。下面的截图显示了 70 米和 90 米处的洪水事件:

90 米淹没区域是较浅的蓝色多边形。您可以采取更大或更小的步骤,以不同的图层显示不同的影响。
这个模型是一个优秀且有用的可视化工具。然而,您可以通过使用 GDAL 的polygonize()方法对洪水掩膜进行分析,进一步扩展这个分析,正如我们在第六章的“从图像中提取特征”部分所做的那样,Python 和遥感。这个操作将为您提供矢量洪水多边形。然后,您可以使用我们在第五章的“执行选择”部分讨论的原则,Python 和地理信息系统,使用多边形选择建筑物以确定人口影响。您还可以将洪水多边形与第五章的“点密度计算”部分中的点密度示例结合起来,以评估洪水潜在的人口影响。可能性是无限的。
创建一个彩色阴影图
在这个例子中,我们将结合之前的技术,将我们从第七章,“Python 和高程数据”中提取的地形阴影与我们在 LIDAR 上使用的颜色分类结合起来。对于这个例子,我们需要之前章节中使用的名为dem.asc和relief.asc的 ASCII 网格 DEM。
我们将创建一个彩色 DEM 和一个阴影,然后使用 PIL 将它们混合在一起以增强高程可视化。代码注释将引导你通过这个例子,因为许多这些步骤你已经很熟悉了:
- 首先,我们导入所需的库:
import gdal_array as gd
try:
import Image
except ImportError:
from PIL import Image
对于下一部分,你需要以下两个文件:github.com/GeospatialPython/Learn/raw/master/relief.zip 和 github.com/GeospatialPython/Learn/raw/master/dem.zip。
- 然后,我们将设置输入和输出的变量:
relief = "relief.asc"
dem = "dem.asc"
target = "hillshade.tif"
- 接下来,我们将加载我们的
relief图像:
# Load the relief as the background image
bg = gd.numpy.loadtxt(relief, skiprows=6)
- 然后,我们将加载 DEM 图像,这样我们就会有高程数据:
# Load the DEM into a numpy array as the foreground image
fg = gd.numpy.loadtxt(dem, skiprows=6)[:-2, :-2]
- 现在,我们将创建一个新的图像用于我们的彩色化,其中高程断点形成类别,并在 LUT 中对应相应的颜色:
# Create a blank 3-band image to colorize the DEM
rgb = gd.numpy.zeros((3, len(fg), len(fg[0])), gd.numpy.uint8)
# Class list with DEM upper elevation range values.
classes = [356, 649, 942, 1235, 1528,
1821, 2114, 2300, 2700]
# Color look-up table (lut)
# The lut must match the number of classes.
# Specified as R, G, B tuples
lut = [[63, 159, 152], [96, 235, 155], [100, 246, 174],
[248, 251, 155], [246, 190, 39], [242, 155, 39],
[165, 84, 26], [236, 119, 83], [203, 203, 203]]
# Starting elevation value of the first class
start = 1
- 我们现在可以进行我们的颜色分类:
# Process all classes.
for i in range(len(classes)):
mask = gd.numpy.logical_and(start <= fg,
fg <= classes[i])
for j in range(len(lut[i])):
rgb[j] = gd.numpy.choose(mask, (rgb[j], lut[i][j]))
start = classes[i]+1
- 然后,我们可以将我们的阴影高程数组转换为图像,以及我们的彩色 DEM:
# Convert the shaded relief to a PIL image
im1 = Image.fromarray(bg).convert('RGB')
# Convert the colorized DEM to a PIL image.
# We must transpose it from the Numpy row, col order
# to the PIL col, row order (width, height).
im2 = Image.fromarray(rgb.transpose(1, 2, 0)).convert('RGB')
- 现在,我们将混合两个图像以产生最终效果,并将其保存为图像文件:
# Blend the two images with a 40% alpha
hillshade = Image.blend(im1, im2, .4)
# Save the hillshade
hillshade.save(target)
下面的图像显示了输出,它非常适合作为 GIS 地图的背景:

现在我们能够模拟地形,让我们学习如何在上面导航。
执行最小成本路径分析
计算驾驶方向是全球最常用的地理空间功能。通常,这些算法计算点 A 和 B 之间的最短路径,或者它们可能会考虑道路的速度限制,甚至当前的交通状况,以便通过驾驶时间选择路线。
但如果你的工作是建造一条新的道路?或者如果你负责决定在偏远地区铺设电力传输线或水管的位置?在地形环境中,最短路径可能会穿过一个困难的山脉,或者穿过一个湖泊。在这种情况下,我们需要考虑障碍物,并在可能的情况下避开它们。然而,如果避开一个小障碍物让我们偏离得太远,实施该路线的成本可能比翻越山脉还要高。
这种类型的高级分析称为最低成本路径分析。我们在区域内搜索最佳折衷路线,该路线是距离与跟随该路线的成本的最佳平衡。我们用于此过程的算法称为A 星或 A算法。最古老的路线方法是Dijkstra 算法*,它计算网络中的最短路径,例如道路网络。A*方法也可以做到这一点,但它更适合穿越类似网格的 DEM。
您可以在以下网页上了解更多关于这些算法的信息:
-
Dijkstra 算法:
en.wikipedia.org/wiki/Dijkstra's_algorithm。
这个例子是本章中最复杂的。为了更好地理解它,我们有一个简单的程序版本,它是基于文本的,并在一个 5 x 5 的网格上操作,使用随机生成的值。您实际上可以在尝试在具有数千个值的等高线网格上之前看到这个程序如何遵循算法。
此程序执行以下步骤:
-
创建一个简单的网格,具有介于 1 和 16 之间的随机生成的伪高程值。
-
在网格的左下角定义一个起始位置。
-
定义终点为网格的右上角。
-
创建一个成本网格,包含每个单元格的高程以及单元格到终点的距离。
-
检查从起始位置开始的每个相邻单元格,并选择成本最低的一个。
-
使用所选单元格重复评估,直到到达终点。
-
返回所选单元格的集合作为最低成本路径。
-
设置测试网格。
您只需从命令行运行此程序并查看其输出。此脚本的第一个部分设置我们的模拟地形网格,作为一个随机生成的 NumPy 数组,具有介于 1 和 16 之间的理论高程值。我们还创建了一个距离网格,该网格计算每个单元格到目标单元格的距离。这个值是每个单元格的成本。
让我们看看以下步骤:
- 首先,我们将导入
numpy并设置我们网格的大小:
import numpy as np
# Width and height
# of grids
w = 5
h = 5
- 接下来,我们设置起始位置单元格和结束位置:
# Start location:
# Lower left of grid
start = (h-1, 0)
# End location:
# Top right of grid
dx = w-1
dy = 0
- 现在,我们可以根据我们的宽度和高度创建一个零网格:
# Blank grid
blank = np.zeros((w, h))
- 接下来,我们将设置我们的距离网格,以便创建阻抗值:
# Distance grid
dist = np.zeros(blank.shape, dtype=np.int8)
# Calculate distance for all cells
for y, x in np.ndindex(blank.shape):
dist[y][x] = abs((dx-x)+(dy-y))
- 现在,我们将打印出我们成本网格中每个单元格的成本值:
# "Terrain" is a random value between 1-16.
# Add to the distance grid to calculate
# The cost of moving to a cell
cost = np.random.randint(1, 16, (w, h)) + dist
print("COST GRID (Value + Distance)\n{}\n".format(cost))
现在我们有一个模拟的地形网格可以工作,我们可以测试一个路由算法。
简单的 A*算法
这里实现的 A*搜索算法以类似于前一个示例中我们的洪水填充算法的方式遍历网格。再次,我们使用集合来避免使用递归,并避免单元格检查的重复。但这次,我们不是检查高程,而是检查通过问题单元格的路线成本。如果移动增加了到达终点的成本,那么我们就选择成本更低的选项。
需要执行以下步骤:
- 首先,我们将通过创建跟踪路径进度的集合来开始我们的 A*函数:
# Our A* search algorithm
def astar(start, end, h, g):
closed_set = set()
open_set = set()
path = set()
- 接下来,我们将起始单元格添加到待处理的开放单元格列表中,以便开始循环处理该集合:
open_set.add(start)
while open_set:
cur = open_set.pop()
if cur == end:
return path
closed_set.add(cur)
path.add(cur)
options = []
y1 = cur[0]
x1 = cur[1]
- 我们检查周围单元格作为前进的选项:
if y1 > 0:
options.append((y1-1, x1))
if y1 < h.shape[0]-1:
options.append((y1+1, x1))
if x1 > 0:
options.append((y1, x1-1))
if x1 < h.shape[1]-1:
options.append((y1, x1+1))
if end in options:
return path
best = options[0]
closed_set.add(options[0])
- 然后,我们将检查每个选项以找到最佳选项,并将其附加到路径上,直到我们到达终点:
for i in range(1, len(options)):
option = options[i]
if option in closed_set:
continue
elif h[option] <= h[best]:
best = option
closed_set.add(option)
elif g[option] < g[best]:
best = option
closed_set.add(option)
else:
closed_set.add(option)
print(best, ", ", h[best], ", ", g[best])
open_set.add(best)
return []
现在我们已经设置了算法,我们可以通过创建路径来测试它:
生成测试路径
在本节中,我们将在测试网格上生成路径。我们将调用 A*函数,使用起点、终点、成本网格和距离网格:
# Find the path
path = astar(start, (dy, dx), cost, dist)
print()
现在,我们将我们的路径放在自己的网格上并打印出来:
# Create and populate the path grid
path_grid = np.zeros(cost.shape, dtype=np.uint8)
for y, x in path:
path_grid[y][x] = 1
path_grid[dy][dx] = 1
print("PATH GRID: 1=path")
print(path_grid)
接下来,我们将查看这个测试的输出。
查看测试输出
当你运行这个程序时,你会生成一个类似以下随机编号的网格:
COST GRID (Value + Distance)
[[13 10 5 15 9]
[15 13 16 5 16]
[17 8 9 9 17]
[ 4 1 11 6 12]
[ 2 7 7 11 8]]
(Y,X), HEURISTIC, DISTANCE
(3, 0) , 4 , 1
(3, 1) , 1 , 0
(2, 1) , 8 , 1
(2, 2) , 9 , 0
(2, 3) , 9 , 1
(1, 3) , 5 , 0
(0, 3) , 15 , 1
PATH GRID: 1=path
[[0 0 0 1 1]
[0 0 0 1 0]
[0 1 1 1 0]
[1 1 0 0 0]
[1 0 0 0 0]]
网格足够小,你可以轻松地手动追踪算法的步骤。这个实现使用的是曼哈顿距离,这意味着距离不使用对角线——只有左、右、上、下的测量。搜索也不会对角移动,以保持简单。
真实世界的例子
现在我们对 A算法有了基本的了解,让我们转到更复杂的例子。对于缓解示例,我们将使用与加拿大不列颠哥伦比亚省温哥华附近相同的 DEM,我们在创建阴影高程*部分第七章中使用了它。这个网格的空间参考是 EPSG:26910 NAD 83/UTM 区域 10N。您可以从git.io/v3fpL下载 DEM、高程和形状文件的起点和终点作为压缩包。
我们实际上将使用阴影高程进行可视化。在这个练习中,我们的目标是以最低的成本从起点移动到终点:

仅从地形来看,有两条路径遵循低海拔路线,方向变化不大。这两条路径在下面的屏幕截图中有展示:

因此,我们预计当我们使用 A*算法时,它将非常接近。记住,算法只查看直接附近,所以它不能像我们一样查看整个图像,并且它不能根据已知的障碍物在路线早期进行调整。
我们将从简单的示例扩展这个实现,使用欧几里得距离,或者说是“如鸟飞”的测量,我们还将允许搜索在八个方向上而不是四个方向上进行。我们将优先考虑地形作为主要决策点。我们还将使用距离,即到终点和从起点的距离,作为次要优先级,以确保我们朝着目标前进,而不是偏离轨道太远。除了这些差异之外,步骤与简单示例相同。输出将是一个栅格,路径值设为1,其他值设为0。
现在我们已经理解了问题,让我们来解决这个问题!
加载网格
在本节和接下来的几节中,我们将创建一个脚本,该脚本可以创建地面的路线。脚本开始得很简单。我们从 ASCII 网格将网格加载到 NumPy 数组中。我们命名我们的输出路径网格,然后定义起始单元格和结束单元格:
- 首先,我们导入我们的库:
import numpy as np
import math
from linecache import getline
import pickle
- 接下来,我们将定义我们的输入和输出数据源:
# Our terrain data
source = "dem.asc"
# Output file name for the path raster
target = "path.asc"
- 然后,我们可以加载网格,跳过标题行:
print("Opening %s..." % source)
cost = np.loadtxt(source, skiprows=6)
print("Opened %s." % source)
- 接下来,我们将解析标题以获取地理空间和网格大小信息:
# Parse the header
hdr = [getline(source, i) for i in range(1, 7)]
values = [float(ln.split(" ")[-1].strip()) for ln in hdr]
cols, rows, lx, ly, cell, nd = values
- 最后,我们将定义我们的起始和结束位置:
# Starting column, row
sx = 1006
sy = 954
# Ending column, row
dx = 303
dy = 109
现在我们已经加载了网格,我们可以设置所需的函数。
定义辅助函数
我们需要三个函数来在地面进行路由。一个是 A*算法,另外两个辅助算法帮助算法选择下一步。我们将简要讨论这些辅助函数。首先,我们有一个简单的欧几里得距离函数,名为e_dist,它返回两点之间的直线距离,以地图单位计。接下来,我们有一个重要的函数,称为weighted_score,它根据相邻单元格和当前单元格之间的高度变化以及到目的地的距离为相邻单元格评分。
这个函数比单独的距离或海拔更好,因为它减少了两个单元格之间出现平局的可能性,使得避免回溯更容易。这个评分公式松散地基于一个称为Nisson 评分的概念,这个概念在类似算法中常用,并在本章前面提到的维基百科文章中有所提及。这个函数的伟大之处在于它可以对相邻单元格进行任何你想要的评分。你也可能使用实时数据来查看相邻单元格的当前天气,并避免有雨或雪的单元格。
以下代码将创建我们的距离函数和权重函数,这些函数是我们穿越地形所需的:
- 首先,我们将创建一个欧几里得距离函数,它将给出两点之间的距离:
def e_dist(p1, p2):
"""
Takes two points and returns
the Euclidian distance
"""
x1, y1 = p1
x2, y2 = p2
distance = math.sqrt((x1-x2)**2+(y1-y2)**2)
return int(distance)
- 现在,我们将创建我们的权重函数,以便为每个节点的移动适宜性评分:
def weighted_score(cur, node, h, start, end):
"""
Provides a weighted score by comparing the
current node with a neighboring node. Loosely
based on the Nisson Score concept: f=g+h
In this case, the "h" value, or "heuristic",
is the elevation value of each node.
"""
- 我们从
score为0开始,检查节点与起点和终点的距离:
score = 0
# current node elevation
cur_h = h[cur]
# current node distance from end
cur_g = e_dist(cur, end)
# current node distance from
cur_d = e_dist(cur, start)
- 接下来,我们检查相邻的节点并决定移动的方向:
# neighbor node elevation
node_h = h[node]
# neighbor node distance from end
node_g = e_dist(node, end)
# neighbor node distance from start
node_d = e_dist(node, start)
# Compare values with the highest
# weight given to terrain followed
# by progress towards the goal.
if node_h < cur_h:
score += cur_h-node_h
if node_g < cur_g:
score += 10
if node_d > cur_d:
score += 10
return score
现在我们已经完成了辅助函数,我们可以构建 A*函数。
实际的 A*算法
这个算法比我们之前示例中的简单版本更复杂。我们使用集合来避免冗余。它还实现了我们更高级的评分算法,并在进行额外计算之前检查我们是否在路径的末端。与我们的上一个示例不同,这个更高级的版本还检查八个方向上的单元格,因此路径可以斜向移动。在这个函数的末尾有一个被注释掉的print语句。你可以取消注释它来观察搜索如何在网格中爬行。下面的代码将实现我们将在本节中使用的 A*算法:
- 首先,我们通过接受起点、终点和分数来打开函数:
def astar(start, end, h):
"""
A-Star (or A*) search algorithm.
Moves through nodes in a network
(or grid), scores each node's
neighbors, and goes to the node
with the best score until it finds
the end. A* is an evolved Dijkstra
algorithm.
"""
- 现在,我们设置跟踪进度的集合:
# Closed set of nodes to avoid
closed_set = set()
# Open set of nodes to evaluate
open_set = set()
# Output set of path nodes
path = set()
- 接下来,我们开始使用起点进行处理:
# Add the starting point to
# to begin processing
open_set.add(start)
while open_set:
# Grab the next node
cur = open_set.pop()
- 如果我们到达终点,我们返回完成的路径:
# Return if we're at the end
if cur == end:
return path
- 否则,我们继续在网格中工作,消除可能性:
# Close off this node to future
# processing
closed_set.add(cur)
# The current node is always
# a path node by definition
path.add(cur)
- 为了保持进度,我们在进行过程中抓取所有需要处理的邻居:
# List to hold neighboring
# nodes for processing
options = []
# Grab all of the neighbors
y1 = cur[0]
x1 = cur[1]
if y1 > 0:
options.append((y1-1, x1))
if y1 < h.shape[0]-1:
options.append((y1+1, x1))
if x1 > 0:
options.append((y1, x1-1))
if x1 < h.shape[1]-1:
options.append((y1, x1+1))
if x1 > 0 and y1 > 0:
options.append((y1-1, x1-1))
if y1 < h.shape[0]-1 and x1 < h.shape[1]-1:
options.append((y1+1, x1+1))
if y1 < h.shape[0]-1 and x1 > 0:
options.append((y1+1, x1-1))
if y1 > 0 and x1 < h.shape[1]-1:
options.append((y1-1, x1+1))
- 我们检查每个邻居是否是目的地:
# If the end is a neighbor, return
if end in options:
return path
- 我们将第一个选项作为“最佳”选项,并处理其他选项,在过程中进行升级:
# Store the best known node
best = options[0]
# Begin scoring neighbors
best_score = weighted_score(cur, best, h, start, end)
# process the other 7 neighbors
for i in range(1, len(options)):
option = options[i]
# Make sure the node is new
if option in closed_set:
continue
else:
# Score the option and compare
# it to the best known
option_score = weighted_score(cur, option,
h, start, end)
if option_score > best_score:
best = option
best_score = option_score
else:
# If the node isn't better seal it off
closed_set.add(option)
# Uncomment this print statement to watch
# the path develop in real time:
# print(best, e_dist(best, end))
# Add the best node to the open set
open_set.add(best)
return []
现在我们有了我们的路由算法,我们可以生成实际路径。
生成实际路径
最后,我们将实际路径作为一个零网格中的一系列一,这个栅格可以随后被导入到 QGIS 等应用程序中,并在地形网格上可视化。在下面的代码中,我们将使用我们的算法和辅助函数来生成路径,如下所示:
- 首先,我们将起点、终点以及地形网格发送到路由函数:
print("Searching for path...")
p = astar((sy, sx), (dy, dx), cost)
print("Path found.")
print("Creating path grid...")
path = np.zeros(cost.shape)
print("Plotting path...")
for y, x in p:
path[y][x] = 1
path[dy][dx] = 1
print("Path plotted.")
- 一旦我们有了路径,我们就可以将其保存为 ASCII 网格:
print("Saving %s..." % target)
header = ""
for i in range(6):
header += hdr[i]
# Open the output file, add the hdr, save the array
with open(target, "wb") as f:
f.write(bytes(header, 'UTF-8'))
np.savetxt(f, path, fmt="%4i")
- 现在,我们想要保存我们的路径数据,因为点按正确的顺序排列,从起点到终点。当我们把它们放入网格中时,我们失去了这个顺序,因为它们都是一个栅格。我们将使用内置的 Python
pickle模块将列表对象保存到磁盘。我们将在下一节中使用这些数据来创建路线的矢量形状文件。因此,我们将我们的路径数据保存为可重用的 pickle Python 对象,以后无需运行整个程序:
print("Saving path data...")
with open("path.p", "wb") as pathFile:
pickle.dump(p, pathFile)
print("Done!")
这是我们的搜索输出路径:

如您所见,A*搜索非常接近我们手动选择的路线之一。在几个案例中,算法选择解决一些地形,而不是尝试绕过它。有时,轻微的地形被认为比绕行的距离成本低。您可以在路线右上角的放大部分中看到这种选择的例子。红线是我们程序通过地形生成的路线:

我们只使用了两个值:地形和距离。但您也可以添加数百个因素,例如土壤类型、水体和现有道路。所有这些项目都可以作为阻抗或直接的障碍。您只需修改示例中的评分函数,以考虑任何额外的因素。请记住,您添加的因素越多,追踪 A*实现选择路线时的“思考”就越困难。
对于这项分析来说,一个明显的未来方向是创建一个作为线的矢量版本的路线。这个过程包括将每个单元格映射到一个点,然后使用最近邻分析正确排序点,最后将其保存为 shapefile 或 GeoJSON 文件。
将路线转换为 shapefile
最短路径路由的光栅版本对于可视化很有用,但它在分析方面并不太好,因为它嵌入在光栅中,因此很难与其他数据集相关联,就像我们在本书中多次做的那样。我们的下一个目标将是使用创建路线时保存的路径数据来创建 shapefile,因为保存的数据是正确排序的。以下代码将我们的光栅路径转换为 shapefile,这使得在 GIS 中进行分析更容易:
- 首先,我们将导入所需的模块,数量并不多。我们将使用
pickle模块来恢复路径data对象。然后,我们将使用linecache模块从路径光栅中读取地理空间标题信息,以便将路径的行和列映射到地球坐标。最后,我们将使用shapefile模块来导出 shapefile:
import pickle
from linecache import getline
import shapefile
- 接下来,我们将创建一个函数来将行和列转换为x和y坐标。该函数接受路径光栅文件中的元数据标题信息,以及列和行号:
def pix2coord(gt,x,y):
geotransform = gt
ox = gt[2]
oy = gt[3]
pw = gt[4]
ph = gt[4]
cx = ox + pw * x + (pw/2)
cy = oy + pw * y + (ph/2)
return cx, cy
- 现在,我们将从 pickle 对象中恢复
path对象:
with open("path.p", "rb") as pathFile:
path = pickle.load(pathFile)
- 然后,我们将解析路径光栅文件中的元数据信息:
hdr = [getline("path.asc", i) for i in range(1, 7)]
gt = [float(ln.split(" ")[-1].strip()) for ln in hdr]
- 接下来,我们需要一个列表对象来存储转换后的坐标:
coords = []
- 现在,我们将每个光栅位置从最短路径对象转换为地理空间坐标,并将其存储在我们创建的列表中:
for y,x in path:
coords.append(pix2coord(gt,x,y))
- 最后,只需几行代码,我们就可以写出一条线 shapefile:
with shapefile.Writer("path", shapeType=shapefile.POLYLINE) as w:
w.field("NAME")
w.record("LeastCostPath")
w.line([coords])
干得好!您已经创建了一个程序,可以根据一组规则自动导航通过障碍物,并将其导出为可以在 GIS 中显示和分析的文件!我们只使用了三个规则,但您可以通过添加其他数据集来添加额外的限制,例如天气或水体,或您能想到的任何其他东西。
现在我们已经了解了在任意表面上开辟路径,我们将看看在网络中路由。
计算卫星图像云覆盖
卫星图像为我们提供了强大的鸟瞰地球的视角。它们在多种用途中都很有用,我们在第六章 Python 和遥感中看到了这一点。然而,它们有一个缺点——云层。当卫星绕地球飞行并收集图像时,不可避免地会拍摄到云层。除了遮挡我们对地球的视线外,云层数据还可能通过在无用的云层数据上浪费 CPU 周期或引入不希望的数据值来不利地影响遥感算法。
解决方案是创建一个云层掩码。云层掩码是一个将云层数据隔离在单独的栅格中的栅格。然后你可以使用这个栅格作为参考来处理图像,以避免云层数据,或者甚至可以使用它从原始图像中移除云层。
在本节中,我们将使用rasterio模块和rio-l8qa插件为 Landsat 图像创建一个云层掩码。云层掩码将作为一个单独的图像创建,仅包含云层:
-
首先,我们需要从
bit.ly/landsat8data下载一些样本 Landsat 8 卫星图像数据作为 ZIP 文件。 -
点击右上角的下载图标以将数据作为 ZIP 文件下载,并将其解压缩到名为
l8的目录中: -
接下来,确保你已经安装了我们需要的栅格库,通过运行
pip:
pip install rasterio
pip install rio-l8qa
- 现在,我们将首先导入我们需要的库来创建云层掩码:
import glob
import os
import rasterio
from l8qa.qa import write_cloud_mask
- 接下来,我们需要提供一个指向我们的卫星图像目录的引用:
# Directory containing landsat data
landsat_dir = "l8"
- 现在,我们需要定位卫星数据的质量保证元数据,它提供了我们生成云层掩码所需的信息:
src_qa = glob.glob(os.path.join(landsat_dir, '*QA*'))[0]
- 最后,我们使用质量保证文件创建一个云层掩码 TIFF 文件:
with rasterio.open(src_qa) as qa_raster:
profile = qa_raster.profile
profile.update(nodata=0)
write_cloud_mask(qa_raster.read(1), profile, 'cloudmask.tif')
以下图像只是来自 Landsat 8 数据集的 7 波段(短波红外)图像:

下一张图像是仅包含云层和阴影位置的云层掩码图像:

最后,这里是对图像上的云层的掩码,显示云层为黑色:

这个例子只是简单介绍了你可以使用图像掩码做什么。另一个rasterio模块,rio-cloudmask,允许你从头开始计算云层掩码,而不使用质量保证数据。但这需要一些额外的预处理步骤。你可以在这里了解更多信息:github.com/mapbox/rio-cloudmask.
沿街道进行路由
沿街道进行路由使用称为图的连接线网络。图中的线条可以具有阻抗值,这会阻止路由算法将它们包括在路径中。阻抗值的例子通常包括交通量、速度限制,甚至是距离。路由图的一个关键要求是,所有称为边的线条都必须是连通的。为制图创建的道路数据集通常会包含节点不交叉的线条。
在这个例子中,我们将通过距离计算图中的最短路径。我们将使用起点和终点,它们不是图中的节点,这意味着我们首先必须找到距离我们的起点和目的地最近的图节点。
为了计算最短路径,我们将使用一个名为 NetworkX 的强大纯 Python 图形库。NetworkX 是一个通用的网络图形库,可以创建、操作和分析复杂网络,包括地理空间网络。如果 pip 在您的系统上没有安装 NetworkX,您可以在 networkx.readthedocs.org/en/stable/ 找到针对不同操作系统的下载和安装 NetworkX 的说明。
您可以从 git.io/vcXFQ 下载道路网络以及位于美国墨西哥湾沿岸的起点和终点,作为一个 ZIP 文件。然后,您可以按照以下步骤操作:
- 首先,我们需要导入我们将要使用的库。除了 NetworkX 之外,我们还将使用 PyShp 库来读取和写入形状文件:
import networkx as nx
import math
from itertools import tee
import shapefile
import os
- 接下来,我们将定义当前目录为我们创建的路线形状文件输出目录:
savedir = "."
- 现在,我们需要一个函数来计算点之间的距离,以便填充我们图中的阻抗值,并找到路线的起点和终点附近的节点:
def haversine(n0, n1):
x1, y1 = n0
x2, y2 = n1
x_dist = math.radians(x1 - x2)
y_dist = math.radians(y1 - y2)
y1_rad = math.radians(y1)
y2_rad = math.radians(y2)
a = math.sin(y_dist/2)**2 + math.sin(x_dist/2)**2 \
* math.cos(y1_rad) * math.cos(y2_rad)
c = 2 * math.asin(math.sqrt(a))
distance = c * 6371
return distance
- 然后,我们将创建另一个函数,该函数从列表中返回点对,为我们提供构建图边所用的线段:
def pairwise(iterable):
"""Return an iterable in tuples of two
s -> (s0,s1), (s1,s2), (s2, s3), ..."""
a, b = tee(iterable)
next(b, None)
return zip(a, b)
- 现在,我们将定义我们的道路网络形状文件。这个道路网络是美国地质调查局(美国地质调查局)(USGS)的 U.S. 州际公路文件形状文件的一个子集,已经编辑过以确保所有道路都是连通的:
shp = "road_network.shp"
- 接下来,我们将使用 NetworkX 创建一个图,并将形状文件段添加为图边:
G = nx.DiGraph()
r = shapefile.Reader(shp)
for s in r.shapes():
for p1, p2 in pairwise(s.points):
G.add_edge(tuple(p1), tuple(p2))
- 然后,我们可以提取连接组件作为子图。然而,在这种情况下,我们已经确保整个图是连通的:
sg = list(nx.connected_component_subgraphs(G.to_undirected()))[0]
- 接下来,我们可以读取我们想要导航的
start和end点:
r = shapefile.Reader("start_end")
start = r.shape(0).points[0]
end = r.shape(1).points[0]
- 现在,我们遍历图,并为每个边分配距离值,使用我们的
haversine公式:
for n0, n1 in sg.edges_iter():
dist = haversine(n0, n1)
sg.edge[n0][n1]["dist"] = dist
- 接下来,我们必须找到图中距离我们的起点和终点最近的节点,以便通过遍历所有节点并测量到终点的距离来开始和结束我们的路线,直到找到最短距离:
nn_start = None
nn_end = None
start_delta = float("inf")
end_delta = float("inf")
for n in sg.nodes():
s_dist = haversine(start, n)
e_dist = haversine(end, n)
if s_dist < start_delta:
nn_start = n
start_delta = s_dist
if e_dist < end_delta:
nn_end = n
end_delta = e_dist
- 现在,我们已经准备好通过我们的道路网络计算最短距离:
path = nx.shortest_path(sg, source=nn_start, target=nn_end, weight="dist")
- 最后,我们将结果添加到 shapefile 中,并保存我们的路线:
w = shapefile.Writer(shapefile.POLYLINE)
w.field("NAME", "C", 40)
w.line(parts=[[list(p) for p in path]])
w.record("route")
w.save(os.path.join(savedir, "route"))
以下屏幕截图显示了浅灰色中的道路网络、起点和终点以及黑色中的路线。你可以看到路线穿过道路网络,以便以最短的距离到达最近的终点道路:

现在我们知道了如何创建各种类型的路线,我们可以看看在沿着路线旅行时可能会拍摄到的照片的位置。
地理定位照片
带有 GPS 功能的相机拍摄的照片,包括智能手机,在文件的头部存储位置信息,格式称为EXIF标签。这些标签主要基于 TIFF 图像标准中使用的相同头部标签。在这个例子中,我们将使用这些标签创建一个包含照片点位置和照片文件路径的 shapefile,并将这些路径作为属性。
在这个例子中,我们将使用 PIL,因为它具有提取 EXIF 数据的能力。大多数用智能手机拍摄的照片都是带有地理标记的图像;然而,你可以从git.io/vczR0下载本例中使用的集合:
- 首先,我们将导入所需的库,包括用于图像元数据的 PIL 和用于 shapefiles 的 PyShp:
import glob
import os
try:
import Image
import ImageDraw
except ImportError:
from PIL import Image
from PIL.ExifTags import TAGS
import shapefile
- 现在,我们需要三个函数。第一个函数提取 EXIF 数据。第二个函数将度、分、秒(DMS)坐标转换为十进制度(EXIF 数据以 DMS 坐标存储 GPS 数据)。第三个函数提取 GPS 数据并执行坐标转换:
def exif(img):
# extract exif data.
exif_data = {}
try:
i = Image.open(img)
tags = i._getexif()
for tag, value in tags.items():
decoded = TAGS.get(tag, tag)
exif_data[decoded] = value
except:
pass
return exif_data
def dms2dd(d, m, s, i):
# convert degrees, min, sec to decimal degrees
sec = float((m * 60) + s)
dec = float(sec / 3600)
deg = float(d + dec)
if i.upper() == 'W':
deg = deg * -1
elif i.upper() == 'S':
deg = deg * -1
return float(deg)
def gps(exif):
# get gps data from exif
lat = None
lon = None
if exif['GPSInfo']:
# Lat
coords = exif['GPSInfo']
i = coords[1]
d = coords[2][0][0]
m = coords[2][1][0]
s = coords[2][2][0]
lat = dms2dd(d, m, s, i)
# Lon
i = coords[3]
d = coords[4][0][0]
m = coords[4][1][0]
s = coords[4][2][0]
lon = dms2dd(d, m, s, i)
return lat, lon
- 接下来,我们将遍历照片,提取坐标,并将坐标和文件名存储在字典中:
photos = {}
photo_dir = "./photos"
files = glob.glob(os.path.join(photo_dir, "*.jpg"))
for f in files:
e = exif(f)
lat, lon = gps(e)
photos[f] = [lon, lat]
- 现在,我们将保存照片信息为 shapefile 格式:
with shapefile.Writer("photos", shapefile.POINT) as w:
w.field("NAME", "C", 80)
for f, coords in photos.items():
w.point(*coords)
w.record(f)
shapefile 中照片的文件名现在是照片拍摄地点的属性。包括 QGIS 和 ArcGIS 在内的 GIS 程序具有将那些属性转换为链接的工具,当你点击照片路径或点时。以下是从 QGIS 中截取的屏幕截图,显示了使用“运行要素动作工具”点击相关点后打开的一张照片:

要查看结果,请按照以下说明操作:
-
从
qgis.org下载 QGIS 并遵循安装说明。 -
打开 QGIS 并将
photos.shp文件拖放到空白地图上。 -
在左侧的图层面板上,右键单击名为“照片”的图层并选择“属性”。
-
在“操作”选项卡上,点击绿色加号以打开新的操作对话框。
-
在类型下拉菜单中,选择“打开”。
-
在描述字段中,输入“打开图像”。
-
点击右下角的“插入”按钮。
-
点击“确定”按钮,然后关闭属性对话框。
-
点击运行功能工具右侧的小黑箭头,该工具是一个带有绿色中心和白色箭头的齿轮图标。
-
在弹出的菜单中,选择打开图片。
-
现在,点击地图上的一个点,查看带有地理标签的图片弹出窗口。
现在,让我们从在地球上拍摄的图片,转到拍摄地球本身的图片,通过处理卫星图像来实现。
摘要
在本章中,我们学习了如何创建三个现实世界的产品,这些产品在政府、科学和工业中每天都会使用。除了这种分析通常使用成本数千美元的黑盒软件包之外,我们能够使用非常少和免费的跨平台 Python 工具。而且除了本章中的示例之外,你现在还有一些可重用的函数、算法和用于其他高级分析的处理框架,这将使你能够解决你在交通、农业和天气等领域遇到的新问题。
在下一章中,我们将进入地理空间分析的一个相对较新的领域:实时和近实时数据。
第九章:实时数据
地理空间分析师中有一句俗语:“地图一旦制作出来,就立刻过时了”。这句话反映了地球及其上的一切都在不断变化的事实。在地理空间分析的大部分历史中,以及本书的大部分内容中,地理空间产品相对静态。原始数据集通常每隔几个月到几年更新一次。地图中地理空间数据的时代被称为数据货币。
由于收集数据所需的时间和费用,数据货币传统上并不是主要关注点。网络地图、无线蜂窝调制解调器和低成本 GPS 天线改变了这一焦点。现在,从物流上讲,监控快速变化的对象或系统并将其变化广播给数百万在线用户变得可行,甚至相当经济。这种变化正在改变地理空间技术,并将其引向新的方向。这一革命的直接证据是使用 Google Maps 或 OpenLayers 等系统进行的网络地图混合应用,以及可在线访问的数据格式。每天都有越来越多的电子设备被连接到网络上,以广播其位置和数据,用于自动化或远程控制。例如,恒温器、摄像头、汽车等等。你还可以使用流行的 Raspberry Pi 等廉价的嵌入式计算机将几乎任何东西变成一个连接的智能设备。将设备连接成数据和信息网络的概念被称为物联网(IoT)。
本章我们将探讨以下主题:
-
实时数据的局限性
-
使用实时数据
-
轨迹车辆
-
暴风雨追逐
-
现场报告
到最后,你将学会如何处理实时地理空间数据,并且能够构建一个可以作为任何类型数据传输源的现场报告工具。
技术要求
本章需要以下几样东西:
-
Python 3.6 或更高版本
-
内存:最小 6 GB(Windows),8 GB(macOS),推荐 8 GB
-
存储:最小 7200 RPM SATA,可用空间 20 GB,推荐使用 SSD,可用空间 40 GB
-
处理器:最小 Intel Core i3 2.5 GHz,推荐 Intel Core i5
-
MapQuest 开发者 API 密钥,可在以下链接获取:
developer.mapquest.com/plan_purchase/steps/business_edition/business_edition_free/register
实时数据的局限性
术语实时数据通常意味着接近实时。一些跟踪设备捕获实时数据,并且可能每秒更新几次。但是,广播这些数据的底层基础设施的限制可能将输出频率限制为每 10 秒或更长。气象雷达是一个完美的例子。多普勒天气雷达(DWR)持续扫描,但数据通常每五分钟在线更新一次。但是,与传统地理空间数据更新相比,几分钟的刷新已经足够实时。限制可以总结如下:
-
网络带宽限制限制数据大小
-
网络延迟限制数据更新频率
-
由于电池寿命等限制,数据源的可访问性
-
由于数据立即对消费者可用而缺乏质量控制
-
由于快速摄取未经验证的数据导致的安全漏洞
实时数据为地理空间应用开辟了额外的机会,因此我们将探讨如何使用它。
使用实时数据
网络混合应用通常使用实时数据。网络混合应用非常神奇,已经改变了许多不同行业的运营方式。但它们通常受到限制,通常只是在一个地图上显示预处理的 数据,并给开发者提供访问 JavaScript API 的权限。但如果你想以某种方式处理数据呢?如果你想过滤、更改,然后将其发送到另一个系统呢?为了使用实时数据进行地理空间分析,你需要能够将其作为点数据或地理参考栅格访问。
你可以在这里了解更多关于网络地图混合应用的信息: www.esri.com/arcgis-blog/products/product/uncategorized/digital-map-mashups/.
与前几章中的示例一样,脚本尽可能简单,并设计为从头到尾阅读,无需太多心理循环。当使用函数时,它们首先列出,然后是脚本变量声明,最后是主程序执行。
现在让我们看看如何使用 NextBus API 中的车辆来访问实时和点位置数据源。
轨迹车辆
对于我们的第一个实时数据源,我们将使用优秀的NextBus API。NextBus (www.nextbus.com/) 是一个商业服务,用于跟踪包括公交车、有轨电车和火车在内的市政公共交通。乘坐这些交通线路的人可以跟踪下一辆公交车的到达时间。
更好的是,在客户同意的情况下,NextBus 通过表示状态转移(REST)API发布跟踪数据。使用 URL API 调用,开发者可以请求有关车辆的信息,并接收有关其位置的 XML 文档。这是一种直接开始使用实时数据的方法。
如果你访问 NextBus,你会看到一个如以下截图所示的网页界面,显示加利福尼亚州洛杉矶的地铁系统数据:

该系统允许你选择多个参数来了解下一站的当前位置和时间预测。在屏幕的右侧,有一个链接到一个 Google Maps 混合应用,显示特定路线的公交跟踪数据,如下面的截图所示:

这是一个非常有用的网站,但它不让我们控制数据的显示和使用方式。让我们直接使用 Python 和 NextBus REST API 来访问原始数据,开始处理实时数据。
在本章的例子中,我们将使用这里找到的文档化的 NextBus API:www.nextbus.com/xmlFeedDocs/NextBusXMLFeed.pdf。
为了开始这个例子,我们需要一个所需公交车的列表。
NextBus 机构列表
NextBus 的客户被称为机构。在我们的例子中,我们将追踪加利福尼亚州洛杉矶的公交车路线。首先,我们需要获取一些关于该机构的信息。NextBus API 由一个名为publicXMLFeed的 Web 服务组成,在其中你设置一个名为command的参数。我们将使用浏览器中的agencyList命令,通过以下 REST URL 获取包含机构信息的 XML 文档:webservices.nextbus.com/service/publicXMLFeed?command=agencyList。
当我们在浏览器中访问该链接时,它返回一个包含<agency/>标签的 XML 文档。洛杉矶的标签如下所示:
<agency tag="lametro" title="Los Angeles Metro" regionTitle="California-Southern"/>
现在我们有了公交车的列表,我们需要获取它们可以行驶的路线。
NextBus 路线列表
tag属性是桑德贝的 ID,我们需要它来执行其他 NextBus API 命令。其他属性是可读的元数据。我们需要获取的下一项信息是关于路线 2公交车路线的详细信息。为了获取这些信息,我们将使用机构 ID 和routeList REST 命令,将 URL 粘贴到我们的网页浏览器中获取另一个 XML 文档。
注意,机构 ID 被设置为 REST URL 中的参数:webservices.nextbus.com/service/publicXMLFeed?command=routeList&a=lametro。
当我们在浏览器中调用此 URL 时,我们得到以下 XML 文档:
<?xml version="1.0" encoding="utf-8" ?>
<body copyright="All data copyright Los Angeles Metro 2015."><route tag="2" title="2 Downtown LA - Pacific Palisades Via"/><route tag="4" title="4 Downtown LA - Santa Monica Via Santa"/>
<route tag="10" title="10 W Hollywood-Dtwn LA -Avalon Sta Via"/>
...
<route tag="901" title="901 Metro Orange Line"/>
<route tag="910" title="910 Metro Silver Line"/>
</body>
我们有了公交车和路线。我们准备好开始追踪它们的地理位置了!
NextBus 车辆位置
因此,根据这些结果,存储在tag属性中的主线路线 ID 仅仅是1。因此,现在我们拥有了追踪洛杉矶地铁路线 2上公交车所需的所有信息。
只有一个必需的参数(称为 t),它代表自 1970 年纪元日期(1970 年 1 月 1 日午夜 UTC)以来的毫秒数。纪元日期是机器用来跟踪时间的一个计算机标准。在 NextBus API 中,最简单的事情就是为这个值指定 0,这将返回最后 15 分钟的数据。
有一个可选的 direction 标签,允许你指定一个终止的公交车站,如果一条路线上有多个公交车在相反方向上运行。但是,如果我们不指定,API 将返回第一个,这符合我们的需求。获取洛杉矶地铁主线路线的 REST URL 看起来如下所示:webservices.nextbus.com/service/publicXMLFeed?command=vehicleLocations&a=lametro&r=2&t=0。
在浏览器中调用此 REST URL 返回以下 XML 文档:
<?xml version="1.0" encoding="utf-8" ?>
<body copyright="All data copyright Los Angeles Metro 2015.">
<vehicle id="7582" routeTag="2" dirTag="2_758_0" lat="34.097992" lon="-118.350365" secsSinceReport="44" predictable="true" heading="90" speedKmHr="0"/>
<vehicle id="7583" routeTag="2" dirTag="2_779_0" lat="34.098076" lon="-118.301399" secsSinceReport="104" predictable="true" heading="90" speedKmHr="37"/>
. . .
</body >
每个 vehicle 标签代表过去 15 分钟内的一个位置。last 标签是最新的位置(尽管 XML 在技术上是无序的)。
这些公共交通系统并非全天运行。许多在当地时间晚上 10:00(22:00)关闭。如果你在脚本中遇到错误,请使用 NextBus 网站定位一个正在运行的系统,并将机构变量和路线变量更改为该系统。
我们现在可以编写一个 Python 脚本来返回给定路线上的公交车位置。如果我们不指定 direction 标签,NextBus 将返回第一个。在这个例子中,我们将通过调用 REST URL 并使用之前章节中演示的内置 Python urllib 库来轮询 NextBus 跟踪 API。
我们将使用简单内置的 minidom 模块解析返回的 XML 文档,该模块在 第四章The minidom module 部分中也有介绍,这是 [Geospatial Python Toolbox*]。此脚本仅输出路线 2 公交的最新纬度和经度。你将在顶部看到机构和路线变量。为此,我们需要遵循以下步骤:
- 首先,我们导入所需的库:
import urllib.request
import urllib.parse
import urllib.error
from xml.dom import minidom
- 现在我们设置 API 模式和我们要查询的客户和路线的变量:
# Nextbus API command mode
command = "vehicleLocations"
# Nextbus customer to query
agency = "lametro"
# Bus we want to query
route = "2"
- 我们将时间值设置为
0,这将获取最后15分钟的数据:
# Time in milliseconds since the
# 1970 epoch time. All tracks
# after this time will be returned.
# 0 only returns data for the last
# 15 minutes
epoch = "0"
- 现在我们需要构建我们将用于访问 API 的查询 URL:
# Build our query url
# webservices base url
url = "http://webservices.nextbus.com"
# web service path
url += "/service/publicXMLFeed?"
# service command/mode
url += "command=" + command
# agency
url += "&a=" + agency
url += "&r=" + route
url += "&t=" + epoch
- 接下来,我们可以使用
urllib调用 API:
# Access the REST URL
feed = urllib.request.urlopen(url)
if feed:
# Parse the xml feed
xml = minidom.parse(feed)
# Get the vehicle tags
vehicles = xml.getElementsByTagName("vehicle")
# Get the most recent one. Normally there will
# be only one.
- 最后,我们可以访问结果并打印出每辆公交车的位置:
if vehicles:
bus = vehicles.pop()
# Print the bus latitude and longitude
att = bus.attributes
print(att["lon"].value, ",", att["lat"].value)
else:
print("No vehicles found.")
此脚本的输出只是一个纬度和经度值,这意味着我们现在控制了 API 并理解了它。输出应该是纬度和经度的坐标值。
现在我们已经准备好使用这些位置值来创建我们自己的地图。
映射 NextBus 位置
可自由获取的街道地图数据最佳来源是 OpenStreetMap (OSM)项目:www.openstreetmap.org。OSM 还有一个公开可用的 REST API,用于创建静态地图图像,称为 StaticMapLite:staticmap.openstreetmap.de。
OSM StaticMapLite API 提供了一个基于 Google 静态地图 API 的 GET API,用于创建具有有限数量点标记和线条的简单地图图像。与 REST API 相比,GET API 允许您在 URL 上的问号后附加名称/值参数对。REST API 将参数作为 URL 路径的一部分。我们将使用该 API 按需创建我们的 NextBus API 地图,并为公交车位置使用红色推针图标。
在下一个示例中,我们将之前的脚本压缩成一个名为 nextbus() 的紧凑函数。nextbus() 函数接受一个机构、路线、命令和纪元作为参数。命令默认为 vehicleLocations,纪元默认为 0 以获取最后 15 分钟的数据。在这个脚本中,我们将传递 LA 路线-2 的路线信息,并使用默认命令返回公交车的最新经纬度。
我们有一个名为 nextmap() 的第二个函数,每次调用时都会在公交车的当前位置创建一个带有紫色标记的地图。地图是通过构建一个针对 OSM StaticMapLite API 的 GET URL 来创建的,该 URL 以公交车的位置为中心,并使用介于 1-18 之间的缩放级别和地图大小来确定地图范围。
您可以直接在浏览器中访问 API,以查看 nextmap() 函数的示例。您需要通过在此处注册来获取免费的 MapQuest 开发者 API 密钥:developer.mapquest.com/plan_purchase/steps/business_edition/business_edition_free/register。一旦您有了密钥,将其插入到 key 参数中,即 YOUR_API_KEY_HERE。然后,您可以测试以下示例 URL:https://www.mapquestapi.com/staticmap/v4/getmap?size=865,512&type=map&pois=mcenter,40.702147,-74.015794|&zoom=14¢er=40.714728,-73.998672&imagetype=JPEG&key=YOUR_API_KEY_HERE。
静态地图看起来类似于以下这样:

nextmap() 函数接受 NextBus 机构 ID、路线 ID 和地图的基本图像名称字符串。该函数调用 nextbus() 函数以获取经纬度对。该程序的执行在定时间隔内循环,第一次通过创建地图,然后在后续通过中覆盖地图。程序还会在每次保存地图时输出一个时间戳。requests 变量指定通过次数,freq 变量代表每个循环之间的时间(以秒为单位)。让我们检查以下代码,看看这个例子是如何工作的:
- 首先,我们导入所需的库:
import urllib.request
import urllib.parse
import urllib.error
from xml.dom import minidom
import time
- 接下来,我们创建一个函数,可以获取给定路线上一辆公交车的最新位置:
def nextbus(a, r, c="vehicleLocations", e=0):
"""Returns the most recent latitude and
longitude of the selected bus line using
the NextBus API (nbapi)
Arguments: a=agency, r=route, c=command,
e=epoch timestamp for start date of track,
0 = the last 15 minutes"""
nbapi = "http://webservices.nextbus.com"
nbapi += "/service/publicXMLFeed?"
nbapi += "command={}&a={}&r={}&t={}".format(c, a, r, e)
xml = minidom.parse(urllib.request.urlopen(nbapi))
# If more than one vehicle, just get the first
bus = xml.getElementsByTagName("vehicle")[0]
if bus:
at = bus.attributes
return(at["lat"].value, at["lon"].value)
else:
return (False, False)
- 现在我们有一个函数可以在地图图像上绘制公交车位置:
def nextmap(a, r, mapimg):
"""Plots a nextbus location on a map image
and saves it to disk using the MapQuest OpenStreetMap Static Map
API (osmapi)"""
# Fetch the latest bus location
lat, lon = nextbus(a, r)
if not lat:
return False
# Base url + service path
- 在该函数中,我们在 URL 中设置 API 参数:
osmapi = "https://www.mapquestapi.com/staticmap/v4/getmap?
type=map&"
# Use a red, pushpin marker to pin point the bus
osmapi += "mcenter={},{}|&".format(lat, lon)
# Set the zoom level (between 1-18, higher=lower scale)
osmapi += "zoom=18&"
# Center the map around the bus location
osmapi += "center={},{}&".format(lat, lon)
# Set the map image size
osmapi += "&size=1500,1000"
# Add our API Key
osmapi += "&key=YOUR_API_KEY_HERE"
- 现在,我们可以通过调用 URL 并保存它来创建图像:
# Create a PNG image
osmapi += "imagetype=png&"
img = urllib.request.urlopen(osmapi)
# Save the map image
with open("{}.png".format(mapimg), "wb") as f:
f.write(img.read())
return True
- 现在在我们的主程序中,我们可以设置我们想要跟踪的公交车的变量:
# Nextbus API agency and bus line variables
agency = "lametro"
route = "2"
# Name of map image to save as PNG
nextimg = "nextmap"
- 然后,我们可以指定我们想要的跟踪点的数量和频率:
# Number of updates we want to make
requests = 1
# How often we want to update (seconds)
freq = 5
- 最后,我们可以开始跟踪和更新我们的地图图像:
# Map the bus location every few seconds
for i in range(requests):
success = nextmap(agency, route, nextimg)
if not success:
print("No data available.")
continue
print("Saved map {} at {}".format(i, time.asctime()))
time.sleep(freq)
- 当脚本运行时,你会看到类似以下输出的结果,显示脚本保存每张地图的时间:
Saved map 0 at Sun Nov 1 22:35:17 2015
Saved map 1 at Sun Nov 1 22:35:24 2015
Saved map 2 at Sun Nov 1 22:35:32 2015
此脚本保存的地图图像类似于以下内容,这取决于你运行脚本时公交车所在的位置:

这张地图是使用 API 创建自定义地图产品的优秀示例。但它是一个非常基础的跟踪应用。为了开始将其开发成一个更有趣的地理空间产品,我们需要将其与其他实时数据源相结合,这些数据源能给我们提供更多的情境感知。
现在我们能够跟踪公交车了,让我们添加一些对乘坐公交车的乘客有用的额外信息到地图上。让我们添加一些天气数据。
暴风雨追逐
到目前为止,我们已经创建了一个比 NextBus 网站所做更简单的版本。但我们以最终使我们完全控制输出的方式完成了它。现在我们想要使用这种控制来超越 NextBus Google Maps 混合应用所能做到的。我们将添加另一个对旅行者和公交车线路运营商都非常重要的实时数据源:天气。
爱荷华州立大学的 Mesonet 项目提供免费且经过精炼的天气数据,适用于各种应用。我们使用这些数据为我们的公交车位置地图创建实时天气地图。我们可以使用开放地理空间联盟(OGC)的网络地图服务(WMS)标准来请求我们感兴趣区域的单张图像。WMS 是 OGC 的一个标准,通过互联网提供地理参考地图图像;它们通过地图服务器通过 HTTP 请求生成。
Mesonet 系统提供了一个优秀的网络地图服务,该服务根据正确格式的 WMS 请求从全球降水镶嵌图中返回一个子集图像。以下是一个此类请求的示例查询:mesonet.agron.iastate.edu/cgi-bin/wms/nexrad/n0r.cgi?SERVICE=WMS&VERSION=1.1.1&REQUEST=GetMap&LAYERS=nexrad-n0r&STYLES=&SRS=EPSG:900913&BBOX=-15269659.42,2002143.61,-6103682.81,7618920.15&WIDTH=600&HEIGHT=600&FORMAT=image/png。
由于本章的示例依赖于实时数据,所列出的具体请求如果没有在感兴趣的区域有活动,可能会产生空白天气图像。您可以访问此链接(radar.weather.gov/ridge/Conus/index.php)以找到正在发生风暴的区域。此页面包含 Google Earth 或 QGIS 的 KML 链接。这些 WMS 图像是类似于以下样本的透明 PNG 图像:

另一方面,OSM 网站不再通过 WMS 提供其街道地图——只提供瓦片。然而,他们允许其他组织下载瓦片或原始数据以扩展免费服务。美国国家海洋和大气管理局(NOAA)就是这样做的,并为他们的 OSM 数据提供了一个 WMS 接口,允许请求检索我们需要的单个基图图像,用于我们的公交路线:

现在我们有了获取基图和天气数据的数据源。我们想要将这些图像合并并绘制公交车的当前位置。而不是一个简单的点,这次我们将更加复杂,并添加以下公交图标:

您需要从以下链接下载此图标busicon.png到您的当前工作目录:github.com/GeospatialPython/Learn/blob/master/busicon.png?raw=true。
现在我们将结合之前编写的脚本和新的数据源来创建一个实时天气公交地图。由于我们将融合街道地图和天气地图,我们需要使用之前章节中提到的Python 图像处理库(PIL)。我们将用之前示例中的nextmap()函数替换为一个简单的wms()函数,该函数可以从任何 WMS 服务中通过边界框获取地图图像。此外,我们还将添加一个将十进制度数转换为米的函数,命名为ll2m()。
该脚本获取公交车位置,将位置转换为米,在位置周围创建一个 2 英里(3.2 公里)的矩形,然后下载街道和天气地图。然后使用 PIL 将地图图像混合在一起。PIL 然后将公交车图标图像缩小到 30 x 30 像素,并将其粘贴在地图的中心,即公交车位置。让我们看看以下代码是如何工作的:
- 首先,我们将导入所需的库:
import sys
import urllib.request
import urllib.parse
import urllib.error
from xml.dom import minidom
import math
try:
import Image
except:
from PIL import Image
- 现在,我们将重用之前示例中的
nextbus函数来获取公交跟踪数据:
def nextbus(a, r, c="vehicleLocations", e=0):
"""Returns the most recent latitude and
longitude of the selected bus line using
the NextBus API (nbapi)"""
nbapi = "http://webservices.nextbus.com"
nbapi += "/service/publicXMLFeed?"
nbapi += "command=%s&a=%s&r=%s&t=%s" % (c, a, r, e)
xml = minidom.parse(urllib.request.urlopen(nbapi))
# If more than one vehicle, just get the first
bus = xml.getElementsByTagName("vehicle")[0]
if bus:
at = bus.attributes
return(at["lat"].value, at["lon"].value)
else:
return (False, False)
- 我们还需要一个将纬度和经度转换为米的函数:
def ll2m(lon, lat):
"""Lat/lon to meters"""
x = lon * 20037508.34 / 180.0
y = math.log(math.tan((90.0 + lat) *
math.pi / 360.0)) / (math.pi / 180.0)
y = y * 20037508.34 / 180
return (x, y)
- 现在我们需要一个函数来检索 WMS 地图图像,我们将使用它来获取天气图像:
def wms(minx, miny, maxx, maxy, service, lyr, epsg, style, img, w,
h):
"""Retrieve a wms map image from
the specified service and saves it as a JPEG."""
wms = service
wms += "?SERVICE=WMS&VERSION=1.1.1&REQUEST=GetMap&"
wms += "LAYERS={}".format(lyr)
wms += "&STYLES={}&".format(style)
wms += "SRS=EPSG:{}&".format(epsg)
wms += "BBOX={},{},{},{}&".format(minx, miny, maxx, maxy)
wms += "WIDTH={}&".format(w)
wms += "HEIGHT={}&".format(h)
wms += "FORMAT=image/jpeg"
wmsmap = urllib.request.urlopen(wms)
with open(img + ".jpg", "wb") as f:
f.write(wmsmap.read())
- 现在我们可以设置主程序中的所有变量以使用我们的函数:
# Nextbus agency and route ids
agency = "roosevelt"
route = "shuttle"
# OpenStreetMap WMS service
basemap = "http://ows.mundialis.de/services/service"
# Name of the WMS street layer
streets = "TOPO-OSM-WMS"
# Name of the basemap image to save
mapimg = "basemap"
# OpenWeatherMap.org WMS Service
weather = "https://mesonet.agron.iastate.edu/cgi-bin/wms/nexrad/n0q.cgi?"
# If the sky is clear over New York,
# use the following url which contains
# a notional precipitation sample:
# weather = "http://git.io/vl4r1"
# WMS weather layer
weather_layer = "nexrad-n0q-900913"
# Name of the weather image to save
skyimg = "weather"
# Name of the finished map to save
final = "next-weather"
# Transparency level for weather layer
# when we blend it with the basemap.
# 0 = invisible, 1 = no transparency
opacity = .5
# Pixel width and height of the
# output map images
w = 600
h = 600
# Pixel width/height of the the
# bus marker icon
icon = 30
- 现在,我们已经准备好获取我们的公交车位置:
# Get the bus location
lat, lon = nextbus(agency, route)
if not lat:
print("No bus data available.")
print("Please try again later")
sys.exit()
# Convert strings to floats
lat = float(lat)
lon = float(lon)
# Convert the degrees to Web Mercator
# to match the NOAA OSM WMS map
x, y = ll2m(lon, lat)
# Create a bounding box 1600 meters
# in each direction around the bus
minx = x - 1600
maxx = x + 1600
miny = y - 1600
maxy = y + 1600
- 然后,我们可以下载我们的街道地图:
# Download the street map
wms(minx, miny, maxx, maxy, basemap, streets, mapimg, w, h)
- 然后,我们可以下载天气地图:
# Download the weather map
wms(minx, miny, maxx, maxy, weather, weather_layer, skyimg, w, h)
- 现在,我们可以将天气数据叠加到公交地图上:
# Open the basemap image in PIL
im1 = Image.open("basemap.png").convert('RGBA')
# Open the weather image in PIL
im2 = Image.open("weather.png").convert('RGBA')
# Convert the weather image mode
# to "RGB" from an indexed PNG
# so it matches the basemap image
im2 = im2.convert(im1.mode)
# Create a blended image combining
# the basemap with the weather map
im3 = Image.blend(im1, im2, opacity)
- 接下来,我们需要将公交图标添加到我们的组合地图中,以显示公交车的位置:
# Open up the bus icon image to
# use as a location marker.
# http://git.io/vlgHl
im4 = Image.open("busicon.png")
# Shrink the icon to the desired
# size
im4.thumbnail((icon, icon))
# Use the blended map image
# and icon sizes to place
# the icon in the center of
# the image since the map
# is centered on the bus
# location.
w, h = im3.size
w2, h2 = im4.size
# Paste the icon in the center of the image
center_width = int((w/2)-(w2/2))
center_height = int((h/2)-(h2/2))
im3.paste(im4, (center_width, center_height), im4)
- 最后,我们可以保存完成的地图:
# Save the finished map
im3.save(final + ".png")
此脚本将生成一个类似于以下地图:

地图显示公交车在其当前位置正经历中等降水。颜色渐变,如之前 Mesonet 网站截图所示,从浅蓝色(轻度降水)到绿色、黄色、橙色,最后到红色(或黑白中的浅灰色到深灰色),随着雨势加大。因此,在创建此地图时,公交车线路运营商可以使用此图像告诉他们的司机开慢一点,乘客也会知道在前往公交车站之前可能需要拿一把伞。
由于我们想从底层学习 NextBus API,我们直接使用内置的 Python 模块来使用 API。但存在几个用于 API 的第三方 Python 模块,包括 PyPI 上名为nextbus的一个,它允许你使用所有 NextBus 命令的高级对象,并提供比本章简单示例中不包括的更健壮的错误处理。
现在我们已经学会了如何检查天气,让我们使用 Python、HTML 和 JavaScript 将离散的实时数据源组合成更有意义的产品。
现场报告
在本章的最后一个示例中,我们将从公交车上下来,进入现场。现代智能手机、平板电脑和笔记本电脑使我们能够从任何地方更新 GIS 并查看这些更新。我们将使用 HTML、GeoJSON、Leaflet JavaScript 库以及一个名为 Folium 的纯 Python 库来创建一个客户端-服务器应用程序,允许我们将地理空间信息发布到服务器,然后创建一个交互式网络地图来查看这些数据更新。
首先,我们需要一个 Web 表单,它显示你的当前位置,并在你提交表单并附上关于你位置的评论时更新服务器。你可以在以下位置找到该表单:geospatialpython.github.io/Learn/fieldwork.html。
以下截图显示了表单:

你可以查看该表单的源代码以了解其工作原理。映射是通过 Leaflet 库完成的,并将 GeoJSON 发布到myjson.com上的一个独特 URL。你可以在移动设备上使用此页面,将其移动到任何 Web 服务器,甚至可以在你的本地硬盘上使用它。
表单发布到以下公开 URL 在myjson.com上:api.myjson.com/bins/467pm。你可以在浏览器中访问该 URL 以查看原始 GeoJSON。
接下来,你需要从 PyPI 安装 Folium 库。Folium 提供了一个简单的 Python API 来创建 Leaflet 网络地图。你可以在以下位置找到更多关于 Folium 的信息:github.com/python-visualization/folium。
Folium 使得制作 Leaflet 地图变得极其简单。这个脚本只有几行,并将输出一个名为map.html的网页。我们传递 GeoJSON URL 给map对象,它将在地图上绘制位置:
import folium
m = folium.Map()
m.geo_json(geo_path="https://api.myjson.com/bins/467pm")
m.create_map(path="map.html")
生成的交互式地图将以标记的形式显示点。当你点击一个标记时,表单中的信息将被显示出来。你只需在任何浏览器中打开 HTML 文件即可。
摘要
实时数据是进行新型地理空间分析的一种令人兴奋的方式,这是由包括网络地图、GPS 和无线通信在内的多种不同技术的进步才最近才成为可能。在本章中,你学习了如何访问实时位置数据的原始数据流,如何获取实时栅格数据的子集,如何仅使用 Python 将不同类型的实时数据组合成定制的地图分析产品,以及如何构建客户端-服务器地理空间应用程序以实时更新 GIS。
与前面的章节一样,这些示例包含构建块,将使你能够使用 Python 构建新的应用程序类型,这些应用程序远远超出了典型的基于 JavaScript 的流行和普遍的混合应用。
在下一章中,我们将把迄今为止所学的一切结合成一个完整的地理空间应用程序,在现实场景中应用算法和概念。
第十章:整合所有内容
在整本书中,我们已经触及了地理空间分析的所有重要方面,并且我们使用了多种不同的 Python 技术来分析不同类型的地理空间数据。在本章的最后,我们将利用我们几乎涵盖的所有主题,来制作一个实际应用且非常受欢迎的产品:GPS 路线分析报告。
这些报告在数十个移动应用服务、GPS 手表、车载导航系统和其他基于 GPS 的工具中很常见。GPS 通常记录位置、时间和海拔。从这些值中,我们可以推导出大量关于记录数据沿途发生事件的辅助信息。包括 RunKeeper、MapMyRun、Strava 和 Nike Plus 在内的健身应用都使用类似的报告来展示跑步、徒步、骑行和步行的 GPS 追踪运动数据。
我们将使用 Python 创建这样的报告。这个程序将近 500 行代码,是我们迄今为止最长的,因此我们将分部分逐步进行。我们将结合以下技术:
-
理解典型的 GPS 报告
-
构建 GPS 报告工具
随着我们逐步通过这个程序,我们将使用所有熟悉的技术,但我们将以新的方式使用它们。
技术要求
我们在本章中需要以下东西:
-
Python 3.6 或更高版本
-
内存:最低要求 – 6 GB(Windows),8 GB(macOS);推荐 8 GB
-
存储:最低要求 7200 RPM SATA,可用空间 20 GB,推荐 SSD,可用空间 40 GB
-
处理器:最低要求 Intel Core i3 2.5 GHz,推荐 Intel Core i5
-
PIL:Python 图像库
-
NumPy:一个多维和数组处理库
-
pygooglechart:Google 图表 API 的 Python 封装器 -
FPDF:一个简单且纯 Python 的 PDF 编写器
理解典型的 GPS 报告
一个典型的 GPS 报告包括常见的元素,如路线图、海拔剖面图和速度剖面图。以下截图是一个通过 RunKeeper 记录的典型路线的报告(runkeeper.com/index):

我们的报告将与这个服务类似,但我们还会增加一个特色。我们将包括路线图和海拔剖面图,就像这个服务一样,但我们还会添加在记录该路线时发生的天气条件以及沿途拍摄的地理定位照片。
既然我们已经了解了 GPS 报告是什么,那么让我们学习如何构建它。
构建 GPS 报告工具
我们程序的名字是 GPX-Reporter.py。如果您还记得 第二章 中关于 标签和标记格式 的部分,学习地理空间数据,GPX 格式是存储 GPS 路线信息最常见的方式。几乎每个依赖 GPS 数据的程序和设备都可以转换为 GPX 格式。
对于这个示例,您可以从以下链接下载一个示例 GPX 文件:git.io/vl7qi。此外,您还需要从 PyPI 安装几个 Python 库。
您只需使用 easy_install 或 pip 安装这些工具。我们还将使用一个名为 SRTM.py 的模块。此模块是用于处理 2000 年由航天飞机奋进号在 11 天的 航天飞机雷达地形测量任务(SRTM)期间收集的近全球高程数据的实用工具。使用 pip 安装 SRTM 模块:
pip install srtm.py
或者,您也可以下载压缩文件,解压后,将 srtm 文件夹复制到您的 Python site-packages 目录或工作目录:git.io/vl5Ls。
您还需要注册一个免费的 Dark Sky API。这项免费服务提供独特的工具。这是唯一提供全球、历史天气数据的服务,对于几乎任何地点,每天最多可免费请求 1,000 次:darksky.net/dev。
Dark Sky 将为您提供一个文本密钥,您需要在运行 GPX-Reporter 程序之前将其插入到名为 api_key 的变量中。最后,根据 Dark Sky 的服务条款,您需要下载一个标志图像并将其插入到报告中:raw.githubusercontent.com/GeospatialPython/Learn/master/darksky.png。
您可以在此处查看 Dark Sky 的 服务条款:darksky.net/dev/docs/terms。
现在,我们准备好通过 GPX-Reporter 程序进行工作。像本书中的其他脚本一样,此程序试图最小化函数,以便您可以更好地在心理上追踪程序并轻松修改它。以下列表包含程序中的主要步骤:
-
设置 Python
logging模块 -
建立我们的辅助函数
-
解析 GPX 数据文件
-
计算路线边界框
-
缓冲边界框
-
将框转换为米
-
下载底图
-
下载高程数据
-
对高程数据进行阴影处理
-
增加阴影对比度
-
混合阴影和高程图
-
在单独的图像上绘制 GPX 轨迹
-
将轨迹图像和底图混合
-
绘制起点和终点
-
保存地图图像
-
计算路线里程标记
-
构建高程剖面图
-
获取路线时间段内的天气数据
-
生成 PDF 报告
下一个子节将带您了解第一步。
初始设置
程序的开始是 import 语句,然后是 Python logging 模块。logging 模块提供了一种比简单的 print 语句更健壮的方式来跟踪和记录程序状态。在这个程序部分,我们按照以下步骤进行配置:
- 我们首先需要安装所有需要的库,如下面的代码所示:
from xml.dom import minidom
import json
import urllib.request
import urllib.parse
import urllib.error
import math
import time
import logging
import numpy as np
import srtm # Python 3 version: http://git.io/vl5Ls
import sys
from pygooglechart import SimpleLineChart
from pygooglechart import Axis
import fpdf
import glob
import os
try:
import Image
import ImageFilter
import ImageEnhance
import ImageDraw
except:
from PIL import Image
from PIL import ImageFilter
from PIL import ImageEnhance
from PIL import ImageDraw
from PIL.ExifTags import TAGS
- 现在,我们可以配置 Python
logging模块,以在整个过程中告诉我们发生了什么,如下所示:
# Python logging module.
# Provides a more advanced way
# to track and log program progress.
# Logging level - everything at or below
# this level will output. INFO is below.
level = logging.DEBUG
# The formatter formats the log message.
# In this case we print the local time, logger name, and message
formatter = logging.Formatter("%(asctime)s - %(name)s - %(message)s")
# Establish a logging object and name it
log = logging.getLogger("GPX-Reporter")
# Configure our logger
log.setLevel(level)
# Print to the command line
console = logging.StreamHandler()
console.setLevel(level)
console.setFormatter(formatter)
log.addHandler(console)
此日志记录器将输出到控制台,但通过简单的修改,你可以将其输出到文件,甚至是一个数据库,只需更改本节中的配置即可。此模块是 Python 内置的,在此处有文档:docs.python.org/3/howto/logging.html。
接下来,我们有几个在程序中多次使用的实用函数。
使用实用函数
所有以下函数(除与时间相关的函数外)已在之前的章节中以某种形式使用过。让我们看看如何在我们的示例中使用实用函数:
- 首先,
ll2m()函数将纬度和经度转换为米:
def ll2m(lat, lon):
"""Lat/lon to meters"""
x = lon * 20037508.34 / 180.0
y = math.log(math.tan((90.0 + lat) *
math.pi / 360.0)) / (math.pi / 180.0)
y = y * 20037508.34 / 180
return (x, y)
world2pixel()函数将地理空间坐标转换为输出地图图像上的像素坐标:
def world2pixel(x, y, w, h, bbox):
"""Converts world coordinates
to image pixel coordinates"""
# Bounding box of the map
minx, miny, maxx, maxy = bbox
# world x distance
xdist = maxx - minx
# world y distance
ydist = maxy - miny
# scaling factors for x, y
xratio = w/xdist
yratio = h/ydist
# Calculate x, y pixel coordinate
px = w - ((maxx - x) * xratio)
py = (maxy-y) * yratio
return int(px), int(py)
- 然后,我们有
get_utc_epoch()和get_local_time()函数将 GPX 文件中存储的 UTC 时间转换为沿路线的本地时间:
def get_utc_epoch(timestr):
"""Converts a GPX timestamp to Unix epoch seconds
in Greenwich Mean Time to make time math easier"""
# Get time object from ISO time string
utctime = time.strptime(timestr, '%Y-%m-%dT%H:%M:%S.000Z')
# Convert to seconds since epoch
secs = int(time.mktime(utctime))
return secs
- 现在我们有一个 haversine 距离函数和我们的简单
wms函数来检索地图图像:
def haversine(x1, y1, x2, y2):
"""Haversine distance formula"""
x_dist = math.radians(x1 - x2)
y_dist = math.radians(y1 - y2)
y1_rad = math.radians(y1)
y2_rad = math.radians(y2)
a = math.sin(y_dist/2)**2 + math.sin(x_dist/2)**2 \
* math.cos(y1_rad) * math.cos(y2_rad)
c = 2 * math.asin(math.sqrt(a))
# Distance in miles. Just use c * 6371
# for kilometers
distance = c * (6371/1.609344) # Miles
return distance
wms()函数使用以下代码检索地图图像:
def wms(minx, miny, maxx, maxy, service, lyr, epsg, style, img, w, h):
"""Retrieve a wms map image from
the specified service and saves it as a JPEG."""
wms = service
wms += "?SERVICE=WMS&VERSION=1.1.1&REQUEST=GetMap&"
wms += "LAYERS={}".format(lyr)
wms += "&STYLES={}&".format(style)
wms += "SRS=EPSG:{}&".format(epsg)
wms += "BBOX={},{},{},{}&".format(minx, miny, maxx, maxy)
wms += "WIDTH={}&".format(w)
wms += "HEIGHT={}&".format(h)
wms += "FORMAT=image/jpeg"
wmsmap = urllib.request.urlopen(wms)
with open(img + ".jpg", "wb") as f:
f.write(wmsmap.read())
- 接下来,我们有一个
exif()函数用于从照片中提取元数据:
def exif(img):
"""Return EXIF metatdata from image"""
exif_data = {}
try:
i = Image.open(img)
tags = i._getexif()
for tag, value in tags.items():
decoded = TAGS.get(tag, tag)
exif_data[decoded] = value
except:
pass
return exif_data
- 然后,我们有一个
dms2dd()函数将度/分/秒坐标转换为十进制度,因为这是照片坐标的存储方式:
def dms2dd(d, m, s, i):
"""Convert degrees/minutes/seconds to
decimal degrees"""
s *= .01
sec = float((m * 60.0) + s)
dec = float(sec / 3600.0)
deg = float(d + dec)
if i.upper() == 'W':
deg = deg * -1.0
elif i.upper() == 'S':
deg = deg * -1.0
return float(deg)
- 最后,我们有一个
gps()函数用于从照片元数据中提取坐标:
def gps(exif):
"""Extract GPS info from EXIF metadat"""
lat = None
lon = None
if exif['GPSInfo']:
# Lat
coords = exif['GPSInfo']
i = coords[1]
d = coords[2][0][0]
m = coords[2][1][0]
s = coords[2][2][0]
lat = dms2dd(d, m ,s, i)
# Lon
i = coords[3]
d = coords[4][0][0]
m = coords[4][1][0]
s = coords[4][2][0]
lon = dms2dd(d, m ,s, i)
return lat, lon
- 接下来,我们有我们的程序变量。我们将访问由名为 Mundalis 的公司免费提供的 OpenStreetMap WMS 服务以及由 NASA 提供的 SRTM 数据。
在本书中,我们为了简便起见使用 Python 的 urllib 库来访问 WMS 服务,但如果你计划频繁使用 OGC 网络服务,你应该使用通过 PyPI 可用的 Python 包 OWSLib:pypi.python.org/pypi/OWSLib。
现在让我们执行以下步骤来设置 WMS 网络服务:
- 我们将输出几个中间产品和图像。这些变量在这些步骤中使用。
route.gpx文件在本节中定义为gpx变量。首先,我们设置一些用于度到弧度转换以及反向转换的转换常量,如下所示:
# Needed for numpy conversions in hillshading
deg2rad = 3.141592653589793 / 180.0
rad2deg = 180.0 / 3.141592653589793
- 接下来,我们设置
.gpx文件的名称如下:
# Program Variables
# Name of the gpx file containing a route.
# https://git.io/fjwHW
gpx = "route.gpx"
- 现在,我们开始设置 WMS 网络服务,它将检索地图:
# NOAA OpenStreetMap Basemap
# OSM WMS service
osm_WMS = "http://ows.mundialis.de/services/service"
# Name of the WMS street layer
# streets = "osm"
osm_lyr = "OSM-WMS"
# Name of the basemap image to save
osm_img = "basemap"
# OSM EPSG code (spatial reference system)
osm_epsg = 3857
# Optional WMS parameter
osm_style = ""
- 接下来,我们设置我们的阴影参数,这将确定人工太阳的角度和方向:
# Shaded elevation parameters
#
# Sun direction
azimuth = 315.0
# Sun angle
altitude = 45.0
# Elevation exageration
z = 5.0
# Resolution
scale = 1.0
- 然后,我们设置没有海拔信息的地方的
no_data值:
# No data value for output
no_data = 0
- 接下来,我们设置输出图像的名称如下:
# Output elevation image name
elv_img = "elevation"
- 现在我们使用以下代码创建我们最小和最大海拔值对应的颜色:
# RGBA color of the SRTM minimum elevation
min_clr = (255, 255, 255, 0)
# RGBA color of the SRTM maximum elevation
max_clr = (0, 0, 0, 0)
# No data color
zero_clr = (255, 255, 255, 255)
- 然后,我们设置我们的输出图像大小,如下所示:
# Pixel width and height of the
# output images
w = 800
h = 800
现在我们了解了函数的工作原理,让我们解析 GPX。
解析 GPX
现在,我们将使用内置的 xml.dom.minidom模块解析 GPX 文件,它只是 XML。我们将提取纬度、经度、海拔和时间戳,并将它们存储在列表中以供以后使用。时间戳使用 Python 的time模块转换为struct_time对象,这使得处理更加容易。
解析需要执行以下步骤:
- 首先,我们使用
minidom模块解析gpx文件:
# Parse the gpx file and extract the coordinates
log.info("Parsing GPX file: {}".format(gpx))
xml = minidom.parse(gpx)
- 接下来,我们获取所有包含海拔信息的
"trkpt"标签:
# Grab all of the "trkpt" elements
trkpts = xml.getElementsByTagName("trkpt")
- 现在,我们设置列表以存储我们解析的位置和海拔值:
# Latitude list
lats = []
# Longitude list
lons = []
# Elevation list
elvs = []
# GPX timestamp list
times = []
- 然后,我们遍历 GPX 中的 GPS 条目并解析值:
# Parse lat/long, elevation and times
for trkpt in trkpts:
# Latitude
lat = float(trkpt.attributes["lat"].value)
# Longitude
lon = float(trkpt.attributes["lon"].value)
lats.append(lat)
lons.append(lon)
# Elevation
elv = trkpt.childNodes[0].firstChild.nodeValue
elv = float(elv)
elvs.append(elv)
时间戳需要一点额外的工作,因为我们必须将 GMT 时间转换为本地时间:
# Times
t = trkpt.childNodes[1].firstChild.nodeValue
# Convert to local time epoch seconds
t = get_local_time(t)
times.append(t)
解析 GPX 后,我们需要路线的边界框以从其他地理空间服务下载数据。
获取边界框
当我们下载数据时,我们希望数据集覆盖的区域比路线更广,这样地图就不会在路线的边缘裁剪得太紧。因此,我们将边界框的每侧缓冲 20%。最后,我们需要以东西和南北方向的数据来与 WMS 服务一起工作。东西和南北是笛卡尔坐标系中点的x和y坐标,以米为单位。它们在 UTM 坐标系中常用:
- 首先,我们按照以下方式从坐标列表中获取范围:
# Find Lat/Long bounding box of the route
minx = min(lons)
miny = min(lats)
maxx = max(lons)
maxy = max(lats)
- 接下来,我们缓冲边界框以确保轨迹不会靠近边缘:
# Buffer the GPX bounding box by 20%
# so the track isn't too close to
# the edge of the image.
xdist = maxx - minx
ydist = maxy - miny
x20 = xdist * .2
y20 = ydist * .2
# 10% expansion on each side
minx -= x20
miny -= y20
maxx += x20
maxy += y20
- 最后,我们在变量中设置我们的边界框并将坐标转换为米,这是网络服务所要求的:
# Store the bounding box in a single
# variable to streamline function calls
bbox = [minx, miny, maxx, maxy]
# We need the bounding box in meters
# for the OSM WMS service. We will
# download it in degrees though to
# match the SRTM file. The WMS spec
# says the input SRS should match the
# output but this custom service just
# doesn't work that way
mminx, mminy = ll2m(miny, minx)
mmaxx, mmaxy = ll2m(maxy, maxx)
通过这种方式,我们现在将下载我们的地图和海拔图像。
下载地图和海拔图像
我们首先下载作为底图的 OSM 底图,它包含街道和标签:
- 首先,我们将使用
log.info下载 OSM 底图:
# Download the OSM basemap
log.info("Downloading basemap")
wms(mminx, mminy, mmaxx, mmaxy, osm_WMS, osm_lyr,
osm_epsg, osm_style, osm_img, w, h)
本节将生成如下截图所示的中间图像:

- 接下来,我们将从SRTM数据集中下载一些海拔数据。SRTM 几乎覆盖全球,提供 30-90 米的分辨率。
SRTM.pyPython 模块使得处理这些数据变得容易。SRTM.py下载数据并设置请求。因此,如果您从不同地区下载数据,您可能需要清理位于您家目录中的缓存(~/.srtm)。这部分脚本可能需要 2-3 分钟才能完成,具体取决于您的计算机和互联网连接速度:
# Download the SRTM image
# srtm.py downloader
log.info("Retrieving SRTM elevation data")
# The SRTM module will try to use a local cache
# first and if needed download it.
srt = srtm.get_data()
# Get the image and return a PIL Image object
image = srt.get_image((w, h), (miny, maxy), (minx, maxx),
300, zero_color=zero_clr, min_color=min_clr,
max_color=max_clr)
# Save the image
image.save(elv_img + ".png")
脚本的这一部分也会输出中间海拔图像,如下截图所示:

现在我们有了海拔图像,我们可以将其转换为阴影图。
创建阴影图
我们可以将这些数据通过与第七章中创建阴影图部分相同的阴影图算法运行。为此,让我们遵循以下步骤:
- 首先,我们打开我们的高程图像并将其读入一个
numpy数组:
# Hillshade the elevation image
log.info("Hillshading elevation data")
im = Image.open(elv_img + ".png").convert("L")
dem = np.asarray(im)
- 现在我们设置我们的处理窗口以通过网格移动并高效地分析小部分:
# Set up structure for a 3x3 windows to
# process the slope throughout the grid
window = []
# x, y resolutions
xres = (maxx-minx)/w
yres = (maxy-miny)/h
- 然后,我们将高程图像分割成如下窗口:
# Create the windows
for row in range(3):
for col in range(3):
window.append(dem[row:(row + dem.shape[0]-2),
col:(col + dem.shape[1]-2)])
- 我们将创建用于处理的窗口数组如下:
# Process each cell
x = ((z * window[0] + z * window[3] + z * window[3] + z * window[6]) -
(z * window[2] + z * window[5] + z * window[5] + z * window[8])) \
/ (8.0 * xres * scale)
y = ((z * window[6] + z * window[7] + z * window[7] + z * window[8]) -
(z * window[0] + z * window[1] + z * window[1] + z * window[2])) \
/ (8.0 * yres * scale)
- 最后,由于
numpy,我们可以一次性处理它们:
# Calculate slope
slope = 90.0 - np.arctan(np.sqrt(x*x + y*y)) * rad2deg
# Calculate aspect
aspect = np.arctan2(x, y)
# Calculate the shaded relief
shaded = np.sin(altitude * deg2rad) * np.sin(slope * deg2rad) \
+ np.cos(altitude * deg2rad) * np.cos(slope * deg2rad) \
* np.cos((azimuth - 90.0) * deg2rad - aspect)
shaded = shaded * 255
现在我们有了等高线层,我们可以开始创建地图。
创建地图
我们已经有了开始构建报告地图所需的数据。我们的方法如下:
-
使用过滤器增强高程和底图图像
-
将图像混合在一起以提供等高线阴影 OSM 地图
-
创建一个半透明层来绘制街道路线
-
将路线层与等高线阴影地图混合
这些任务都将使用 PIL Image和ImageDraw模块完成,如下所示:
- 首先,我们将阴影高程
numpy数组转换回图像并平滑它:
# Convert the numpy array back to an image
relief = Image.fromarray(shaded).convert("L")
# Smooth the image several times so it's not pixelated
for i in range(10):
relief = relief.filter(ImageFilter.SMOOTH_MORE)
log.info("Creating map image")
- 现在我们将增加图像的对比度,使其更加突出:
# Increase the hillshade contrast to make
# it stand out more
e = ImageEnhance.Contrast(relief)
relief = e.enhance(2)
- 接下来,我们将地图图像裁剪到与高程图像相同的大小:
# Crop the image to match the SRTM image. We lose
# 2 pixels during the hillshade process
base = Image.open(osm_img + ".jpg").crop((0, 0, w-2, h-2))
- 然后我们增加地图图像的对比度,并将其与等高线图像混合:
# Enhance basemap contrast before blending
e = ImageEnhance.Contrast(base)
base = e.enhance(1)
# Blend the the map and hillshade at 90% opacity
topo = Image.blend(relief.convert("RGB"), base, .9)
- 现在我们准备在混合地图上绘制 GPS 轨迹,首先将我们的点转换为像素:
# Draw the GPX tracks
# Convert the coordinates to pixels
points = []
for x, y in zip(lons, lats):
px, py = world2pixel(x, y, w, h, bbox)
points.append((px, py))
- 我们还需要从即将创建的轨迹图像的边缘缓冲区中减去缓冲区:
# Crop the image size values to match the map
w -= 2
h -= 2
- 接下来,我们创建一个透明图像,并将我们的轨迹作为红线绘制:
# Set up a translucent image to draw the route.
# This technique allows us to see the streets
# and street names under the route line.
track = Image.new('RGBA', (w, h))
track_draw = ImageDraw.Draw(track)
# Route line will be red at 50% transparency (255/2=127)
track_draw.line(points, fill=(255, 0, 0, 127), width=4)
- 现在我们可以使用以下代码将轨迹粘贴到我们的图像上:
# Paste onto the basemap using the drawing layer itself
# as a mask.
topo.paste(track, mask=track)
- 现在我们将在路线上绘制一个起点,如下所示:
# Now we'll draw start and end points directly on top
# of our map - no need for transparency
topo_draw = ImageDraw.Draw(topo)
# Starting circle
start_lon, start_lat = (lons[0], lats[0])
start_x, start_y = world2pixel(start_lon, start_lat, w, h, bbox)
start_point = [start_x-10, start_y-10, start_x+10, start_y+10]
topo_draw.ellipse(start_point, fill="lightgreen", outline="black")
start_marker = [start_x-4, start_y-4, start_x+4, start_y+4]
topo_draw.ellipse(start_marker, fill="black", outline="white")
- 下面的代码片段是终点:
# Ending circle
end_lon, end_lat = (lons[-1], lats[-1])
end_x, end_y = world2pixel(end_lon, end_lat, w, h, bbox)
end_point = [end_x-10, end_y-10, end_x+10, end_y+10]
topo_draw.ellipse(end_point, fill="red", outline="black")
end_marker = [end_x-4, end_y-4, end_x+4, end_y+4]
topo_draw.ellipse(end_marker, fill="black", outline="white")
现在我们已经绘制了轨迹,我们准备放置我们的地理标记照片。
定位照片
我们将使用带有 GPS 位置坐标的手机照片。你可以从以下链接下载:
raw.githubusercontent.com/GeospatialPython/Learn/master/RoutePhoto.jpg。
将图像放置在与脚本同一级别的名为photos的目录中。我们只会使用一张照片,但脚本可以处理你想要的任意多张图片。我们将在地图上绘制并放置一个照片图标,然后按照以下步骤保存完成的底图:
- 首先,我们使用以下代码获取图像列表:
# Photo icon
images = glob.glob("photos/*.jpg")
- 然后,我们遍历每个图像并获取其 GPS 信息:
for i in images:
e = exif(i)
- 然后,我们使用我们的 GPS 函数如下解析位置信息:
photo_lat, photo_lon = gps(e)
#photo_lat, photo_lon = 30.311364, -89.324786
- 现在,我们可以将照片坐标转换为图像像素坐标:
photo_x, photo_y = world2pixel(photo_lon, photo_lat, w, h, bbox)
- 然后,我们将使用以下代码为照片位置绘制一个图标:
topo_draw.rectangle([photo_x - 12, photo_y - 10, photo_x + 12, \
photo_y + 10], fill="black", outline="black")
topo_draw.rectangle([photo_x - 9, photo_y - 8, photo_x + 9, \
photo_y + 8], fill="white", outline="white")
topo_draw.polygon([(photo_x-8,photo_y+7), (photo_x-3,photo_y-1), (photo_x+2,photo_y+7)], fill = "black")
topo_draw.polygon([(photo_x+2,photo_y+7), (photo_x+7,photo_y+3), (photo_x+8,photo_y+7)], fill = "black")
- 最后,我们将以如下方式保存我们的地图:
# Save the topo map
topo.save("{}_topo.jpg".format(osm_img))
尽管没有保存到文件系统,但等高线高程看起来如下:

混合地形地图看起来如下截图:

虽然阴影映射可以给我们一个关于海拔的印象,但它不提供任何定量数据。为了更详细,我们将创建一个简单的高程图表。
测量海拔
使用出色的 Google Chart API,我们可以快速构建一个很好的高程剖面图表,显示海拔在路线上的变化:
- 首先,我们将创建用于高程剖面的
chart对象:
# Build the elevation chart using the Google Charts API
log.info("Creating elevation profile chart")
chart = SimpleLineChart(600, 300, y_range=[min(elvs), max(elvs)])
- 现在,我们需要创建一个表示最小值的线,如下所示:
# API quirk - you need 3 lines of data to color
# in the plot so we add a line at the minimum value
# twice.
chart.add_data([min(elvs)]*2)
chart.add_data(elvs)
chart.add_data([min(elvs)]*2)
# Black lines
chart.set_colours(['000000'])
- 接下来,我们可以按照以下方式填写我们的高程剖面:
# fill in the elevation area with a hex color
chart.add_fill_range('80C65A', 1, 2)
- 然后,我们可以按照以下方式设置高程标签并将它们分配给一个轴:
# Set up labels for the minimum elevation, halfway value, and max value
elv_labels = int(round(min(elvs))), int(min(elvs)+((max(elvs)-min(elvs))/2))
# Assign the labels to an axis
elv_label = chart.set_axis_labels(Axis.LEFT, elv_labels)
- 接下来,我们可以使用以下代码为轴本身添加标签:
# Label the axis
elv_text = chart.set_axis_labels(Axis.LEFT, ["FEET"])
# Place the label at 30% the distance of the line
chart.set_axis_positions(elv_text, [30])
- 现在,我们可以计算轨迹点之间的距离:
# Calculate distances between track segments
distances = []
measurements = []
coords = list(zip(lons, lats))
for i in range(len(coords)-1):
x1, y1 = coords[i]
x2, y2 = coords[i+1]
d = haversine(x1, y1, x2, y2)
distances.append(d)
total = sum(distances)
distances.append(0)
j = -1
我们已经有了高程剖面,但我们需要在x轴上添加距离标记,以便我们知道剖面在路线上的变化位置。
测量距离
为了理解海拔数据图表,我们需要x轴上的参考点来帮助我们确定路线上的海拔。我们将计算路线上的英里分割,并将它们放置在图表 x 轴的适当位置。让我们看看以下步骤:
- 首先,我们按照以下方式在轴上定位英里标记:
# Locate the mile markers
for i in range(1, int(round(total))):
mile = 0
while mile < i:
j += 1
mile += distances[j]
measurements.append((int(mile), j))
j = -1
- 接下来,我们设置英里标记的标签:
# Set up labels for the mile points
positions = []
miles = []
for m, i in measurements:
pos = ((i*1.0)/len(elvs)) * 100
positions.append(pos)
miles.append(m)
# Position the mile marker labels along the x axis
miles_label = chart.set_axis_labels(Axis.BOTTOM, miles)
chart.set_axis_positions(miles_label, positions)
- 现在,我们可以按照以下方式为英里标记添加标签:
# Label the x axis as "Miles"
miles_text = chart.set_axis_labels(Axis.BOTTOM, ["MILES", ])
chart.set_axis_positions(miles_text, [50, ])
# Save the chart
chart.download('{}_profile.png'.format(elv_img))
我们的图表现在应该看起来像以下截图:

我们的第一张图表已经完成。现在,让我们看看路线上的天气数据。
获取天气数据
在本节中,我们将检索我们的最终数据元素:天气。如前所述,我们将使用 Dark Sky 服务,该服务允许我们收集世界上任何地方的天气历史报告。天气 API 是基于 REST 和 JSON 的,因此我们将使用urllib模块来请求数据,并使用json库来解析它。在本节中值得注意的是,我们将在本地缓存数据,这样您就可以在需要时离线运行脚本进行测试。在本节早期,您需要放置您的 Dark Sky API 密钥,该密钥由YOUR KEY HERE文本标记。让我们看看以下步骤:
- 首先,我们需要我们感兴趣区域的中心:
log.info("Creating weather summary")
# Get the bounding box centroid for georeferencing weather data
centx = minx + ((maxx-minx)/2)
centy = miny + ((maxy-miny)/2)
- 现在,我们按照以下方式设置免费的 Dark API 密钥,以便我们可以检索天气数据:
# DarkSky API key
# You must register for free at DarkSky.net
# to get a key to insert here.
api_key = "YOUR API KEY GOES HERE"
- 然后,我们获取我们用于天气查询的数据中的最新时间戳:
# Grab the latest route time stamp to query weather history
t = times[-1]
- 现在我们已经准备好按照以下方式执行我们的天气数据查询:
history_req = "https://api.darksky.net/forecast/{}/".format(api_key)
#name_info = [t.tm_year, t.tm_mon, t.tm_mday, route_url.split(".")[0]]
#history_req += "/history_{0}{1:02d}{2:02d}/q/{3}.json" .format(*name_info)
history_req += "{},{},{}".format(centy, centx, t)
history_req += "?exclude=currently,minutely,hourly,alerts,flags"
request = urllib.request.urlopen(history_req)
weather_data = request.read()
- 我们将像这样缓存天气数据,以防我们以后想查看它:
# Cache weather data for testing
with open("weather.json", "w") as f:
f.write(weather_data.decode("utf-8"))
- 然后,我们按照以下方式解析天气 JSON 数据:
# Retrieve weather data
js = json.loads(open("weather.json").read())
history = js["daily"]
- 我们需要的只是天气摘要,这是列表中的第一项:
# Grab the weather summary data.
# First item in a list.
daily = history["data"][0]
- 现在,我们将按照以下方式获取特定的天气属性:
# Max temperature in Imperial units (Farenheit).
# Celsius would be metric: "maxtempm"
maxtemp = daily["temperatureMax"]
# Minimum temperature
mintemp = daily["temperatureMin"]
# Maximum humidity
maxhum = daily["humidity"]
# Precipitation in inches (cm = precipm)
if "precipAccumulation" in daily:
precip = daily["precipAccumulation"]
else:
precip = "0.0"
- 现在我们已经将天气数据存储在变量中,我们可以完成最后一步:将其全部添加到 PDF 报告中。
在某些情况下,fpdf库除了 PIL 没有其他依赖。就我们的目的而言,它将工作得相当好。我们将继续向下添加页面元素。fpdf.ln()用于分隔行,而fpdf.cells包含文本,并允许更精确的布局。
我们终于准备好按照以下步骤创建我们的 PDF 报告:
- 首先,我们按照以下方式设置我们的
pdf对象:
# Simple fpdf.py library for our report.
# New pdf, portrait mode, inches, letter size
# (8.5 in. x 11 in.)
pdf = fpdf.FPDF("P", "in", "Letter")
- 然后,我们将为我们的报告添加一个页面并设置我们的字体偏好:
# Add our one report page
pdf.add_page()
# Set up the title
pdf.set_font('Arial', 'B', 20)
- 我们将使用以下代码为我们的报告创建一个标题:
# Cells contain text or space items horizontally
pdf.cell(6.25, 1, 'GPX Report', border=0, align="C")
# Lines space items vertically (units are in inches)
pdf.ln(h=1)
pdf.cell(1.75)
# Create a horizontal rule line
pdf.cell(4, border="T")
pdf.ln(h=0)
pdf.set_font('Arial', style='B', size=14)
- 现在,我们可以添加路线图,如下所示:
# Set up the route map
pdf.cell(w=1.2, h=1, txt="Route Map", border=0, align="C")
pdf.image("{}_topo.jpg".format(osm_img), 1, 2, 4, 4)
pdf.ln(h=4.35)
- 接下来,我们添加如下所示的高程图:
# Add the elevation chart
pdf.set_font('Arial', style='B', size=14)
pdf.cell(w=1.2, h=1, txt="Elevation Profile", border=0, align="C")
pdf.image("{}_profile.png".format(elv_img), 1, 6.5, 4, 2)
pdf.ln(h=2.4)
- 然后,我们可以使用以下代码编写天气数据摘要:
# Write the weather summary
pdf.set_font('Arial', style='B', size=14)
pdf.cell(1.2, 1, "Weather Summary", align="C")
pdf.ln(h=.25)
pdf.set_font('Arial', style='', size=12)
pdf.cell(1.8, 1, "Min. Temp.: {}".format(mintemp), align="L")
pdf.cell(1.2, 1, "Max. Hum.: {}".format(maxhum), align="L")
pdf.ln(h=.25)
pdf.cell(1.8, 1, "Max. Temp.: {}".format(maxtemp), align="L")
pdf.cell(1.2, 1, "Precip.: {}".format(precip), align="L")
pdf.ln(h=.25)
- 暗黑天空条款要求我们在报告中添加一个标志,以感谢优秀的数据来源:
# Give Dark Sky credit for a great service (https://git.io/fjwHl)
pdf.image("darksky.png", 3.3, 9, 1.75, .25)
- 现在,我们可以使用以下代码添加地理定位的图片:
# Add the images for any geolocated photos
pdf.ln(h=2.4)
pdf.set_font('Arial', style='B', size=14)
pdf.cell(1.2, 1, "Photos", align="C")
pdf.ln(h=.25)
for i in images:
pdf.image(i, 1.2, 1, 3, 3)
pdf.ln(h=.25)
- 最后,我们可以保存报告并查看它:
# Save the report
log.info("Saving report pdf")
pdf.output('report.pdf', 'F')
你应该在你的工作目录中有一个名为report.pdf的 PDF 文档,其中包含你的成品。它应该看起来像以下截图所示的图片:

通过这种方式,我们已经使用了本书中学到的所有技术,并构建了一个 GPS 报告工具。
摘要
恭喜!在这本书中,你汇集了成为一名现代地理空间分析师所需的最基本工具和技能。无论你偶尔使用地理空间数据还是一直使用它,你都将更好地利用地理空间分析。本书主要关注使用开源工具,这些工具几乎都可以在 PyPI 目录中找到,以便于安装和集成。但即使你使用 Python 作为商业 GIS 软件包或 GDAL 等流行库的驱动程序,纯 Python 测试新概念的能力也总是很有用的。
进一步阅读
Python 提供了一套丰富的库用于数据可视化。其中最突出的是Matplotlib,它可以生成多种类型的图表和地图,并将它们保存到 PDF 中。Packt 有几本关于 Matplotlib 的书,包括《Matplotlib 30 个食谱》:www.packtpub.com/big-data-and-business-intelligence/matplotlib-30-cookbook。


浙公网安备 33010602011771号