Python-地理空间分析学习手册-全-
Python 地理空间分析学习手册(全)
零、前言
这本书首先向您介绍了地理空间分析的背景,然后提供了所使用的技术和工艺流程,并将该领域分为其组成专业领域,例如地理信息系统 ( 地理信息系统)、遥感、高程数据、高级建模和实时数据。本书的重点是为使用强大的 Python 语言和框架来有效地进行地理空间分析奠定坚实的基础。在这样做的时候,我们将重点关注使用纯 Python 以及某些 Python 工具和 API,并使用通用算法。读者将能够分析各种形式的地理空间数据,了解实时数据跟踪,并了解如何将学到的知识应用到有趣的场景中。
虽然在整个示例中使用了许多第三方地理空间库,但我们将尽可能使用没有依赖关系的纯 Python。这种对纯 Python 3 示例的关注将使这本书区别于该领域几乎所有其他资源。我们还将浏览一些在这本书的早期版本中没有的流行图书馆。
这本书是给谁的
这本书是为任何想要理解数字制图和分析,并使用 Python 或任何其他脚本语言来手动自动化或处理数据的人准备的。本书主要面向希望使用 Python 执行地理空间建模和地理信息系统分析的 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。
- 选择“支持”选项卡。
- 点击代码下载。
- 在搜索框中输入图书的名称,并按照屏幕指示进行操作。
下载文件后,请确保使用最新版本的解压缩文件夹:
- 视窗系统的 WinRAR/7-Zip
- zipeg/izp/un ARX for MAC
- 适用于 Linux 的 7-Zip/PeaZip
这本书的代码包也托管在 GitHub 上,网址为https://GitHub . com/PacktPublishing/Learning-geography-Analysis-with-Python-第三版。如果代码有更新,它将在现有的 GitHub 存储库中更新。
我们还有来自丰富的图书和视频目录的其他代码包,可在【https://github.com/PacktPublishing/】获得。看看他们!
下载彩色图像
我们还提供了一个 PDF 文件,其中包含本书中使用的截图/图表的彩色图像。可以在这里下载:https://static . packt-cdn . com/downloads/9781789959277 _ color images . pdf。
使用的约定
本书通篇使用了许多文本约定。
CodeInText:表示文本中的码字、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟网址、用户输入和推特句柄。这里有一个例子:“为了演示这一点,下面的例子访问了我们刚才看到的同一个文件,但是使用了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
粗体:表示一个新的术语、一个重要的单词或者你在屏幕上看到的单词。例如,菜单或对话框中的单词像这样出现在文本中。下面是一个示例:“从管理面板中选择系统信息。”
Warnings or important notes appear like this. Tips and tricks appear like this.
取得联系
我们随时欢迎读者的反馈。
一般反馈:如果你对这本书的任何方面有疑问,在你的信息主题中提到书名,发邮件给我们customercare@packtpub.com。
勘误表:虽然我们已经尽了最大的努力来保证内容的准确性,但是错误还是会发生。如果你在这本书里发现了一个错误,如果你能向我们报告,我们将不胜感激。请访问www.packtpub.com/support/errata,选择您的图书,点击勘误表提交链接,并输入详细信息。
盗版:如果您在互联网上遇到任何形式的我们作品的非法拷贝,如果您能提供我们的位置地址或网站名称,我们将不胜感激。请通过copyright@packt.com联系我们,并提供材料链接。
如果你有兴趣成为一名作者:如果有一个你有专长的话题,你有兴趣写或者投稿一本书,请访问authors.packtpub.com。
复习
请留下评论。一旦你阅读并使用了这本书,为什么不在你购买它的网站上留下评论呢?然后,潜在的读者可以看到并使用您不带偏见的意见来做出购买决定,我们在 Packt 可以了解您对我们产品的看法,我们的作者可以看到您对他们的书的反馈。谢谢大家!
更多关于 Packt 的信息,请访问packt.com。
一、使用 Python 学习地理空间分析
地理空间技术目前正在影响我们的世界,因为它正在改变我们对人类历史的认识。在这本书里,我们将逐步了解地理空间分析的历史,它早于计算机甚至纸质地图。然后,我们将研究为什么您可能想要学习并使用一种编程语言作为地理空间分析师,而不是仅仅使用地理信息系统 ( 地理信息系统)应用程序。这将有助于我们理解让尽可能多的人能够访问地理空间分析的重要性。
在本章中,我们将涵盖以下主题:
- 地理空间分析和我们的世界
- 莎拉·帕尔卡博士与考古学
- 地理信息系统
- 遥感概念
- 高程数据
- 计算机辅助制图
- 地理空间分析和计算机编程
- 地理空间分析的重要性
- 地理信息系统概念
- 通用地理信息系统流程
- 常见的遥感过程
- 常见的栅格数据概念
- 创建尽可能简单的 Python 地理信息系统
是的,你没听错!我们将从一开始就使用 Python 从头开始构建最简单的地理信息系统。
技术要求
这本书假设你有一些 Python 编程语言的基本知识,基本的计算机知识,以及至少地理空间分析的意识。本章为地理空间分析提供了基础,这是研究遥感和地理信息系统领域的任何主题所必需的,包括本书所有其他章节中的材料。
本书中的例子基于 Python 3.4.3,可以在这里下载:https://www.python.org/downloads/release/python-343/。
地理空间分析和我们的世界
19 世纪 80 年代,英国探险家开始运用科学的严谨性来挖掘古代文化遗址。考古领域是一个令人沮丧的、低成本的、通常也是危险的工作,需要耐心和好运。地球非常擅长保守秘密和抹去人类努力的故事。不断变化的河流、洪水、火山、沙尘暴、飓风、地震、火灾和其他事件将整个城市吞噬到周围的景观中,我们因时间的流逝而失去了它们。
我们对人类历史的了解是基于通过考古挖掘对古代文化的一瞥,以及对我们有幸通过有根据的猜测或反复试验偶然发现的遗址的研究。在过去,除非一个团队挖掘了一个遗址,发现了一些东西,并正确地识别了它,否则考古是不会成功的。关于去哪里寻找的预测是基于一些主要因素,如支持农业所需的水的接近度、以前发现的地点、早期探险者的描述以及其他广泛的线索。
2007 年,来自伯明翰阿拉巴马大学的考古学家萨拉·帕尔卡克博士开始劝说我们顽固的地球揭露人类去过哪里和做过什么的秘密。从那以后,她的方法彻底改变了考古学领域。
在短短的几年里,帕尔卡克博士和她的团队发现了 17 座金字塔、1000 多座坟墓的痕迹,以及埃及 3000 个古代定居点的脚印,包括著名的失落之城坦尼斯的城市网格。她确定了罗马尼亚、纳巴泰王国和突尼斯的重要考古遗址。她在挖掘得很好的古罗马港口葡萄牙找到了一个竞技场,以及它的灯塔和通往罗马的靠近台伯河的运河。
她是如何发现这么多隐藏了近两个世纪仍未被发现的宝藏的?她看着更大的画面。帕尔卡克博士完善了利用卫星图像在离地球近 400 英里的地方定位古代遗址的技术。她的职业生涯恰好与随时可用的高分辨率卫星图像的出现相吻合,该图像具有 10 英寸或更小的像素分辨率,从而提供了检测景观细微变化所需的细节,从而显示了古代遗址。
尽管她的发现数量巨大,意义重大,但从太空中寻找文化遗产需要大量的工作。太空考古学家首先研究旧地图和历史记录。然后,他们查看现有站点的现代数字地图。他们还研究数字地形模型,以确定古代人为了躲避洪水而建造的土地上的细微隆起。然后,他们使用多光谱图像,包括红外线,这可以暴露植被或土壤的变化,当处理时,由于进口的石头和其他材料埋在地下,气泡上升到表面。这种由假颜色表示的变色,让我们能够区分从地面上完全看不见的地方反射的阳光的带宽,甚至从空中反射到肉眼,它们突然以鲜明的对比脱颖而出,在卫星图像上显示出精确的位置。
古代文化遗址从地面上往往是肉眼看不见的。例如,下面的照片显示了美国伊利诺伊州刘易斯顿附近一个保存完好的美洲土著人墓地,由于它的位置,它已经存活了几千年,很容易被看到:

然而,在气候条件恶劣的地区,遗址可能会被部分破坏,因此很难找到。下面这张照片展示了路易斯安那州的一片沼泽地,这里遍布着古老的美洲土著人埋葬土墩,这些土墩已经被侵蚀了几个世纪,如果没有卫星图像,现在几乎无法探测到:

下面这张经过处理的卫星图像来自美国宇航局科学家马尔科·贾尔迪诺博士,它和上一张照片位于同一个沼泽地区,显示了从地面上看不到的四个不同埋葬土堆的遗迹。尽管这个地方已经有几百年的历史了,但与周围的沼泽相比,这里的植被种类和健康状况是不同的。虽然考古学家在该地区研究了几十个类似的遗址,但该项目首次确定土堆建造者经常使用一种将土堆放置在四个主要方向(北、南、西、东)的模式,这种模式在太空中高度可见,但在地面上很难实现:

尽管太空考古学家在寻找古代遗址方面速度很快,但他们现在发现自己面临的不仅仅是地质和气象因素。抢劫一直是考古界的一个威胁,但由于战争和黑市文物,这已经成为一个更大的问题。现代建筑也会破坏有价值的遗址。然而,坚定的考古学家正在使用他们用来发现上述遗址的相同技术来检测抢劫或建筑威胁。一旦他们发现威胁的证据,他们就会通知政府,这样他们就可以进行干预。下图显示了在叙利亚东部的罗马杜拉欧罗普斯遗址发生抢劫的证据。被圈起来的区域有盗墓者挖的洞:

除了卫星图像处理和视觉解释,太空考古学家还使用地理信息系统制图技术来标记或数字化遗址,覆盖现代道路和城市足迹,创建标记地图,等等。令人兴奋的空间考古新领域是我们将在本书中介绍的地理空间分析的许多最新应用之一。
**Beyond archaeology: **Geospatial analysis can be found in almost every industry, including real estate, oil and gas, agriculture, defense, disaster management, health, transportation, and oceanography, to name a few. For a good overview of how geospatial analysis is used in dozens of different industries, go to https://www.esri.com/what-is-gis/who-uses-gis.
地理空间分析的历史
地理空间分析可以追溯到 15000 年前,法国西南部的拉斯科洞穴。在这个洞穴里,旧石器时代的艺术家绘制了常见的狩猎动物和许多专家认为的天文星图,用于宗教仪式,甚至潜在的猎物迁移模式。虽然粗糙,这些画展示了一个古代的例子,人类创造了他们周围世界的抽象模型,并关联时空特征来寻找关系。下面的照片展示了其中一幅画,上面覆盖着星图:

几个世纪以来,制图学艺术和土地测量科学得到了发展,但直到 19 世纪,地理分析才出现了重大进展。1830 年至 1860 年间欧洲致命的霍乱爆发导致巴黎和伦敦的地理学家使用地理分析进行流行病学研究。
1832 年,查尔斯·皮奎特(Charles Picquet)在一份关于霍乱爆发的报告中,使用不同的半色调灰色来代表巴黎 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," which is translated as "Figurative map of the successive losses of men of the French army in the Russian Campaign 1812-13."
这描绘了 1812 年俄国战役中拿破仑军队的大屠杀。这张地图显示了军队的规模和位置,以及当时的天气状况。下图包含单个主题的四个不同系列的信息。这是一个用笔和纸进行地理分析的极好例子。军队的规模由棕色和黑色条带的宽度代表,比例为每 10,000 人一毫米。数字也是沿着狭长地带写的。棕色的路径显示了进入俄罗斯的士兵,而黑色的路径代表了那些成功的士兵。地图比例显示在中央右侧,为一个法国联赛(2.75 英里或 4.4 公里)。底部的图表从右到左,描绘了士兵们在从俄罗斯回家的路上经历的严寒温度:

米纳尔德发布了另一张引人注目的地图,记录了从法国各地送往巴黎的牛的数量,尽管这远比一场战争更普通。米纳德在法国各地区使用不同大小的饼图来显示每个地区的牛的品种和出货量:

20 世纪初,大规模印刷推动了地图图层概念的发展——这是地理空间分析的一个关键特征。制图员在玻璃板上绘制不同的地图元素(植被、道路和高程轮廓),然后将这些元素堆叠起来拍照,打印成一幅图像。如果制图员犯了一个错误,只需要更换一块玻璃,而不是整个地图。后来,塑料薄片的发展使得以这种方式创建、编辑和存储地图变得更加容易。然而,地图的分层概念作为分析的一种好处直到现代计算机时代才开始发挥作用。
地理信息系统
20 世纪 60 年代,计算机制图随着计算机本身而发展。然而,地理信息系统一词的起源始于加拿大林业和农村发展部。罗杰·汤姆林森博士领导了一个由 40 名开发人员组成的团队,他们与 IBM 达成协议,将建设加拿大地理信息系统(T1)(T2)CGIS(T3)。CGIS 跟踪了加拿大的自然资源,并允许对这些特征进行剖面分析,以供进一步分析。CGIS 将每种类型的土地覆盖储存为不同的层。
它还将数据存储在适用于整个国家的加拿大特定坐标系中,该坐标系是为优化面积计算而设计的。尽管以今天的标准来看,所使用的技术还很原始,但该系统在当时具有非凡的能力。CGIS 包括看起来相当现代的软件功能:
- 地图投影切换
- 扫描图像的橡胶薄片
- 地图比例变化
- 线平滑和泛化以减少要素中的点数
- 多边形的自动间隙闭合
- 面积测量
- 多边形的分解和合并
- 几何缓冲
- 新多边形的创建
- 扫描
- 参考数据中新特征的数字化
The National Film Board of Canada produced a documentary in 1967 on the CGIS, which can be viewed at the following URL: https://youtu.be/3VLGvWEuZxI.
汤姆林森经常被称为地理信息系统之父。在发起 CGIS 之后,他凭借 1974 年的论文获得了伦敦大学的博士学位,该论文题为“电子计算方法和技术在地图数据存储、编辑和评估中的应用”,描述了地理信息系统和地理空间分析。汤姆林森现在经营着自己的全球咨询公司汤姆林森联合有限公司,他仍然是这个行业的积极参与者。他经常在地理空间会议上发表主旨演讲。
根据本书的定义,CGIS 是地理空间分析的起点。然而,如果不是霍华德·费舍尔和哈佛大学设计研究生院的哈佛计算机图形和空间分析实验室的工作,这本书就不会写成。他在 SYMAP 地理信息系统软件上的工作开创了实验室的发展时代,该软件产生了另外两个重要的包,并从整体上永久地定义了地理空间行业。SYMAP 带来了其他包,包括来自同一个实验室的 GRID 和奥德赛项目:
- 网格是一个基于栅格的地理信息系统,它使用像元来表示地理特征,而不是几何。《全球资源数据库》是卡尔·斯坦尼茨和大卫·辛顿写的。该系统后来成为 IMGRID。
- 《奥德赛》是由尼克·克里斯曼和丹尼斯·怀特领导的团队合作。这是一个程序系统,其中包括许多现代地理数据库系统典型的高级地理空间数据管理功能。哈佛试图将这些包装商业化,但成功有限。然而,它们的影响今天仍然可见。
事实上,每一个现有的商业和开源包都与这些代码库有关。
霍华德·费舍尔(Howard Fisher)制作了一部 1967 年的电影,利用 SYMAP 的输出,通过将几十年的房产信息手工编码到系统中,展示了密歇根州兰辛市从 1850 年到 1965 年的城市扩张。这种分析花了几个月的时间,但是现在由于现代工具和数据的原因,只需要几分钟就可以重建它们。
You can watch the film at https://www.youtube.com/watch?v=xj8DQ7IQ8_o.
现在有几十个图形用户界面 ( 图形用户界面)地理空间桌面应用程序,这些应用程序可以从包括 Esri、ERDAS、Intergraph、ENVI 等公司获得。Esri 是历史最悠久、持续运营的地理信息系统软件公司,始于 20 世纪 60 年代末。在开源领域,包括量子地理信息系统 ( QGIS )和地理资源分析支持系统 ( GRASS )在内的包被广泛使用。除了全面的桌面软件包,用于构建新软件的软件库有成千上万个。
地理信息系统可以提供关于地球的详细信息,但它仍然只是一个模型。有时,我们需要一个直接的代表,以便获得关于我们星球上当前或最近变化的知识。到那时,我们需要遥感。
遥感
遥感是指在不与某个物体发生物理接触的情况下,收集该物体的信息。在地理空间分析的背景下,该物体通常是地球。遥感还包括处理收集的信息。地理信息系统的潜力只受到现有地理数据的限制。土地测量的成本,即使使用现代全球定位系统来填充地理信息系统,也一直是资源密集型的。
遥感的出现不仅大大降低了地理空间分析的成本,而且将该领域带入了全新的方向。除了为地理信息系统提供强大的参考数据之外,遥感还通过从图像和地理数据中提取特征,使自动和半自动生成地理信息系统数据成为可能。1858 年,古怪的法国摄影师加斯帕德·菲丽克斯·图尔纳琼(又名纳达尔)在巴黎上空的热气球上拍摄了第一张航拍照片:

真正鸟瞰世界的价值是显而易见的。早在 1920 年,关于航拍判读的书籍就开始出现。
当美国在二战后与苏联进入冷战时,随着美国 U-2 侦察机的发明,监控军事能力的航空摄影变得多产。U-2 侦察机可以在 75,000 英尺的高度飞行,这使它超出了现有防空武器的设计范围,只能达到 50,000 英尺。当苏联最终击落一架 U-2 并俘获飞行员时,美国在俄罗斯上空的 U-2 飞行结束了。
然而,航空摄影对现代地理空间分析几乎没有影响。飞机只能捕捉一个区域的小脚印。照片被钉在墙上或放在光桌上检查,但不是在其他信息的背景下。尽管非常有用,航拍照片解释只是另一种视觉视角。
游戏规则的改变发生在 1957 年 10 月 4 日,当时苏联发射了人造卫星 1 号。由于制造上的困难,苏联已经废弃了一个更加复杂和精密的卫星原型。一旦被纠正,这个原型后来成为人造卫星 3 号。相反,他们选择了一个简单的金属球体,带有四根天线和一个简单的无线电发射器。包括美国在内的其他国家也在研究卫星。这些卫星计划并不完全是秘密。作为国际地球物理年的一部分,他们受到科学动机的驱使(T2 IGY T3)。
火箭技术的进步使人造卫星成为地球科学的自然进化。然而,几乎在每一个案例中,每个国家的国防机构都参与其中。与苏联类似,其他国家也在与装满科学仪器的复杂卫星设计作斗争。苏联人决定改用最简单的设备,唯一的原因是在美国人有效之前发射了一颗卫星。当人造卫星经过时,天空中可以看到它,业余无线电操作员可以听到它的无线电脉冲。尽管人造卫星很简单,但它提供了宝贵的科学信息,这些信息可以从它的轨道力学和射频物理中获得。
人造卫星计划最大的影响是对美国太空计划的影响。美国的主要对手在太空竞赛中获得了巨大的优势。美国最终以阿波罗登月作为回应。然而,在此之前,美国启动了一项直到 1995 年仍是国家机密的计划。分类电晕计划产生了第一批来自太空的照片。美国和苏联签署了一项终止间谍飞机飞行的协议,但卫星显然没有参与谈判。
下图显示了电晕过程。虚线是卫星的飞行路线,白色的长管是卫星,白色的小圆锥体是胶片罐,黑色的斑点是控制站,它们触发了胶片的弹出,这样飞机就可以在空中捕捉到它:

第一颗 CORONA 卫星是一项历时 4 年的努力,经历了许多挫折。然而,该计划最终成功了。即使在今天,卫星成像的困难在于从太空中获取图像。CORONA 卫星使用了黑白胶片罐,这些胶片一旦曝光就会从车辆中弹出。当一个胶片筒空降到地球上时,一架美国军用飞机会在半空中接住这个包裹。如果飞机错过了滤罐,它会在水中漂浮一小段时间,然后沉入海洋,以保护敏感信息。
美国继续开发 CORONA 卫星,直到它们与 U-2 侦察机照片的分辨率和摄影质量相匹配。CORONA 仪器的主要缺点是可重用性和及时性。一旦脱离胶片,卫星就不能再使用了。此外,电影的恢复是在一个固定的时间表,使该系统不适合监测实时情况。然而,CORONA 计划的全面成功为下一波卫星铺平了道路,从而开启了现代遥感时代。
由于 CORONA 计划的秘密地位,它对遥感的影响是间接的。美国载人航天任务中拍摄的地球照片激发了民用遥感卫星的想法。这种卫星的好处是显而易见的,但这个想法仍然有争议。政府官员质疑卫星是否像航空摄影一样划算。军方担心这颗公共卫星会危及科罗纳计划的保密性。其他官员担心未经许可对其他国家进行成像的政治后果。然而,内政部最终获得了美国宇航局制造一颗卫星来监测地球表面资源的许可。
1972 年 7 月 23 日,美国宇航局发射了地球资源技术卫星 ( ERTS )。ERTS 号很快被重新命名为陆地卫星 1 号。该平台包含两个传感器。第一个是返回光束摄像机 ( RBV )传感器,本质上是一个摄像机。它是由被称为美国无线电公司的广播电视巨头建造的。RBV 号立即出现了问题,包括关闭卫星的高度引导系统。对卫星的第二次尝试是高度实验性的多光谱扫描仪 ( MSS )。MSS 表现完美,产生了比 RBV 更好的结果。MSS 捕获了从地球表面反射的四种不同波长的光的四幅独立图像。
这种传感器具有几项革命性的功能。第一个也是最重要的能力是第一次对行星进行全球成像,每 16 天扫描地球上的每个点。美国宇航局的下图展示了这种飞行和收集模式,这是传感器围绕地球运行时的一系列重叠条带,每次传感器对地球上的一个位置成像时都会捕获数据切片:

它还记录了可见光谱以外的光。虽然它确实捕获了人眼可见的绿光和红光,但它也扫描了人眼不可见的两种不同波长的近红外光。这些图像被存储并以数字方式传输到马里兰州、加利福尼亚州和阿拉斯加州的三个不同的地面站。它的多光谱能力和数字格式意味着陆地卫星提供的鸟瞰图不仅仅是另一张天空照片。它正在传送数据。计算机可以处理这些数据,输出关于地球的衍生信息,就像地理信息系统通过分析一个地理特征与另一个地理特征的关系来提供关于地球的衍生信息一样。美国国家航空航天局在全球范围内推广使用陆地卫星,并以非常实惠的价格向任何提出要求的人提供数据。
这种全球成像能力导致了许多科学突破,包括发现了以前未知的地理,这发生在 1976 年。例如,加拿大政府利用大地卫星图像找到了一个北极熊居住的未知小岛。他们给新的陆地命名为陆地卫星岛。
陆地卫星 1 号之后还有 6 个任务移交给了作为负责机构的国家海洋和大气管理局(T2)和美国国家海洋和大气管理局。由于歧管破裂,陆地卫星 6 号未能进入轨道,这使其机动发动机失效。在其中一些任务中,卫星由地球观测卫星 ( EOSAT )公司管理,该公司现在被称为空间成像,但由 Landsat 7 任务归还给政府管理。美国国家航空航天局的以下图像是陆地卫星 7 号产品的样本:

陆地卫星数据连续性任务 ( LDCM )于 2013 年 2 月 13 日发射,并于 2013 年 4 月 27 日开始收集图像,作为其校准周期的一部分,成为陆地卫星 8 号。LDCM 号是美国宇航局和美国地质调查局(T4)的联合任务。
高程数据
遥感数据可以二维测量地球。但是我们也可以使用遥感技术,通过数字高程数据来测量地球的三维空间,这些数据包含在数字高程模型中。一个数字高程模型 ( 数字高程模型)是一个行星地形的三维表示。在这本书的上下文中,这个星球就是地球。数字高程模型的历史远没有遥感影像复杂,但意义同样重大。在计算机出现之前,高程数据的表示仅限于通过传统的土地测量创建的地形图。这项技术的存在是为了从立体图像中创建三维模型,或者从粘土或木材等材料中创建物理模型,但这些方法并没有广泛用于地理。
数字高程模型的概念产生于 1986 年,当时法国航天局国家空间研究中心(T1)(【CNES】)或国家空间研究中心(T5)发射了其 SPOT-1 卫星,其中包括一个立体雷达。该系统创建了第一个可用的数字高程模型。其他几颗美国和欧洲卫星也以类似的任务遵循这一模式。
2000 年 2 月,奋进号航天飞机执行了“T0”号航天飞机雷达地形任务“T1”(“T2”号“SRTM”号“T3”),该任务使用允许一次通过的特殊雷达天线配置收集了地球表面 80%以上的高程数据。这个模型在 2009 年被美国和日本的联合任务超越,该任务使用了美国宇航局 Terra 卫星上的高级星载热发射和反射辐射计 ( ASTER )传感器。该系统捕获了地球表面的 99%,但已被证明存在小的数据问题。由于航天飞机的轨道没有越过地球的两极,它没有捕获整个表面。SRTM 仍然是金本位制。下图来自美国地质勘探局(https://www . USGS . gov/media/img/national-elevation-dataset)显示了一个被称为山体阴影的彩色 DEM。绿色区域是低海拔地区,而黄色和棕色区域是中高海拔地区:

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

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

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

数据集的凸包类似于边界框,但它不是正方形,而是可能包含数据集的最小多边形。下图显示了与上一示例相同的点数据,凸包多边形显示为红色:

如您所见,数据集的边界框总是包含凸包。
关于多边形的地理空间规则
在地理空间分析中,关于多边形有几种不同于多边形数学描述的一般经验法则:
- 多边形必须至少有四个点,第一个点和最后一个点必须相同
- 多边形边界不应重叠
- 图层中的多边形不应该重叠
- 另一个多边形内的图层中的多边形被视为基础多边形中的一个洞
不同的地理空间软件包和库以不同的方式处理这些规则的异常,这可能会导致混乱的错误或软件行为。最安全的路线是确保你的多边形遵守这些规则。还有一条关于多边形的重要信息我们需要谈谈。
根据定义,多边形是封闭的形状,这意味着多边形的第一个和最后一个顶点是相同的。如果您没有将第一个点显式复制为面数据集中的最后一个点,某些地理空间软件会抛出错误。其他软件会自动关闭多边形而不会抱怨。用于存储地理空间数据的数据格式也可能决定多边形的定义方式。这个问题是一个灰色区域,所以它没有制定多边形规则,但是知道这个怪癖会在某一天当你遇到一个你无法轻易解释的错误时派上用场。
缓冲器
缓冲操作可以应用于空间对象,包括点、线或多边形。该操作在对象周围指定距离处创建一个多边形。缓冲操作用于邻近分析:例如,在危险区域周围建立安全区。让我们回顾一下这个图表:

黑色形状表示原始几何图形,而红色轮廓表示从原始形状生成的较大缓冲多边形。
溶解
融合操作从相邻多边形中创建一个多边形。溶解还用于简化从遥感提取的数据,如下所示:

解散操作的一个常见用途是合并单一所有者购买的税务数据库中的两个相邻属性。
推广
具有超过地理空间模型所需的点的对象可以被一般化,以减少用于表示形状的点的数量。这种操作通常需要几次尝试才能在不影响整体形状的情况下获得最佳点数。这是一种数据优化技术,用于简化数据以提高计算效率或更好的可视化。这项技术在 web 地图应用程序中非常有用。
以下是多边形泛化的一个示例:

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

合并操作将两个或多个不重叠的形状合并到一个多形状对象中。多形状对象是保持独立几何的形状,但被 GIS 视为具有一组属性的单个要素:

基本的地理空间操作是检查点是否在多边形内。这一个操作是许多不同类型空间查询的原子构建块。如果该点在多边形的边界上,则认为它在内部。很少有空间查询不以某种方式依赖这种计算。然而,在大量点上它可能非常慢。
检测一个点是否在多边形内的最常见和最有效的算法叫做光线投射算法。首先,执行测试以查看该点是否在多边形边界上。接下来,该算法从所讨论的点沿一个方向画一条线。程序计算直线穿过多边形边界的次数,直到它到达多边形的边界框,如下所示:

边界框是可以围绕整个多边形绘制的最小框。如果数字是奇数,那么点在里面。如果边界交点的数量为偶数,则该点位于外部。
联盟
联合操作不太常见,但当您希望将两个或多个重叠的多边形组合成一个形状时,它非常有用。它类似于溶解,但在这种情况下,多边形是重叠的,而不是相邻的:

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

颜色用来显示森林最近被砍伐的程度。绿色代表原始雨林,白色代表日期范围结束后两年内砍伐的森林,红色代表 22 年内砍伐的森林,其他颜色介于两者之间,如传说中所述。
柱状图
直方图是数据集中值的统计分布。横轴表示数据集中的唯一值,纵轴表示栅格中该唯一值的频率。美国国家航空航天局的以下示例显示了卫星图像的直方图,该直方图被分为不同的类别,代表了下面的表面特征:

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

现在我们已经看到了海龟图形模块可以做什么,让我们用它来构建一个实际的 GIS!
构建简单的地理信息系统
代码分为两个不同的部分:
- 数据模型部分
- 绘制数据的地图渲染器
对于数据模型,我们将使用简单的 Python 列表。Python 列表是一种本机数据类型,以指定的顺序作为其他 Python 对象的容器。Python 列表可以包含其他列表,非常适合简单的数据结构。如果您决定要进一步开发您的脚本,它们还可以很好地映射到更复杂的结构甚至数据库。
代码的第二部分将使用 Python 海龟图形引擎呈现地图。在地理信息系统中,我们只有一个功能可以将世界坐标(在本例中是经度和纬度)转换为像素坐标。所有图形引擎都有一个原点 (0,0) ,它通常在画布的左上角或左下角。
海龟图形旨在直观地教授编程。海龟图形画布在中间使用了 (0,0) 的原点,类似于图形计算器。下图说明了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]
- 这些城市将被存储为嵌套列表。每个城市的位置由作为经度和纬度对的单个点组成。这些条目将完成我们的地理信息系统数据模型。我们将从一个名为
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])
- 现在,我们将通过首先定义地图大小来将地理信息系统数据渲染为地图。宽度和高度可以是您想要的任何值,具体取决于您的屏幕分辨率:
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]
激动人心的部分来了!我们准备将地理信息系统渲染为专题地图。
渲染地图
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()函数返回的像素坐标处绘制一个小圆,而不是通过移动笔来绘制线条。然后我们用城市的名字来标记这个点,并添加人口。你会注意到,我们必须将种群数量转换成一个字符串,以便在海龟(T2)方法中使用。为此,我们将使用 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()
- 现在,我们将执行最后一个操作来证明我们已经创建了一个真正的地理信息系统。我们将对数据执行属性查询,以确定哪个城市的人口最多。然后,我们将执行一个空间查询来查看哪个城市位于最西边。最后,我们将在主题地图页面上安全打印问题的答案,不在地图范围内。
- 对于我们的查询引擎,我们将使用 Python 内置的
min()和max()函数。这些函数将列表作为参数,并返回该列表的最小值和最大值。这些函数有一个特殊的特性,称为关键参数,允许您对复杂的对象进行排序。由于我们在数据模型中处理嵌套列表,我们将利用这些函数中的关键参数。key 参数接受一个函数,该函数在返回最终值之前临时更改列表进行计算。在这种情况下,我们想要隔离用于比较的总体值,然后是点。我们可以编写一个全新的函数来返回指定的值,但是我们可以使用 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 解释器还是将整个程序作为脚本运行,您都应该看到实时呈现的如下图:

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

它显示了使用天梭指示线的墨卡托投影造成的失真,天梭指示线在地图上投影大小相等的小椭圆。椭圆的变形清楚地显示了投影如何影响大小和距离:网络地图服务减少了搜寻数据的繁琐工作和分析师创建底图的大量预处理工作。但是,要创建任何有价值的东西,您必须了解地理空间数据以及如何使用它。本章概述了地理空间分析中常见的数据类型和问题。
在本章中,通常使用两个术语:
- 向量数据:向量数据包括使用点、线或多边形最低限度地表示地理位置数据的任何格式。
- 栅格数据:栅格数据包括以行和列的网格存储数据的任何格式。光栅数据包括所有图像格式。
这是大多数地理空间数据集可以归入的两个主要类别。
If you want to see a projection that shows the relative size of continents more accurately, refer to the Goode homolosine projection: https://en.wikipedia.org/wiki/Goode_homolosine_projection.
理解数据结构
尽管有几十种格式,地理空间数据有一些共同的特点。通过识别几乎所有空间数据共有的成分,了解这些特征可以帮助您接近和理解不熟悉的数据格式。给定数据格式的结构通常由其预期用途驱动。
有些数据针对高效存储或压缩进行了优化,有些数据针对高效访问进行了优化,有些数据设计为轻量级和可读的(网络格式),而其他数据格式则寻求包含尽可能多的不同数据类型。
有趣的是,当今一些最流行的格式也是一些最简单的格式,甚至缺乏功能更强大、更复杂的格式。易用性对于地理空间分析师极其重要,因为将数据集成到地理信息系统以及在分析师之间交换数据花费了大量时间。简单的数据格式最有利于这些活动。
共同特征
地理空间分析是一种将信息处理技术应用于具有地理背景的数据的方法。该定义包含地理空间数据的最重要元素:
- 地理定位数据:地理定位信息可以简单到地球上的一个点参考一张照片的拍摄地点。它也可以像卫星相机工程模型和轨道力学信息一样复杂,用于重建卫星捕获图像的确切条件和位置。
- 主题信息:主题信息也可以涵盖多种可能性。有时,图像中的像素是地面视觉表现方面的数据。其他时候,可以使用多光谱波段(如红外光)处理图像,以提供图像中不可见的信息。处理后的图像通常使用结构化调色板进行分类,调色板链接到一个键,描述每种颜色代表的信息。其他可能性包括某种形式的数据库,其中包含每个地理位置要素的行和列信息,例如从第 1 章开始的
SimpleGIS中与每个城市相关联的人口,使用 Python 学习地理空间分析。
这两个因素存在于可以被视为地理空间数据的每一种格式中。地理空间数据的另一个共同特征是空间索引。概览数据集也与索引相关。
理解空间索引
地理空间数据集通常是非常大的文件,很容易达到数百兆字节甚至几千兆字节的大小。执行分析时,地理空间软件在尝试重复访问大型文件时可能会非常慢。
正如在第 1 章中简要讨论的,使用 Python 学习地理空间分析,空间索引创建了一个指南,允许软件快速定位查询结果,而无需检查数据集中的每个要素。空间索引允许软件排除可能性,并对小得多的数据子集执行更详细的搜索或比较。
空间索引算法
许多空间索引算法是已经在非空间信息上使用了几十年的成熟算法的衍生。最常见的两种空间索引算法是四叉树索引和 R 树索引。
四叉树索引
四叉树算法实际上代表了一系列基于共同主题的不同算法。四叉树索引中的每个节点包含四个子节点。这些子节点通常是正方形或矩形。当一个节点包含指定数量的要素并且添加了更多要素时,该节点会拆分。
将空间分成嵌套正方形的概念加快了空间搜索的速度。软件一次只能处理五个点,并使用简单的大于/小于比较来检查一个点是否在一个节点内。四叉树索引最常见于基于文件的索引格式。
下图显示了按四叉树算法排序的点数据集。黑点是实际的数据集,而框是索引的边界框。请注意,没有任何边界框重叠。左边的图显示了索引的空间表示,而右边的图显示了典型索引的层次关系,这就是空间软件如何查看索引和数据。
这种结构允许空间搜索算法在试图定位一个或多个与其他特征集相关的点时快速排除可能性,如下图所示:

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

索引会分解大型数据集,但为了加快搜索速度,它们可能会采用一种称为网格的技术。我们接下来看看。
网格
空间索引也经常使用整数网格的概念。地理空间坐标通常是小数点后两位到十六位的浮点数。对浮点数进行比较比使用整数在计算上要昂贵得多。索引搜索是关于首先消除不需要精度的可能性。
因此,大多数空间索引算法将浮点坐标映射到固定大小的整数网格。在搜索特定功能时,软件可以使用更有效的整数比较,而不是使用浮点数。一旦缩小结果范围,软件就可以访问全部分辨率数据。
对于简单的文件格式,网格大小可以小到 256 x 256,或者在大型地理空间数据库中可以大到 300 万 x 300 万,这些数据库旨在整合所有已知的坐标系和可能的分辨率。
整数映射技术非常类似于在映射程序中用于在图形画布上绘制数据的渲染技术。第 1 章、中的SimpleGIS脚本使用 Python 学习地理空间分析,也使用这种技术使用内置的 Python 海龟图形引擎渲染点和多边形。
什么是概述?
概览数据最常见于栅格格式。概视图是栅格数据集的重采样和低分辨率版本,提供缩略图视图或以不同的地图比例快速加载图像视图。它们也被称为金字塔,而创建它们的过程被称为聚合一个图像。这些概视图通常经过预处理,并与嵌入文件或单独文件中的全分辨率数据一起存储。
这种便利性的折衷是,额外的图像会增加数据集的整体文件大小;然而,它们加快了图像查看器的速度。向量数据也有概视图的概念,通常用于在概视图中给出数据集的地理环境。然而,因为向量数据是可扩展的,所以通常由软件使用泛化操作动态创建尺寸减小的概视图,如第 1 章、使用 Python 学习地理空间分析中所述。
有时,向量数据会通过转换为缩略图图像进行栅格化,缩略图图像与图像标题一起存储或嵌入其中。下图演示了图像概视图的概念,直观地显示了它们为什么通常被称为金字塔:

空间索引和概览有助于加快分析师通过软件访问数据的速度。接下来,我们将看看元数据,它提供了一种人类可读和机器可读的方式来理解、搜索甚至编目数据。
什么是元数据?
正如在第 1 章中所讨论的,使用 Python 学习地理空间分析,元数据是描述相关数据集的任何数据。元数据的常见示例包括基本元素,如数据集在地球上的足迹,以及更详细的信息,如空间投影和描述数据集如何创建的信息。
大多数数据格式都包含地球上数据的足迹或边界框。详细的元数据通常以标准格式存储在单独的位置,例如美国联邦地理数据委员会 ( FGDC )、数字地理空间元数据内容标准 ( CSDGM )、ISO 或包括元数据要求的较新的欧盟倡议,并被称为欧洲共同体空间信息基础设施 ( INSPIRE )。
理解文件结构
根据格式的不同,上述元素可以以多种方式存储在单个文件、多个文件或数据库中。此外,这些地理空间信息可以以多种格式存储,包括嵌入的二进制标题、XML、数据库表、电子表格/CSV、单独的文本或二进制文件。
人类可读的格式,如 XML 文件、电子表格和结构化文本文件,只需要一个文本编辑器就可以进行研究。使用 Python 的内置模块、数据类型和字符串操作函数,也可以轻松解析和处理这些文件。基于二进制的格式更复杂。因此,使用第三方库处理二进制格式通常更容易。
但是,您不必使用第三方库,尤其是如果您只想在高层次上调查数据。Python 内置的struct模块拥有你需要的一切。struct模块允许您以字符串形式读写二进制数据。使用struct模块时,您需要了解字节顺序的概念。字节顺序是指组成文件的信息字节如何存储在内存中。这种顺序通常是特定于平台的,但是在一些罕见的情况下,包括 shapefiles,字节顺序被混合到文件中。
Python struct模块使用大于(>)和小于(<)符号来指定字节顺序(分别为大端和小端)。
下面的简单例子演示了 Python struct模块从 Esri 形状文件向量数据集中解析边界框坐标的用法。您可以从以下网址下载此形状文件作为压缩文件:https://github . com/GeospatialPython/Learn/blob/master/Hancock . zip?原始=真实。
当你解压这个文件时,你会看到三个文件。对于这个例子,我们将使用hancock.shp。Esri shapefile 格式在文件头中具有固定的位置和数据类型,从字节 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 种与向量数据直接相关的格式。向量数据仅存储几何图元,包括点、线和多边形。但是,对于每种类型的形状,只存储点。例如,在简单的直线向量形状的情况下,只有端点必须被存储并定义为一条线。显示这些数据的软件会读取形状类型,然后用一条线动态连接端点。
地理空间向量数据类似于向量计算机图形的概念,但有一些明显的例外。地理空间向量数据包含正的和负的基于地球的坐标,而向量图形通常存储计算机屏幕坐标。地理空间向量数据通常还与几何图形所表示的对象的其他信息相关联。在全球定位系统数据的情况下,该信息可以像时间戳一样简单,或者对于较大的地理信息系统,可以像整个数据库表一样简单。
向量图形通常存储描述颜色、阴影和其他显示相关指令的样式信息,而地理空间向量数据通常不存储。另一个重要的区别是形状。地理空间向量通常只包括基于点、直线和直线多边形的非常原始的几何图形,而许多计算机图形向量格式具有曲线和圆的概念。然而,地理空间向量可以使用更多的点来建模这些形状。
其他人类可读的格式,如 CSV、简单文本字符串、GeoJSON 和基于 XML 的格式,在技术上是向量数据,因为它们存储的是几何数据,而不是栅格数据,后者代表数据集边界框内的所有数据。直到 20 世纪 90 年代末 XML 的爆发,向量数据格式几乎都是二进制的。XML 提供了一种计算机可读和人类可读的混合方法。折中的办法是,与二进制格式相比,文本格式(如 GeoJSON 和 XML 数据)会大大增加文件大小。这些格式将在本节后面讨论。
可供选择的向量格式数量惊人。OGR(http://www.gdal.org/ogr_formats.html)开源向量库列出了超过 86 种支持的向量格式。其商业对手,安全软件的功能操纵引擎 ( FME )列出了超过 188 种支持的向量格式(http://www.safe.com/fme/format-search/#filters%5B%5D=VECTOR)。这些列表包括一些向量图形格式,以及人类可读的地理空间格式。至少还有很多格式需要注意,以防你遇到它们。
现在,让我们来看看一种特定的、广泛使用的向量数据类型,称为 shapefiles。
Shapefiles
最普遍的地理空间格式是 Esri shapefile。被称为 Esri 的地理空间软件公司在 1998 年发布了作为开放格式的 shapefile 格式规范(http://www.esri.com/library/whitepapers/pdfs/shapefile.pdf)。Esri 将其开发为 ArcView 软件的一种格式,旨在作为低端 GIS 选项,以补充其高端专业软件包, ArcInfo ,以前称为 ARC/INFO 。然而,这种格式的开放规范、效率和简单性使它成为一种非官方的地理信息系统标准,在 15 年后仍然非常流行。
实际上,每一个被标记为地理空间软件的软件都支持 shapefile,因为 shape file 格式非常常见。由于这个原因,作为一名分析师,你几乎可以通过密切熟悉 shapefiles 而忽略其他格式来勉强度日。您可以通过源格式的本机软件或第三方转换器将几乎任何其他格式转换为 shapefile,例如 OGR 库,其中有一个 Python 模块。其他处理 Shapely 文件的 Python 模块是 Shapely 和 Fiona,它们基于 OGR。
shapefile 最显著的特点之一是格式由多个文件组成(从最小到最大,可以有 3-15 个不同的文件)。下表描述了文件格式。有效的形状文件需要.shp、.shx和.dbf文件:
| 支持文件扩展名的 Shapefile】 | 配套文件用途 | 注释 |
|---|---|---|
.shp |
这是形状文件。它包含几何图形。 | 这是一个必需的文件。一些只需要几何图形的软件会接受没有.shx或.dbf文件的.shp文件。 |
.shx |
这是形状索引文件。它是一个固定大小的记录索引,引用几何图形以加快访问速度。 | 这是一个必需的文件。没有.shp文件,这个文件没有意义。 |
.dbf |
这是数据库文件。它包含几何属性。 | 这是一个必需的文件。一些软件会在没有.shp文件的情况下访问这种格式,因为规范早于 shapefiles。它基于非常古老的 FoxPro 和 dBase 格式。它有一个开放的规范,叫做 Xbase。大多数类型的电子表格软件都可以打开.dbf文件。 |
.sbn |
这是空间 bin 文件,即 shapefile 空间索引。 | 它包含映射到 256 x 256 整数网格的要素边界框。这个文件伴随大型 shapefile 数据集是非常常见的。 |
.sbx |
.sbn文件的固定大小的记录索引。 |
空间索引的传统有序记录索引。经常看到。 |
.prj |
这包含以众所周知的文本格式存储的地图投影信息。 | 由地理信息系统软件进行动态投影的一种非常常见且必需的文件。这种相同的格式也可以伴随栅格数据。 |
.fbn |
只读要素的空间索引。 | 很少见到。 |
.fbx |
.fbn空间索引的固定大小的记录索引。 |
很少见到。 |
.ixs |
地理编码索引。 | 常见于地理编码应用,包括行驶方向类型应用。 |
.mxs |
另一种地理编码索引。 | 比.ixs格式不常见。 |
.ain |
属性索引。 | 大多是遗留格式,很少用于现代软件。 |
.aih |
属性索引。 | 伴随.ain文件。 |
.qix |
四叉树索引。 | 一种由开源社区创建的空间索引格式,因为 Esri .sbn和.sbx文件直到最近才被记录下来。 |
.atx |
属性索引。 | 一个更新的 Esri 软件特有的属性索引,用于加速属性查询。 |
.shp.xml |
元数据。 | 一个地理空间元数据.xml容器。它可以是多种 XML 标准中的任何一种,包括 FGDC 和国际标准化组织。 |
.cpg |
.dbf的代码页文件。 |
用于.dbf文件的国际化。 |
您可能永远不会同时遇到所有这些格式。但是,您使用的任何 shapefile 都将有多个文件。你通常会看到.shp、.shx、.dbf、.prj、.sbn、.sbx,偶尔也会看到.shp.xml文件。如果要重命名形状文件,必须用相同的名称重命名所有关联的文件;但是,在 Esri 软件和其他 GIS 软件包中,这些数据集将显示为单个文件。
shapefiles 的另一个重要特性是记录没有编号。记录包括几何图形、.shx索引记录和.dbf记录。这些记录以固定的顺序存储。当您使用软件检查 shapefile 记录时,它们看起来是有编号的。
人们在删除 shapefile 记录、保存文件并重新打开文件时,经常会感到困惑;已删除记录的编号仍会出现。原因是 shapefile 记录在加载时是动态编号的,而不是保存的。因此,例如,如果您删除记录号 23 并保存 shapefile,则下次读取 shapefile 时,记录号 24 将变为 23。许多人希望打开 shapefile,看到记录从 22 跳到 24。以这种方式跟踪 shapefile 记录的唯一方法是在.dbf文件中创建一个名为 ID 或类似的新属性,并为每个记录分配一个永久且唯一的标识符。
就像重命名 shape file 一样,在编辑 shape file 时必须小心。最好使用将 shapefiles 作为单个数据集处理的软件。如果您单独编辑任何文件,并在不编辑附带文件的情况下添加/删除记录,大多数地理空间软件会认为 shapefile 已损坏。
计算机辅助设计文件
CAD 代表计算机辅助设计。计算机辅助设计数据的主要格式是由 Autodesk 为其领先的 AutoCAD 软件包创建的。常见的两种格式是图纸交换格式 ( DXF )和 AutoCAD 的原生图纸 ( DWG )格式。
DWG was traditionally a closed format, but it has become more open.
计算机辅助设计软件用于所有与工程相关的事情,从设计自行车到汽车、公园和城市下水道系统。作为一名地理空间分析师,你不必担心机械工程设计;然而,土木工程设计成为一个相当大的问题。大多数工程公司使用地理空间分析的程度非常有限,但几乎所有数据都以计算机辅助设计格式存储。DWG 和 DXF 格式可以使用地理空间软件中没有的要素或地理空间系统支持较弱的要素来表示对象。这些功能的一些示例包括:
- 曲线
- 曲面(对于不同于地理空间高程曲面的对象)
- 三维实体
- 文本(呈现为对象)
- 文本样式
- 视口配置
这些特定于计算机辅助设计和工程的功能使得很难将计算机辅助设计数据干净地转换为地理空间格式。如果您遇到计算机辅助设计数据,最简单的选择是询问数据提供商他们是否有形状文件或其他以地理空间为中心的格式。
基于标签和基于标记的格式
基于标记的标记格式通常是 XML 格式。它们还包括其他结构化文本格式,如知名文本 ( WKT )格式,用于投影信息文件以及不同类型的数据交换。
XML 格式包括锁眼标记语言(KML)OpenStreetMap(OSM)格式,以及 GPS 数据的 Garmin GPX 格式,后者已经成为一种流行的交换格式。开放地理空间联盟的地理标记语言 ( GML )标准是最古老、使用最广泛的基于 XML 的地理格式之一。它也是 OGC 网络功能服务 ( WFS )网络应用标准的基础。然而,GML 已经基本上被 KML 和 GeoJSON 格式所取代。
XML 格式通常不仅仅包含几何图形。它们还包含属性和渲染说明,如颜色、样式和符号系统。谷歌的 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投影文件中的投影信息,以及形状文件或光栅。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,被用作字符串的简写,如前面的代码。还有一些常用投影的简称,如墨卡托投影,可以在不同的软件包中引用一个投影。
More information on these reference systems can be found on the spatial reference website at http://spatialreference.org/ref/.
杰尤森
GeoJSON 是一种基于JavaScript Object Notation(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 APIs 的关键组件,这将在本章稍后介绍。目前,它在二进制格式的计算机资源效率、文本格式的人类可读性和编程实用性之间提供了最佳折衷。
地质包装
我们将在此简要提及地理包格式,因为它包含在第 3 章、地理空间技术景观中,也因为它是一种地理数据库。geopackage格式是基于 SQLite 文件的数据库容器上的 OGC 开放标准,该数据库容器独立于平台、供应商和软件。它试图摆脱所有由专有数据格式或受限数据格式产生的问题。
接下来,我们将看看其他主要的数据类型:栅格数据。
了解栅格数据类型
栅格数据由行和列的像元或像素组成,每个像元代表一个值。将栅格数据视为图像是最简单的方式,这是软件通常表示它们的方式。但是,栅格数据集不一定存储为图像。它们也可以是 ASCII 文本文件或数据库中的二进制大对象。
地理空间栅格数据和常规数字图像的另一个区别是分辨率。如果以全尺寸打印,数字图像以每英寸点数表示分辨率。分辨率也可以表示为图像中的像素总数,定义为百万像素。但是,地理空间栅格数据使用每个像元代表的地面距离。例如,分辨率为 2 英尺的栅格数据集意味着单个像元代表地面上的 2 英尺,这也意味着只有大于 2 英尺的对象才能在数据集中直观地识别出来。
栅格数据集可能包含多个波段,这意味着可以在同一时间在同一区域收集不同波长的光。通常,这个范围是从 3-7 个波段,但是在高光谱系统中可以是几百个。这些波段作为图像的 RGB 波段单独查看或交换进出。还可以使用数学方法将它们重新组合成衍生的单波段图像,然后使用代表数据集中值的一定数量的类重新着色。
栅格数据的另一个常见应用是在科学计算领域,该领域共享地理空间遥感的许多元素,但增加了一些有趣的曲折。科学计算经常使用复杂的栅格格式,包括网络公共数据表单 ( 网络 CDF)GRIB和 HDF5 ,它们存储整个数据模型。这些格式更像文件系统中的目录,可以包含多个数据集或同一数据集的多个版本。海洋学和气象学是这种分析最常见的应用。科学计算数据集的一个示例是天气模型的输出,其中不同波段的栅格数据集的像元可能表示时间序列中模型的不同变量输出。
像向量数据一样,栅格数据可以有多种格式。开源的raster库,被称为地理空间数据抽象库 ( GDAL ),它实际上包括我们前面提到的向量 OGR 库,列出了超过 130 种支持的栅格格式(http://www.gdal.org/formats_list.html)。FME 软件包也支持这许多。然而,就像形状文件和计算机辅助设计数据一样,也有一些突出的光栅格式。
TIFF 文件
标记图像文件格式 ( TIFF )是最常见的地理空间栅格格式。TIFF 格式的灵活标记系统允许它在单个文件中存储任何类型的数据。TIFFs 可以包含概览图像、多个波段、整数高程数据、基本元数据、内部压缩以及通常以其他格式存储在附加支持文件中的各种其他数据。任何人都可以通过向文件结构添加标记数据来非正式地扩展 TIFF 格式。这种可扩展性有优点也有缺点。然而,一个 TIFF 文件可能在一个软件中运行良好,但在另一个软件中访问时会失败,因为这两个软件包在不同程度上实现了大量的 TIFF 规范。一个关于 TIFF 的老笑话有一个令人沮丧的事实: TIFF 代表成千上万不兼容的文件格式。GeoTIFF 扩展定义了地理空间数据的存储方式。存储为 TIFF 文件的地理空间栅格可能具有以下任何文件扩展名:.tiff、.tif或.gtif。
JPEG、GIF、BMP 和 PNG
JPEG、GIF、BMP 和 PNG 格式通常是常见的图像格式,但也可用于基本的地理空间数据存储。通常,这些格式依赖于信息地理配准的辅助文本文件,以便与地理信息系统软件兼容,如 WKT、.prj或世界文件。
JPEG 格式对于地理空间数据来说也相当常见。JPEGs 有一个内置的元数据标记系统,类似于 TIFFs,叫做 EXIF。除了栅格地理信息系统图层之外,JPEGs 通常还用于地理标记的照片。位图 ( BMP )图像用于桌面应用和文档图形。但是,JPEG、GIF 和 PNG 是网络地图应用程序中使用的格式,尤其是用于预生成的服务器地图切片,以便通过滑动地图快速访问。
压缩格式
由于地理空间栅格往往非常大,因此通常使用高级压缩技术存储它们。最新的开放标准是 JPEG 2000 格式,它是 JPEG 格式的升级,包括小波压缩和一些其他功能,如地理参考数据。多分辨率无缝图像数据库 ( MrSID ) ( .sid)和增强压缩小波 ( ECW ) ( .ecw)是地理空间环境中常见的两种专有小波压缩格式。
TIFF 格式支持压缩,包括伦佩尔-齐夫-韦尔奇 ( 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 轴的单元尺寸
- 第二行:在 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 )像元大小允许您计算任意像元的坐标或一组像元之间的距离。旋转值对于地理空间软件很重要,因为遥感图像经常由于数据收集平台而旋转。
旋转图像会有重采样数据的风险,从而导致数据丢失,因此旋转值允许软件考虑失真。图像外部的周围像素通常被赋予一个no data值,并表示为黑色。
以下图片由来自https://viewer.nationalmap.gov/advanced-viewer/的美国地质调查局 ( 美国地质勘探局)提供,演示了图像旋转,其中卫星采集路径的方向为从东南到东北,但底层底图为北:

在 Python 中处理栅格数据时,世界文件是一个很好的工具。大多数地理空间软件和数据库都支持世界文件,因此在地理配准方面,它们通常是一个不错的选择。
You'll find that world files are very useful, but as you use them infrequently, you will forget what the unlabeled contents represent. A quick reference for world files is available at https://kralidis.ca/gis/worldfile.htm.
向量数据和栅格数据是两种最常见的数据类型。然而,由于收集成本逐渐变得更便宜,另一种类型越来越受欢迎。这种类型就是点云数据,接下来我们将对其进行研究。
什么是点云数据?
点云数据是基于某种聚焦能量返回作为表面点的( x 、 y 、 z )位置收集的任何数据。这可以使用激光、雷达波、声学探测或其他波形生成设备来创建。点之间的间距是任意的,并且取决于收集数据的传感器的类型和位置。
在这本书里,我们将主要关注激光雷达数据和雷达数据。雷达点云数据通常是在空间任务中收集的,而激光雷达通常是由地面或机载车辆收集的。从概念上讲,这两种类型的数据是相似的。
激光雷达
激光雷达使用强大的激光测距系统,以非常高的精度模拟世界。术语 LIDAR ,或者 LIDAR,是词语光和雷达的组合。有人声称它也代表光探测和测距。激光雷达传感器可以安装在空中平台上,包括卫星、飞机或直升机。它们也可以安装在车辆上进行地面收集。
由于激光雷达提供的高速、连续的数据采集,以及宽视野(通常是传感器的 360 度),激光雷达数据通常不像其他形式的栅格数据那样具有矩形覆盖区。激光雷达数据集通常被称为点云,因为数据是一系列 (x,y,z )位置,其中 z 是从激光到被检测物体的距离,( x , y )值是根据传感器位置计算的物体投影位置。
下图由美国地质勘探局提供,显示了使用地面传感器而不是航空传感器的城市地区点云激光雷达数据集。颜色基于激光能量返回的强度,红色区域更靠近激光雷达传感器,绿色区域更远,可以给出几厘米以内的精确高度:

LIDAR 数据最常见的数据格式是 LIDAR 交换格式 ( LAS ,这是一个社区标准。LIDAR 数据可以用多种方式表示,包括一个简单的文本文件,每行有一个( x 、 y 、 z )元组。有时,激光雷达数据可以使用同时收集的图像像素颜色进行着色。激光雷达数据也可用于创建 2D 高程栅格。
这种技术是激光雷达在地理空间分析中最常见的用途。任何其他用途都需要专门的软件,允许用户以 3D 方式工作。在这种情况下,其他地理空间数据不能与点云结合。
什么是 web 服务?
地理空间 web 服务允许用户通过 web 执行数据发现、数据可视化和数据访问。Web 服务通常由基于用户输入的应用程序访问,例如放大在线地图或搜索数据目录。最常见的协议是返回渲染地图图像的网络地图服务 ( WMS )和返回 GML 的网络要素服务 ( WFS ),这在本章的介绍中有所提及。
许多 WFS 服务还可以返回 KML、JSON、压缩的形状文件和其他格式。这些服务通过 HTTP GET请求调用。以下 URL 是一个 WMS GET请求的示例,该请求返回一个 640 像素宽、400 像素高的世界地图图像,EPSG 代码为 900913:http://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:900
Web 服务正在快速发展。开放地理信息系统联盟正在为传感器网络和其他地理空间环境增加新的标准。代表性状态转移 ( REST )服务也是常用的。REST 服务使用简单的 URL,通过相应地定制 URL 参数及其值,使得请求数据在几乎任何编程语言中都非常容易实现。几乎每种编程语言都有强大的 HTTP 客户端库,能够使用 REST 服务。
这些 REST 服务可以返回许多类型的数据,包括图像、XML 或 JSON。目前还没有通用的地理空间 REST 标准,但 OGC 已经研究了很长时间。Esri 已经创建了一个目前广泛使用的工作实现。
The following URL is an example of an Esri geospatial REST service that would return KML based on a weather radar image layer. You can add this URL to Google Earth as a network link, or you can download it as compressed KML (KMZ) in a browser to import it into another program: https://idpgis.ncep.noaa.gov/arcgis/rest/services/NWS_Observations/radar_base_reflectivity/MapServer/generateKml?docName=NWSRadar&layers=0&layerOptions=separateImage.
You can find tutorials on the myriad of OGC services here: http://cite.opengeospatial.org/pub/cite/files/edu/fundamental-concepts/text/basic.html.
在撰写本书时,OGC 正在经历一场应用编程接口的演变,它将通过诸如 REST、OpenAPI、JSON/HTML 和 Swagger 等技术显著降低使用地理空间应用编程接口的障碍。你可以在这里通过 OGC 的技术路线图追踪这些趋势:https://github.com/opengeospatial/OGC-Technology-Trends。
现在,我们将从单个文件格式转向功能强大的地理数据库,这些地理数据库可以通过单个应用编程接口整合数据。
了解地理空间数据库
地理空间数据库或地理数据库是指文件格式、数据模式甚至软件的整个类别。在第 3 章地理空间技术领域中,我们将把地理数据库作为软件包进行介绍,正式称为数据库管理系统。但是在本节中,我们将把它们的属性描述为文件格式。地理数据库历史上仅存储向量数据,但现代地理数据库也非常适合栅格数据管理。
地理数据库可以展示我们之前提到的所有共同特征。这些信息存储在我们称之为数据库模型的数据库中。一种非常流行的模型是传统的关系模型,它使用行和列的表。每行和每列的组合称为一个单元格。行可以与另一个表相关联,以使用指定的列链接信息,其中每个单元格都成为引用另一个表中的单元格的键,然后将行链接在一起。
列的实际名称和数据之间的关系构成了数据定义。地理数据库至少会将几何描述与几何所代表的对象的属性相关联。单点通常由x和y列表示。但是,多边形和多段线具有任意数量的点。这意味着地理数据库通常将几何信息存储为一个 BLOB ,使用的格式标准称为众所周知的二进制或 WKB 。
属性信息通常被定义为数据类型,如整数、浮点十进制数、字符串或日期。该表还可以包括用于地图显示的投影信息,以及用于加速搜索和地理空间比较的空间索引栏。地理数据库也可能有另一个相关表,以便链接关于地理空间数据的详细元数据。
大型地理空间栅格数据集很少直接存储在数据库中。通常,栅格数据以一个名称存储在磁盘上,文件系统引用存储在指向栅格数据的数据库中。地理数据库还可以存储表示栅格数据地面覆盖区的几何列,然后可以将其用作地理空间操作的代理。
摘要
现在,您已经具备了处理常见类型的地理空间数据所需的背景。您还了解地理空间数据集的共同特征,这些特征将允许您评估不熟悉的数据类型,并确定关键元素,这些元素将推动您在与这些数据交互时使用哪些工具。
在下一章中,我们将研究可用于处理地理空间数据集的模块和库。我们将了解地理空间技术生态系统,它由数千个软件库和包组成。我们还将了解地理空间软件的层次结构,以及它如何让您快速理解和评估任何地理空间工具。
进一步阅读
你可以在这里找到无数 OGC 服务的教程。*
三、地理空间技术前景
地理空间技术生态系统由数百个软件库和包组成。对于地理空间分析的新手来说,这种大量的选择可能是压倒性的。快速学习地理空间分析的秘诀是了解少数真正重要的库和包。大多数软件,无论是商业软件还是开源软件,都是从这些关键包中派生出来的。了解地理空间软件的生态系统及其使用方式可以让您快速理解和评估任何地理空间工具。
地理空间库可以分配给以下一个或多个高级核心功能,它们在一定程度上实现了这些功能。我们将在本章中学习这些功能:
- 数据存取
- 计算几何(包括数据重投影)
- 图像处理
- 可视化工具
- 元数据工具
在本章中,我们将研究对地理空间分析影响最大的包,以及您可能经常遇到的包。然而,和任何信息过滤一样,我们鼓励你自己做研究,得出自己的结论。
以下网站提供了本章未包括的软件的更多信息:
- 维基百科地理信息系统软件列表:https://en . Wikipedia . org/wiki/List _ of _ geographic _ information _ systems _ software
- OSGeo 项目清单及孵化器项目:http://www.osgeo.org
图像处理软件的功能是用于遥感。然而,这类软件非常分散,包含几十个软件包,很少集成到衍生软件中。大多数遥感图像处理软件都是基于相同的数据访问库,在此基础上实现定制的图像处理算法。
看看下面这些类型软件的例子,包括开源和商业软件包:
- 开源软件映像图 ( OSSIM )
- 地理资源分析支持系统 ( GRASS )
- OTB 奥菲欧工具箱
- ERDAS IMAGINE
- 我发
技术要求
以下是本章的技术要求列表:
- Python 3.6 或更高版本
- 内存:最小 6 GB (Windows),8gb(macOS);推荐 8 GB
- 存储:最低 7200 转/分的 SATA,可用空间为 20gb;推荐的具有 40 GB 可用空间的固态硬盘
- 处理器:最低英特尔酷睿 i3 2.5 GHz 推荐的英特尔酷睿 i5
了解数据访问
如第 2 章、学习地理空间数据所述,地理空间数据集通常很大、很复杂且变化多端。这一挑战使得能够高效读取(在某些情况下是写入)这些数据的库对于地理空间分析至关重要。如果无法访问数据,地理空间分析就无法开始。
此外,准确性和精确性是地理空间分析的关键因素。未经许可重新采样数据的图像库,或者将坐标四舍五入几个小数位的计算几何库,都会对分析质量产生不利影响。此外,这些库必须有效地管理内存。一个复杂的地理空间过程可能会持续数小时,甚至数天。
如果数据访问库出现内存故障,它可能会延迟整个项目甚至整个工作流,涉及依赖该分析输出的几十个人。
像地理空间数据抽象库 ( GDAL )这样的数据访问库,为了速度和跨平台兼容性,大部分都是用 C 或 C++编写的。速度很重要,因为地理空间数据集通常很大。但是,您也会看到许多用 Java 编写的包。写得好的时候,纯 Java 可以达到处理大型向量或栅格数据集可以接受的速度,并且通常对于大多数应用程序来说是可以接受的。
下面的概念图显示了主要的地理空间软件库和包以及它们之间的关系。粗体的库表示被主动维护的根库,并且没有明显地从任何其他库派生。这些根库代表地理空间操作,相当难实现,绝大多数人选择使用其中一个库,而不是创建一个竞争的库。如您所见,少数库构成了不成比例的地理空间分析软件。下图绝非详尽无遗。在本书中,我们将只讨论最常用的包:

GDAL 、 GEOS (简称几何引擎-开源)和 PROJ 库是商业和开源两方面地理空间分析社区的核心和灵魂。需要注意的是,这些库都是用 C 或 C++编写的。以地理工具和 Java 拓扑套件 ( JTS )核心库的形式,在 Java 中也做了大量工作,这些核心库在一系列台式机、服务器和移动软件中使用。鉴于有数百个地理空间包可用,几乎所有的包都依赖于这些库来做任何有意义的事情,您将开始了解地理空间数据访问和计算几何的复杂性。将这个软件领域与文本编辑器进行比较,文本编辑器在开源项目网站(http://sourceforge.net/)上搜索时会返回 5000 多个选项。
地理空间分析是一个真正的全球社区,对该领域的重大贡献来自全球的每个角落。但是,当你更多地了解软件领域中心的高质量软件包时,你会发现这些程序往往来自加拿大,或者由加拿大开发人员大量贡献。
地理空间分析被认为是现代地理信息系统的诞生地,是国家的骄傲。此外,加拿大政府和公私地理连接项目在研究和公司方面投入巨资,既是出于经济原因,也是出于必要,以更好地管理该国丰富的自然资源和人口需求。
断续器
GDAL 承担着地理空间行业最繁重的任务。GDAL 网站列出了 80 多种使用该库的软件,这个列表一点也不完整。这些包中的许多都是行业领先的开源商业工具。该列表不包括数百个较小的项目和使用该库进行地理空间分析的独立分析师。GDAL 还包括一组命令行工具,无需任何编程即可完成各种操作。
A list of projects using GDAL can be found at the following URL: http://trac.osgeo.org/gdal/wiki/SoftwareUsingGdal.
栅格数据和栅格数据
GDAL 为地理空间行业中的大量栅格数据类型提供了一个单一的抽象数据模型。它整合了不同格式的独特数据访问库,并为读写数据提供了一个通用的应用编程接口。在开发人员 Frank Warmerdam 于 20 世纪 90 年代末创建 GDAL 之前,每种数据格式都需要一个单独的数据访问库,该库具有不同的 API,以便读取数据,或者在最坏的情况下,开发人员经常编写定制的数据访问例程。
下图直观地描述了 GDAL 如何抽象栅格数据:

在前面的软件概念图中,您可以看到 GDAL 在任何单一的地理空间软件中具有最大的影响力。将 GDAL 和它的姊妹库 OGR 结合起来,用于向量数据,影响几乎加倍。PROJ 图书馆也产生了巨大的影响,但它通常是通过 OGR 或 GDAL 访问的。
The GDAL home page can be found at http://www.gdal.org/.
GDAL 和向量数据
除了栅格数据,GDAL 还列出了至少对 70 多种向量数据格式的部分支持。GDAL 包成功的一部分是 X11/MIT 开源许可。该许可证是商业和开源友好的。GDAL 库可以包含在专有软件中,而不会向用户透露专有源代码。
GDAL 具有以下向量功能:
- 统一向量数据和建模抽象
- 向量数据重投影
- 向量数据格式转换
- 属性数据过滤
- 基本几何过滤,包括裁剪和多边形内点测试
GDAL 有几个命令行实用程序,展示了它处理向量数据的能力。这种能力也可以通过它的编程接口来访问。下图概述了 GDAL 向量体系结构:

考虑到该模型能够表示 70 多种不同的数据格式,GDAL 向量架构相当简洁:
- 几何图形:该对象表示点、线串、多边形、几何集合、多多边形、多点和多线的开放地理空间联盟 ( OGC )简单要素规范数据模型。
- 特征定义:该对象包含一组相关特征的属性定义。
- 特征:该对象将几何和特征定义信息联系在一起。
- 空间参考:该对象包含 OGC 空间参考定义。
- 图层:该对象表示在数据源中分组为图层的要素。
- 数据源:该对象是 GDAL 访问的文件或数据库对象。
- 驱动程序:这个对象包含 GDAL 可用的 70 多种数据格式的翻译器。
这种体系结构运行平稳,只有一个小问题——层的概念甚至用于只包含一个层的数据格式。例如,shapefiles 只能表示一个层。但是,当您使用 GDAL 访问一个 shapefile 时,您仍然必须使用 shapefile 的基本名称调用一个新的层对象,而不需要文件扩展名。这个设计特性只是一个小的不便,远远超过了 GDAL 提供的功能。
现在,让我们超越访问数据,使用它进行分析。
理解计算几何
计算几何包括对向量数据进行运算所需的算法。这个领域在计算机科学中非常古老;但是,由于地理空间坐标系的原因,大多数用于地理空间操作的库与计算机图形库是分开的。正如第 1 章末尾所述,使用 Python 学习地理空间分析时,计算机屏幕坐标几乎总是以正数表示,而地理空间坐标系在向西和向南移动时通常使用负数。
几个不同的地理空间库属于这一类别,但它们也有广泛的用途,从空间选择到渲染。应该注意的是,前面描述的 GDAL 的一些特性使它超越了数据访问的范畴,进入了计算几何的领域。但是,它被列入前一类,因为这是它的主要目的。
计算几何是一门迷人的学科。当编写一个简单的脚本来自动化地理空间操作时,您不可避免地需要一个空间算法。于是问题来了,你是自己尝试实现这个算法,还是经历使用第三方库的开销?选择总是带有欺骗性的,因为有些任务在视觉上很容易理解和实现,有些任务看起来很复杂,但结果很容易,有些任务理解起来很琐碎,但实施起来异常困难。一个这样的例子是地理空间缓冲操作。
这个概念很简单,但算法却相当困难。本节中的以下库是用于计算几何算法的主要包。
PROJ 投影图书馆
美国地质勘探局(美国地质勘探局)分析师杰里·埃文登在美国地质勘探局工作期间,于 20 世纪 90 年代中期创建了现在被称为 PROJ 投影库的项目。从那以后,它成为了开源地理空间基金会 ( OSGeo )的一个项目,得到了许多其他开发者的贡献。PROJ 完成了在数千个坐标系之间转换数据的艰巨任务。在如此多的坐标系中转换点所需的数学极其复杂。没有其他图书馆能比得上 PROJ 的能力。这一事实以及应用程序将不同来源的数据集转换为通用投影所需的例行程序,使 PROJ 成为该领域无可争议的领导者。
下面的图是一个例子,说明 PROJ 支持的预测有多具体。此图从https://calcofi.org开始,代表加州合作海洋渔业调查 ( 卡尔科菲)项目伪投影的线/站坐标系,仅 NOAA (简称国家海洋和大气管理局)、加州大学斯克里普斯海洋研究所和加州鱼类和野生动物部用于收集加州海岸线过去 60 年的海洋和渔业数据:

PROJ 使用一个简单的语法,能够描述任何投影,包括自定义的,本地化的,如前图所示。PROJ 可以在几乎每个主要的地理信息系统包中找到,提供重投影支持,它也有自己的命令行工具。
它可通过 GDAL 获得向量和栅格数据。但是,直接访问库通常很有用,因为它使您能够重新投影单个点。大多数包含 PROJ 的库只允许您重新投影整个数据集。
For more information on PROJ, visit https://proj4.org.
CGAL 的
计算几何算法库 ( CGAL )最初发布于 20 世纪 90 年代末,是一个健壮且成熟的开源计算几何库。它不是专门为地理空间分析而设计的,但通常在现场使用。
CGAL 经常被引用作为可靠的几何处理算法的来源。 CGAL 用户和参考手册中的下图提供了 CGAL 中经常引用的一种算法的可视化,称为多边形直骨架,它是精确增长或收缩多边形所必需的:

直骨架算法既复杂又重要,因为缩小或增长多边形不仅仅是使其变大或变小的问题。多边形实际上会改变形状。当多边形收缩时,不相邻的边会碰撞并消除连接边。随着多边形的增长,相邻的边会分开,并形成新的边来连接它们。这个过程是地理空间多边形缓冲的关键。下图同样来自 CGAL 用户和参考手册,显示了在前面的多边形上使用 insets 的效果:

CGAL can be found online at http://www.cgal.org/.
JTS
JTS 是一个用 100%纯 Java 编写的地理空间计算几何库。JTS 通过实现 SQL 的 OGC 简单功能规范,将自己与其他计算几何库区分开来。有趣的是,其他开发人员已经将 JTS 移植到其他语言,包括 C++、微软。NET,甚至 JavaScript。
JTS 包括一个奇妙的测试程序,叫做 JTS 测试生成器,它提供了一个图形用户界面来测试功能,而不需要设置整个程序。地理空间分析最令人沮丧的一个方面是奇异的几何形状,它打破了大多数时候都有效的算法。另一个常见的问题是由于数据中的微小错误而导致的意外结果,例如多边形在不容易看到的非常小的区域中相交。JTS 测试构建器允许您交互测试 JTS 算法来验证数据,或者只是直观地理解一个过程,如下所示:

即使您不使用 JTS,而是另一种语言的几个端口之一,这个工具也很方便。需要注意的是,自 2006 年 12 月的 1.8 版本以来,JTS 的维护者 Vivid Solutions 一直没有发布新版本。包装相当稳定,仍在积极使用中。
The JTS home page is available at https://locationtech.github.io/jts.
乔斯大衣呢
GEOS 是前面解释过的 JTS 库的 C++端口。这里提到它是因为这个港口对地理空间分析的影响比最初的 JTS 要大得多。C++版本可以在许多平台上编译,因为它避免了任何平台特定的依赖关系。造成全球测地系统流行的另一个因素是,存在大量的基础设施来创建到各种脚本语言(包括 Python)的自动化或半自动绑定。另一个因素是,大多数地理空间分析软件都是用 C 或 C++编写的。全球测地系统最常见的用途是通过包括全球测地系统作为库的其他应用编程接口。
全球测地系统提供以下功能:
- OGC 简单的特征
- 地理空间谓词函数
- 交叉
- 高光
- 解体
- 交叉
- 在…之内
- 包含
- 重复
- 等于
- 覆盖
- 地理空间操作
- 联盟
- 距离
- 交集
- 对称差
- 凸包
- 信封
- 缓冲器
- 简化
- 多边形装配
- 多边形验证
- 面积
- 长度
- 空间索引
- OGC 知名文字 ( WKT )和知名二进制 ( WKB )输入/输出
- C 和 C++ API
- 螺纹安全
全球测地系统可以用全球测地系统汇编,以利用其所有能力。
GEOS can be found online at https://trac.osgeo.org/geos.
邮政地理信息系统
就开源地理空间数据库而言,PostGIS 是最常用的空间数据库。PostGIS 本质上是在著名的 PostgreSQL 关系数据库之上的一个模块。PostGIS 的大部分功能来自前面提到的 GEOS 库。像 JTS 一样,它也实现了 SQL 的 OGC 简单特性规范。地理空间环境中计算几何能力的这种组合将 PostGIS 置于自己的类别中。
PostGIS 允许您对数据集执行属性和空间查询。回想一下第 2 章学习 地理空间数据中的观点,典型的空间数据集由多种数据类型组成,包括几何、属性(一行中的一列或多列数据)以及大多数情况下的索引数据。在 PostGIS 中,您可以像使用 SQL 查询任何数据库表一样查询属性数据。
这种能力并不奇怪,因为属性数据存储在传统的数据库结构中。但是,您也可以使用 SQL 语法查询几何图形。空间操作可以通过 SQL 函数来实现,您可以将这些函数作为查询的一部分。以下示例 PostGIS SQL 语句在Florida状态周围创建了一个 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()功能对通常包含在the_geom列中的Florida的几何图形执行实际的空间选择。在这种情况下,the_geom列是后置地理信息系统图层的几何列。功能名称中的ST缩写代表空间类型。ST_Buffer()功能接受包含空间几何图形和底层地图单位距离的列。
usa_states图层中的地图单位以米为单位表示,因此在前面的示例中,14.5 公里为 14,500 米。回想一下章节1使用 Python 学习地理空间分析,中的观点,类似这样的查询缓冲区用于邻近分析。碰巧的是,佛罗里达州的水域边界扩大了 9 海里,即从该州西部和西北部海岸线延伸到墨西哥湾约 14.5 公里。
下图以虚线显示了佛罗里达州的官方水边界,在地图上标注为:

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

目前,PostGIS 维护以下功能集:
- 地理空间几何类型,包括点、线串、多边形、多点、多线、多多边形和几何集合,它们可以存储不同类型的几何,包括用于测试几何关系的其他空间函数集合(例如,面中点或并集)
- 用于导出新几何图形的空间函数(例如,缓冲区和交叉点)
- 空间测量,包括周长、长度和面积
- 基于 R 树算法的空间索引
- 基本地理空间栅格数据类型
- 拓扑数据类型
- 基于TIGER(T2 的简称】拓扑集成地理编码和参考)普查数据的美国地理编码器
- 一种新的 JSONB 数据类型,允许对 JSON 和 GeoJSON 进行索引和查询
PostGIS 要素集在所有地理数据库中具有竞争力,是所有开源或免费地理数据库中最广泛的。PostGIS 开发社区的活跃势头是该系统成为同类最佳的另一个原因。邮政编码保持在http://postgis.net。
其他空间数据库
PostGIS 是免费和开源地理空间数据库的黄金标准。但是,作为地理空间分析师,您还应该了解其他几个系统。该列表包括商业和开源系统,每个系统都有不同程度的地理空间支持。
地理数据库与地理空间软件、标准和网络并行发展。互联网推动了对能够服务大量数据的大型多用户地理空间数据库服务器的需求。下图由www.OSGeo.org提供,显示了地理空间架构是如何演变的,其中很大一部分演变发生在数据库级别:

甲骨文空间与图形
Oracle 关系数据库是一种广泛使用的数据库系统,由于其成本和巨大的可扩展性,通常由非常大的组织使用。它也非常稳定和快速。它运行着世界上一些最大、最复杂的数据库,经常出现在医院、银行和管理数百万重要记录的政府机构中。
地理空间数据能力最初出现在甲骨文第 4 版中,是由加拿大水文局 ( CHS )进行的修改。CHS 还实现了 Oracle 的第一个空间索引,其形式是一个不同寻常但高效的三维螺旋。甲骨文随后纳入了这一修改,并在主数据库版本 7 中发布了甲骨文空间数据库选项 ( SDO )。SDO 系统在 Oracle 版本 8 中变成了 Oracle Spatial。Oracle Spatial 的数据库模式在一些列名和表名上仍然有 SDO 前缀,类似于 PostGIS 如何使用 OGC 约定 ST 在模式级别将空间信息与传统的关系数据库表和函数分开。
从 2012 年开始,甲骨文开始调用包甲骨文空间和图形,以强调网络数据模块。该模块用于分析网络数据集,如交通或公用事业。然而,该模块也可以用于对抗抽象网络,例如社交网络。社交网络数据的分析是大数据分析的共同目标,现在这种趋势越来越明显。大数据社交网络分析很可能是甲骨文更改产品名称的原因。
Oracle Spatial 具有以下功能:
- 地理空间数据模式
- 现在基于 R 树索引的空间索引系统
- 一个执行几何运算的应用编程接口
- 优化特定数据集的空间数据调整应用编程接口
- 拓扑数据模型
- 网络数据模型
- 用于存储、索引、查询和检索栅格数据的地理主数据类型
- 三维数据类型,包括不规则三角网(tin)和 LIDAR (简称光探测和测距)点云
- 用于搜索位置名称和返回坐标的地理编码器
- 用于驱动方向类型查询的路由引擎
- OGC 法规遵从性
Oracle Spatial 和 PostGIS 具有合理的可比性,并且都是常用的。在执行地理空间分析时,您迟早会将这两个系统视为数据源。
Oracle Spatial and Graph is sold separately from Oracle itself. A little-known fact is that the SDO data type is native to the main Oracle database. If you have a simple application that just inputs points and retrieves them, you can use the main Oracle API to add, update, and retrieve SDOs without Oracle Spatial and Graph.
美国海洋能源、管理、法规和执行局使用甲骨文管理全球最大的地理空间系统之一中价值数十亿美元的石油、天然气和矿产权利的环境、商业和地理空间数据。以下地图由京东方提供:

Oracle Spatial and Graph can be found online at the following URL: http://www.oracle.com/us/products/database/options/spatial/overview.
ArcSDE
ArcSDE 是 Esri 的空间数据引擎 ( SDE )。作为一款独立产品,它已经存在了十多年,现在已经推出到 Esri 的 ArcGIS Server 产品中。让 ArcSDE 有趣的是,该引擎大多独立于数据库,支持多个数据库后端。ArcSDE 支持 IBM DB2、Informix、微软 SQL Server、Oracle 和 PostgreSQL 作为数据存储系统。虽然 ArcSDE 能够在系统(如微软 SQL Server 和甲骨文)上从头开始创建和管理空间模式,但它使用本机空间引擎(如果有)。这种安排适用于 IBM DB2、Oracle 和 PostgreSQL。对于 Oracle,ArcSDE 管理表结构,但可以依赖 Oracle SDO 数据类型进行要素存储。
与前面提到的地理数据库一样,ArcSDE 也有丰富的空间选择应用编程接口,可以处理栅格数据。但是,ArcSDE 没有像 Oracle 和 PostGIS 那样丰富的 SQL 空间 API。Esri 在技术上支持与 ArcSDE 相关的基本 SQL 功能,但它鼓励用户和开发人员使用 Esri 软件或编程 API 来操作通过 ArcSDE 存储的数据,因为它旨在成为 Esri 软件的数据源。
Esri 确实提供了软件库,允许开发人员使用 ArcSDE 或 Esri 的基于文件的地理数据库(称为个人地理数据库)在 Esri 软件之外构建应用程序。但是,这些库是黑盒,ArcSDE 使用的通信协议从未进行过逆向工程。通常情况下,ArcSDE 和第三方应用程序之间会使用 ArcGIS Server API(在某种程度上支持 OGC 服务)和返回 GeoJSON 的相当简单的 REST API 服务在 web 服务级别进行交互。
以下截图来自美国联邦网站http://catalog.data.gov,这是一个基于 ArcSDE 的非常大的地理空间数据目录,该目录反过来将美国联邦数据联网,包括来自其他联邦机构的其他 ArcSDE 安装。
ArcSDE is integrated into ArcGIS Server; however, information on it can be found at http://www.esri.com/software/arcgis/arcsde.
微软 SQL 服务器
微软在其旗舰数据库产品微软 SQL Server 2008 中增加了空间数据支持。自该版本以来,它已经逐渐改进,但仍远不如甲骨文空间地理信息系统或邮政地理信息系统复杂。Microsoft 支持与 PostGIS 相同的数据类型,但使用略有不同的命名约定,但不直接支持栅格数据。它还支持输出到 WKT 和 WKB 格式。
它为空间选择提供了一些非常基本的支持,但这显然不是微软目前的优先事项。这种有限的支持可能是因为它是所有可用于微软软件映射组件的支持,并且几个第三方引擎可以在 SQL Server 之上提供空间支持。
Microsoft's support for spatial data in SQL Server is documented at the following link: http://msdn.microsoft.com/en-us/library/bb933790.aspx.
关系型数据库
另一个非常受欢迎的免费数据库 MySQL 提供了与微软 SQL Server 几乎相同的支持。基本空间关系函数支持 OGC 几何类型。通过一系列收购,MySQL 已经成为甲骨文的财产。
虽然甲骨文目前仍然致力于将 MySQL 作为开源数据库,但这一购买使世界上最受欢迎的开源数据库的最终未来受到质疑。但是,就地理空间分析而言,MySQL 几乎不是竞争者,也不太可能成为任何项目的首选。
For more information on MySQL spatial support, visit the following link: https://dev.mysql.com/doc/refman/8.0/en/spatial-types.html
太空员
SpatiaLite 是开源 SQLite 数据库引擎的扩展。SQLite 使用文件数据库,旨在集成到应用程序中,而不是大多数关系数据库服务器使用的典型客户端-服务器模型中。SQLite 已经有了空间数据类型和空间索引,但是 SpatiaLite 增加了对 OGC 简单要素规范的支持,以及地图投影。
应该注意的是,非常流行的 SQLite 与 Oracle、PostgreSQL 或 MySQL 不属于同一类别,因为它是为单用户应用程序设计的基于文件的数据库。
SpatiaLite can be found at http://www.gaia-gis.it/gaia-sins/.
地质包装
地理包是一种基于文件的地理数据库格式。官方地理打包网站http://geopackage.org将其描述为:
"An open, standards-based, platform-independent, portable, self-describing, compact format for transferring geospatial information."
这也是对 Esri 文件地理数据库格式的直接回答,也是对开放地理空间社区指定的 shapefile 杀手的直接回答,以取代老化的、部分封闭的 shapefile 格式。这两种格式都是真正的文件规范,需要依靠其他软件来读写数据。
地理打包是 OGC 规范,这意味着它作为行业数据格式的未来是安全的。它也是一种包罗万象的格式,可以处理向量数据、栅格数据、属性信息和扩展来满足新的需求。而且,像任何好的数据库一样,它处理多个层。您可以将整个地理信息系统项目存储在一个包中,从而使数据管理更加简单。
You can read more about Esri's file geodatabase format here: http://desktop.arcgis.com/en/arcmap/10.3/manage-data/administer-file-gdbs/file-geodatabases.htm.
按指定路线发送
路由是计算几何中一个非常小众的领域。这也是一个非常丰富的研究领域,远远超出了熟悉的驾驶方向用例。路由算法的要求只是一个网络数据集和影响网络传输速度的阻抗值。通常,数据集是基于向量的,但栅格数据也可用于某些应用。
这一领域的两个主要竞争者是 Esri 的网络分析师和 PostGIS 的开源 pgRouting 引擎。最常见的路由问题是访问多个点位置的最有效方式。这个问题叫做旅行推销员问题 ( TSP )。旅行商问题是计算几何中研究最深入的问题之一。它通常被认为是任何路由算法的基准。
More information on the TSP can be found at http://en.wikipedia.org/wiki/Travelling_salesman_problem.
Esri 网络分析师和空间分析师
Esri 进入路由领域,即网络分析师,是一个真正通用的路由引擎,可以处理大多数路由应用,而无需考虑上下文。空间分析师是另一个以栅格为中心的 Esri 扩展,它可以对栅格地形数据执行最小成本路径分析。
The ArcGIS Network Analyst product page is located on Esri's website at: http://www.esri.com/software/arcgis/extensions/networkanalyst.
pgRouting
后置地理信息系统的 pgRouting 扩展为地理数据库添加了路由功能。它面向道路网络,但也适用于其他类型的网络数据。
下图显示了 pgRouting 输出的行驶距离半径计算,显示在 QGIS 中。这些点根据它们与起始位置的接近程度,从绿色到红色进行颜色编码。如下图所示,这些点是网络数据集中的节点,由 QGIS.org(https://qgis.org/en/site/)提供,在本例中为道路:

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

因为 QIS 基于 GDAL/OGR 和 GEOS,并且可以使用 PostGIS,所以它支持这些包提供的所有数据源。它也有不错的光栅处理功能。QGIS 非常适合使用可用的扩展来制作纸质地图或整个地图册。
QGIS is well documented on the QGIS website at the following link: http://www.qgis.org/en/documentation.html. You can also find numerous online and video tutorials by searching for QGIS or particular operation.
opencv
OpenEV 是一个开源的地理空间查看器,最初由 Atlantis Scientific 在 2002 年左右开发,在被微软收购之前成为 Vexcel。Vexcel 将 OpenEV 开发为可免费下载的卫星图像浏览器,用于加拿大地理空间数据基础设施 ( CGDI )。它是使用 GDAL 和 Python 构建的,部分由 GDAL 创建者 Frank Warmerdam 维护。
OpenEV 是目前最快的光栅浏览器之一。尽管最初是作为一个查看器设计的,OpenEV 提供了 GDAL 和 PROJ 的所有功能。虽然创建为光栅工具,但它可以覆盖向量数据,如形状文件,甚至支持基本编辑。还可以使用内置的光栅计算器更改光栅图像,并且可以转换、重新投影和裁剪数据格式。
以下截图显示了 OpenEV 查看器窗口中的一个 25 MB、16 位整数 GeoTIFF 高程文件:

OpenEV 主要是用 Python 构建的,它提供了一个 Python 控制台,可以访问程序的全部功能。OpenEV 的图形用户界面没有 QGIS 等其他工具复杂。例如,不能像在 QGIS 中那样将地理空间数据集拖放到查看器中。但是,OpenEV 的原始速度使得它对于简单的光栅观看,或者对于基本的处理和数据转换非常有吸引力。
The OpenEV home page is available at http://openev.sourceforge.net.
地理信息系统
GRASS 是现存最古老的持续开发的地理空间系统之一。美国陆军工程兵部队于 1982 年开始 GRASS 的开发。它最初是为在 Unix 系统上运行而设计的。1995 年,军队发布了最后一个补丁,软件被转移到社区开发,此后一直保留在那里。
即使重新设计了用户界面,GRASS 对现代地理信息系统用户来说仍然有些深奥。然而,由于其几十年的历史和不存在的价格标签,多年来,许多地理空间工作流和高度专业化的模块已经在 GRASS 中实现,使其与许多组织和个人高度相关,特别是在研究社区。由于这些原因,GRASS 仍在积极开发中。
GRASS 也已经和 QGIS 集成在一起,所以可以使用更现代更熟悉的 QGIS GUI 来运行 GRASS 功能。GRASS 还与 Python 深度集成,可以用作库或命令行工具。下面的截图显示了原生 GRASS GUI 中的一些地形分析,该 GUI 是使用wxPython库构建的:

GRASS is housed online at http://grass.osgeo.org/.
格夫吉先生
另一个基于 Java 的桌面 GIS 是 gvSIG 。全球虚拟空间信息小组项目始于 2004 年,是将西班牙巴伦西亚地区基础设施和运输部的信息技术系统迁移到自由软件的更大项目的一部分。结果是 gvSIG,它一直在不断成熟。该特征集大部分可与 QGIS 相媲美,也具有一些独特的功能。
官方 gvSIG 项目有一个非常活跃的分叉叫做 gvSIG 社区版 ( gvSIG CE )。还有一个手机版叫 gvSIG 手机。gvSIG 代码库是开源的。
The official home page for gvSIG is available at http://www.gvsig.org/web/.
OpenJUMP
OpenJUMP 是另一个基于 Java 的开源桌面 GIS。 JUMP 代表 Java 统一制图平台,最初由生动解决方案为不列颠哥伦比亚省政府创建。在生动解决方案交付 JUMP 后,开发停止了。生动解决方案最终将 JUMP 发布到开源社区,并在那里更名为 OpenJUMP。
OpenJUMP 具有读写 shapefiles 和 OGC GML (简称地理标记语言)的能力,支持 PostGIS 数据库。还可以显示来自 OGC WMS (简称网络地图服务器)和 WFS (简称网络要素服务)服务的部分图像格式和数据。它有一个插件架构,也可以作为定制应用的开发平台。
You can find out more about OpenJUMP on the official web page at http://www.openjump.org/.
谷歌地球
谷歌地球无处不在,似乎不值得一提。2001 年首次发布的 EarthViewer 3D(由一家名为 Keyhole Inc .的公司创建)和 EarthViewer 3D 项目由非营利风险投资公司 In-Q-Tel 资助,后者又由美国中央情报局资助。这种间谍机构的血统,以及随后谷歌购买 Keyhole 来创建和分发谷歌地球,让全球关注地理空间分析领域。
自 2005 年谷歌地球首次发布该软件以来,谷歌一直在不断完善它。一些值得注意的新增功能是创建了谷歌月亮、谷歌火星、谷歌天空和谷歌海洋。这些是虚拟地球应用程序,以月球和火星的数据为特色,但谷歌海洋除外,它在谷歌地球上增加了海底高程测绘,即水深测量。
谷歌地球引入了旋转虚拟地球概念,用于探索地理数据。在看了几个世纪的 2D 地图或低分辨率物理地球仪后,虚拟地绕着地球飞行,落在世界任何地方的街角都令人惊叹——尤其是对地理空间分析师和其他地理爱好者来说,正如下面俯瞰路易斯安那州新奥尔良中央商务区的谷歌地球截图所示:

正如谷歌通过其基于图块的滑动制图方法彻底改变了网络制图一样,虚拟地球概念是地理空间可视化的一大推动力。
最初的兴奋消失后,许多地理空间分析师意识到谷歌地球是一个非常生动有趣的地理探索工具,但它对任何有意义的地理空间分析的效用都非常有限。谷歌地球正好属于地理空间浏览器软件领域。
它消耗的唯一数据格式是其原生的锁眼标记语言 ( KML ),这是一种集数据和样式于一体的格式,在第 2 章、学习 地理空间数据中进行了讨论。由于这种格式现在是 OGC 标准,只使用一种数据格式会立即限制任何工具的效用。任何涉及谷歌地球的项目都必须首先从 KML 完整的数据转换和样式开始,让人想起大约 10-20 年前的地理空间分析。支持 KML 的工具,包括谷歌地图,只支持 KML 的有限部分。
谷歌地球的原生数据集覆盖全球,但它是跨越几年和来源的数据集的混合。谷歌已经大大改进了工具中的内联元数据,它可以识别当前视图的来源和大致日期。但是,这种方法在外行人中造成了混乱。许多人认为,谷歌地球中的数据更新频率远远高于实际情况。谷歌街景系统显示了世界大部分地区的街道级 360 度视图,这在一定程度上有助于纠正这种误解。
人们能够很容易地识别出几年前熟悉地点的图像。谷歌地球造成的另一个常见误解是,整个世界已经被详细绘制,因此创建地理空间分析的基础地图应该是微不足道的。正如第 2 章学习 地理空间数据中所讨论的,使用现代数据和软件绘制感兴趣区域的地图远比几年前容易,但这仍然是一项复杂且劳动密集型的工作。这种误解是地理空间分析师在启动项目时必须管理的第一批客户期望之一。
尽管有这些误解,谷歌对地理空间分析的影响几乎完全是积极的。几十年来,发展地理空间产业面临的最大挑战之一是让潜在的利益相关者相信,在做出有关人员、资源和环境的决策时,地理空间分析几乎总是最好的方法。这个障碍与汽车经销商形成鲜明对比。当潜在客户来到停车场时,销售员不需要说服买家他们需要一辆车,只需要告诉他们车的类型。
地理空间分析师必须首先对项目发起人进行技术教育,然后说服他们地理空间方法是应对挑战的最佳方式。对于分析师来说,谷歌基本上取消了这些步骤。
Google Earth can be found online at http://www.google.com/earth/index.html.
美国宇航局世界风
NASA WorldWind 是一个开源的虚拟地球仪和地理空间浏览器,最初由美国国家航空航天局于 2004 年发布。它最初是基于微软的。NET 框架,使其成为一个以 Windows 为中心的应用程序。
下面这张 NASA WorldWind 的截图看起来和谷歌地球很像:

2007 年,一款基于 Java 的软件开发工具包 ( SDK )发布,名为 WorldWind Java ,使得 WorldWind 更加跨平台。向 Java 的过渡也导致了 WorldWind 浏览器插件的创建。
WorldWind Java 软件开发工具包被认为是一个软件开发工具包,而不是像。. NET 版本但是,SDK 附带的演示提供了一个无需任何额外开发的查看器。虽然美国宇航局的世界风最初是受谷歌地球的启发,但它作为开源项目的地位使它走向了一个完全不同的方向。
谷歌地球是一个受 KML 规范限制的通用工具。美国宇航局的世界风现在是一个平台,任何人都可以在其上无限制地发展。随着新类型数据的出现和计算资源的增长,虚拟地球范式的潜力无疑为地理空间可视化带来了更多尚未被发掘的潜力。
NASA WorldWind is available online at http://worldwind.arc.nasa.gov/java/.
ArcGIS
Esri 走的路线是成为地理空间分析方法的最大推动者之一,以了解我们的世界,并且是一家私人控股的盈利企业,必须在一定程度上关注自身利益。ArcGIS 软件套件代表了所有已知的地理空间可视化类型,包括向量、栅格、球体和 3D。它也是许多国家的市场领导者。如本章前面的地理空间软件地图所示,Esri 越来越多地将开源软件纳入其工具套件,包括用于栅格显示的 GDAL,以及作为 ArcGIS 脚本语言的 Python。
下面的截图显示了带有海洋跟踪密度数据分析的核心 ArcGIS 应用程序 ArcMap。该界面与 QGIS 有很多共同之处,如本截图https://marinecadastre.gov/提供:

The ArcGIS product page is available online at http://www.esri.com/software/arcgis.
现在我们已经了解了可视化和分析数据的工具,让我们来看看如何管理数据。
理解元数据管理
数据在互联网上的分布增加了元数据的重要性。数据保管人能够向全世界发布数据集供下载,而无需任何个人交互。地理空间数据集的元数据记录可以遵循这一点,以帮助确保维护这些数据的完整性和可问责性。
适当格式化的元数据还允许自动编目、搜索索引和数据集集成。元数据已经变得如此重要,以至于地理空间社区中的一个常见口号是没有元数据的数据不是数据,这意味着没有元数据,地理空间数据集就无法得到充分利用和理解。
下一节列出了一些可用的常见元数据工具。元数据管理的 OGC 标准是网络目录服务,它创建了一个基于元数据的目录系统和一个用于分发和发现数据集的应用编程接口。
Python 的 pycsw 库
pycsw是一个符合 OGC 标准的 CSW,用于发布和发现地理空间元数据。它支持多种应用编程接口,包括 CSW 2/CSW 3、开放搜索、OAI-PMH 和 SRU。它非常轻便,是纯 Python。有关使用pycsw构建的 CSW 和客户端的优秀示例,请参见太平洋岛屿海洋观测系统 ( 太平洋观测系统)目录,可通过以下链接获得:http://pacioos.org/search/。pycsw库也用在一个更大的名为地理节点的包中。
地理编码
GeoNode 是一个基于 Python 的地理空间内容管理系统。它将地理空间数据创建、元数据和可视化结合在一个服务器包中。它还包括社交功能,如评论和评级系统。它是开源的,可在http://geonode.org/获得。以下截图来自 GeoNode 在线演示:

GeoNode 和pycsw是 Python 的两个主要元数据工具。接下来,我们将看看一些用其他语言编写的工具。
地理网络
GeoNetwork 是一个开源的、基于 Java 的目录服务器,用于管理地理空间数据。它包括一个元数据编辑器和搜索引擎,以及一个交互式网络地图查看器。该系统旨在全球连接空间数据基础设施。它可以使用元数据编辑工具通过网络发布元数据。它还可以通过嵌入式地理服务器地图服务器发布空间数据。它具有用户和组安全权限,以及 web 和桌面配置实用程序。
还可以配置地理网络,以便按计划的时间间隔从其他目录中获取元数据。以下是联合国粮食及农业组织实施地理网络的截图:

You can find out more about GeoNetwork at http://geonetwork-opensource.org/.
摘要
在本章中,您学习了地理空间分析软件的层次结构。您还学习了一个处理数百个现有地理空间软件包和库的框架,将它们分为一个或多个主要功能,包括数据访问、计算几何、栅格处理、可视化和元数据管理。
我们还检查了常用的基础库,包括 GDAL、OGR、PROJ 和 GEOS,这些库在地理空间软件中反复出现。你可以走近任何一款新的地理空间软件,追溯到这些核心库,然后问,wT2【增值是什么?更好地了解套餐。如果软件没有使用这些库中的一个,你需要问,为什么这些开发人员要违背规则?为了理解该系统带来了什么。
本章中只提到了几次 Python,以避免在理解地理空间软件领域时分心。但是,正如我们将会看到的,Python 在本章中被交织到每一个单独的软件中,它本身就是一个完全有能力的地理空间工具。Python 是 ArcGIS、QGIS、GRASS 和许多其他包的官方脚本语言,这绝非巧合。GDAL、OGR、PROJ、CGAL、JTS、GEOS 和 PostGIS 都有 Python 绑定也不是偶然的。
至于这里没有提到的包,它们都是 Python 通过 Jython Java 发行版 IronPython 掌握的。NET 发行版,Python 的各种数据库和 web APIs,以及内置的ctypes模块。作为一名地理空间分析师,如果有一项技术你不能错过,那就是 Python。
在下一章中,我们将看到 Python 是如何进入地理空间行业的。我们还将学习地理信息系统脚本语言、混搭粘合语言和成熟的编程语言。
进一步阅读
以下是您可以参考的网页列表:
四、地理空间 Python 工具箱
本书的前三章涵盖了地理空间分析的历史、分析师使用的地理空间数据类型以及地理空间行业中的主要软件和库。我们到处使用一些简单的 Python 示例来说明某些要点,但我们主要关注地理空间分析领域,与任何特定技术无关。从这里开始,我们将使用 Python 来征服地理空间分析,我们将在本书的剩余部分继续使用这种方法。本章解释了您在工具箱中需要的软件,以便在地理空间领域做任何您想做的事情。
我们将发现用于访问不同类型数据的 Python 库,这些数据位于第 2 章、学习地理空间数据的向量数据和栅格数据部分。其中一些库是纯 Python,以及我们在第 3 章、地理空间技术领域中看到的不同软件包的一些绑定。
在本章中,我们将涵盖以下主题:
- 安装第三方 Python 模块
- Python 虚拟环境
- 伯爵夫人
- 码头工人
- 用于获取数据的 Python 网络库
- 基于 Python 标签的解析器
- Python JSON 库
- OGR
- PyShp
- DBFPY
- 形状美观的
- 断续器
- 菲奥纳
- NumPy
- 地球 andas
- Python 图像库(PIL)
- PNGCanvas
- ReportLab(报告实验室)
- 地球资源(geopdf)
- Python NetCDF 库
- Python HDF 库
- OSMnx
- 空间索引库
- Jupyter
- 伯爵夫人
我们将尽可能检查纯 Python 解决方案。Python 是一种非常有能力的编程语言,但是有些操作,尤其是遥感操作,计算量太大,因此在使用纯 Python 或其他解释语言时不切实际。幸运的是,地理空间分析的每个方面都可以通过 Python 以某种方式解决,即使它绑定到一个高效的 C/c++/其他编译语言库。
我们将避免使用涵盖地理空间分析以外的其他领域的广泛科学库,以使解决方案尽可能简单。使用 Python 进行地理空间分析的原因有很多,但最有力的论据之一是它的可移植性。
此外,Python 已经作为 Jython 发行版移植到了 Java,并移植到了。NET 公共语言运行时 ( CLR )作为 IronPython。Python 也有类似 Stackless Python 的版本,用于大规模并发程序。Python 的一些版本被设计为在集群计算机上运行,用于分布式处理。Python 也可以在许多不允许您安装自定义可执行文件的托管应用服务器上使用,例如谷歌应用引擎平台,它有一个 Python 应用编程接口。
技术要求
- Python 3.6 或更高版本
- 内存:最小 6 GB (Windows),8 GB (macOS)建议 8 GB
- 存储:最低 7200 转/分的 SATA,可用空间为 20gb;推荐的具有 40 GB 可用空间的固态硬盘
- 处理器:最低英特尔酷睿 i3 2.5 GHz 推荐的英特尔酷睿 i5
安装第三方 Python 模块
用纯 Python 编写的模块(使用标准库)将主要运行在 Python(https://www.python.org/)网站提到的 20 个平台中的任何一个平台上。每次添加依赖于绑定到其他语言外部库的第三方模块时,都会降低 Python 固有的可移植性。您还增加了一层复杂性,通过向组合中添加另一种语言来从根本上改变代码。纯 Python 让事情变得简单。此外,Python 到外部库的绑定往往是自动或半自动生成的。
这些自动生成的绑定非常通用和深奥,它们只是使用 C/C++ API 中的方法名将 Python 连接到该 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 模块,也可以将其放在您的 Python site-packages目录中。当您尝试导入模块时,这两个目录在 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']
These installation methods will be used in the rest of this book. You can find the latest Python version, the source code for your platform installation, and compilation instructions at http://python.org/download/.
Python virtualenv模块允许您为特定项目轻松创建 Python 的独立副本,而不会影响您的主要 Python 安装或其他项目。使用此模块,您可以使用同一库的不同版本进行不同的项目。一旦你有了一个可以工作的代码库,你就可以将它与你使用的模块甚至 Python 本身的变化隔离开来。virtualenv模块使用简单,可以用于本书的任何示例;但是,不包括其使用的明确说明。
To get started with virtualenv, follow this simple guide: http://docs.python-guide.org/en/latest/dev/virtualenvs/.
Python virtualenv
Python 地理空间分析要求我们使用各种具有许多依赖性的模块。这些模块通常使用特定版本的 C 或 C++库相互构建。当您向系统中添加 Python 模块时,经常会遇到版本冲突。有时,当您升级一个特定的模块时,由于应用编程接口的变化,它可能会破坏您现有的 Python 程序,或者您可能同时运行 Python 2 和 Python 3 来利用为每个版本编写的库。您需要的是一种安全安装新模块而不损坏工作系统或代码的方法。这个问题的解决方案是通过virtualenv模块使用 Python 虚拟环境。
Python virtualenv模块为每个项目创建独立的 Python 环境,这样您就可以避免冲突模块污染您的主要 Python 安装。您可以通过激活或停用特定环境来打开或关闭它。virtualenv模块是高效的,因为它不会在您每次创建环境时复制您的整个系统 Python 安装。让我们开始吧:
- 安装
virtualenv就像运行以下代码一样简单:
pip install virtualenv
- 然后,为虚拟 Python 环境创建一个目录。想叫什么就叫什么:
mkdir geospatial_projects
- 现在,您可以使用以下命令创建第一个虚拟环境:
virtualenv geospatial_projects/project1
- 然后,输入以下命令后,您可以激活环境:
source geospatial_projects/project1/bin/activate
- 现在,当您在该目录中运行任何 Python 命令时,它将使用隔离的虚拟环境。完成后,您可以使用以下简单命令停用该环境:
deactivate
这是您安装、激活使用和停用virtualenv模块的方法。然而,还有一个环境你应该知道。我们接下来会研究这个。
伯爵夫人
这里还值得一提的是 Conda,它是一个开源的、跨平台的包管理系统,也可以创建和管理类似virtualenv的环境。Conda 使安装复杂的包变得容易,包括地理空间包。除了 Python 之外,它还可以与其他语言一起工作,包括 R、Node.js 和 Java。
这里有 conda:https://docs.conda.io/en/latest/。
现在,让我们看看如何安装 GDAL,以便我们可以开始处理地理空间数据。
安装 GDAL
包括 OGR 在内的地理空间数据抽象库 ( GDAL )对于本书中的许多示例至关重要,也是更复杂的 Python 设置之一。由于这些原因,我们将在这里单独讨论。PyPI 上有最新的 GDAL 绑定;但是,由于 GDAL 库需要额外的资源,安装需要更多的步骤。
有三种方法可以安装 GDAL,以便与 Python 一起使用。您可以使用其中任何一种:
- 从源代码编译它。
- 将其作为更大软件包的一部分安装。
- 安装二进制发行版,然后安装 Python 绑定。
如果你有编译 C 库以及所需编译软件的经验,那么第一个选项给你最大的控制权。但是,如果您只想尽快开始,就不建议这样做,因为即使是有经验的软件开发人员也会发现编译 GDAL 和相关的 Python 绑定具有挑战性。在领先平台上编译 GDAL 的说明可以在http://trac.osgeo.org/gdal/wiki/BuildHints找到。PyPI GDAL 页面上也有基本的构建说明;看看https://pypi.python.org/pypi/GDAL。
第二种选择是迄今为止最快最简单的。开源地理空间基金会 ( OSGeo )发布了一个名为 OSGeo4W 的安装程序,只需点击一个按钮,就可以在 Windows 上安装所有顶级开源地理空间包。OSGeo4W 可以在http://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 是针对
win32的 3.4.2 版本,也就是说是 32 位版本。一旦你有了这些信息,请访问以下网址:http://www.lfd.uci.edu/~gohlke/pythonlibs/#gdal。 - 这个网页包含几乎每个开源科学图书馆的 Python 窗口二进制文件和绑定。在该网页的 GDAL 部分,找到与您的 Python 版本相匹配的版本。发布名称使用 C Python 的缩写
cp,后跟主要的 Python 版本号,32 位窗口使用win32,64 位窗口使用win_amd64。
In the previous example, we would download the file named GDAL-1.11.3-cp34-none-win32.whl.
- 该下载包采用较新的 Python
pip轮格式。要安装它,只需打开命令提示符并键入以下代码:
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的形式返回它的版本。
If you have trouble installing modules using easy_install or pip and PyPI, try to download and install the wheel package from the same site as the GDAL example.
Linux 操作系统
Linux 上的 GDAL 安装因发行版而异。下面的https://gdal.org二进制网页列出了几个发行版的安装说明:http://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
mac os x
要在 macOS X 上安装 GDAL,还可以使用家酿软件包管理系统,该系统在http://brew.sh/提供。
或者,您可以使用 MacPorts 软件包管理系统,该系统可在https://www.macports.org/获得。
这两个系统都有很好的文档记录,并且包含 Python 3 的 GDAL 包。只有那些需要用 C 语言编写的正确编译的二进制文件的库才真正需要它们,这些文件有很多依赖项,并且包括许多科学和地理空间库。
用于获取数据的 Python 网络库
绝大多数地理空间数据共享都是通过互联网完成的,Python 在几乎任何协议的网络库方面都有很好的装备。自动数据下载通常是地理空间过程自动化的重要一步。数据通常从网站的统一资源定位符 ( 网址)或文件传输协议 ( 文件传输协议)服务器中检索,由于地理空间数据集通常包含多个文件,数据通常以 ZIP 文件的形式分发。
Python 的一个很好的特性是它的类似文件的对象的概念。大多数读取和写入数据的 Python 库使用一组标准方法,允许您从不同类型的资源中访问数据,就像您在磁盘上写一个简单的文件一样。Python 标准库中的网络模块也使用这种约定。这种方法的好处是,它允许您将类似文件的对象传递给其他库和方法,这些库和方法可以识别约定,而无需对以不同方式分布的不同类型的数据进行大量设置。
Python urllib 模块
Python urllib包是为简单访问任何带有网址的文件而设计的。Python 3 中的urllib包由几个模块组成,处理管理网络请求和响应的不同部分。这些模块实现了 Python 的一些类似文件的对象约定,从它的open()方法开始。当您调用open()时,它准备连接到资源,但不访问任何数据。有时,您只想抓取一个文件并将其保存到磁盘,而不是在内存中访问它。该功能可通过urllib.request.retrieve()方法获得。
以下示例使用urllib.request.retrieve()方法下载名为hancock.zip的压缩形状文件,该文件在其他示例中使用。我们将网址和本地文件名定义为变量。该网址作为一个参数以及我们想要使用的文件名传递,以将其保存到我们的本地机器上,在本例中,本地机器只是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模块的消息确认文件已下载到当前目录。网址和文件名也可以作为字符串直接传递给retrieve()方法。如果只指定文件名,下载将保存到当前工作目录。您也可以指定一个完全限定的路径名,将其保存到其他地方。您还可以指定一个回调函数作为第三个参数,它将接收文件的下载状态信息,以便您可以创建一个简单的下载状态指示器或执行一些其他操作。
urllib.request.urlopen()方法允许您更精确和更可控地访问在线资源。正如我们前面提到的,它实现了除seek()方法之外的大多数类似 Python 文件的对象方法,该方法允许您跳转到文件中的任意位置。您可以一次一行地在线读取文件,以列表形式读取所有行,读取指定数量的字节,或者遍历文件的每一行。所有这些功能都在内存中执行,因此您不必将数据存储在磁盘上。此功能对于在线访问经常更新的数据非常有用,您可能希望在不保存到磁盘的情况下处理这些数据。
在下面的例子中,我们通过访问美国地质调查局 ( 美国地质勘探局)地震源来查看过去一小时内发生的所有地震,从而展示了这一概念。这些数据以逗号分隔值 ( CSV )文件的形式分发,我们可以像文本文件一样逐行读取。CSV 文件类似于电子表格,可以在文本编辑器或电子表格程序中打开:
-
首先,您需要打开网址并读取文件中带有列名的标题。
-
然后,您需要读取第一行,其中包含最近地震的记录,如以下代码行所示:
>>> 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 解释器中运行这个例子,需要按两次回车或回车键来执行循环。这个动作是必要的,因为它向解释器发出信号,表明您已经完成了循环的构建。在以下示例中,我们将输出缩写为:
>>> 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模块具有以下特点:
- 保活和连接池
- 国际域名和网址
- 具有 cookie 持久性的会话
- 浏览器风格的 SSL 验证
- 自动内容解码
- 基本/摘要身份验证
- 精美的钥匙/价值饼干
- 自动减压
- Unicode 响应正文
- 超文本传输协议代理支持
- 多部分文件上传
- 流式下载
- 连接超时
- 分块请求
.netrc支持
在下面的例子中,我们将下载与使用urllib模块下载的相同的 ZIP 文件,除了这次使用requests模块。首先,我们需要安装requests模块:
pip install requests
然后,我们可以导入它:
import requests
然后,我们可以为网址和输出文件名设置变量:
url = "https://github.com/GeospatialPython/Learning/raw/master/hancock.zip"
fileName = "hancock.zip"
检索 ZIP 文件就像使用requests模块的get()方法一样简单:
r = requests.get(url)
现在,我们可以从.zip文件中获取内容,并将其写入我们的输出文件:
with open(fileName, 'wb') as f:
f.write(r.content)
requests模块有很多更高级的特性,和这个例子一样容易使用。现在我们知道了如何通过 HTTP 协议获取信息,让我们来研究一下 FTP 协议,它通常用于从在线档案中访问地理空间数据。
文件传送协议
FTP 允许您浏览在线目录,并使用 FTP 客户端软件下载数据。直到 2004 年左右,当地理空间网络服务变得非常普遍时,FTP 才是分发地理空间数据的最常见方式之一。FTP 现在不太常见了,但是在搜索数据时偶尔会遇到。同样,Python 的电池标准库有一个名为ftplib的合理的 FTP 模块,它有一个名为FTP()的主类。
在以下示例中,我们将执行以下操作:
- 我们将访问由美国国家海洋和大气管理局托管的文件传输协议服务器,以访问一个文本文件,该文件包含来自“T4”深海海啸评估和报告网络的数据,该网络用于监视世界各地的海啸。这个特殊的浮标位于秘鲁海岸。
- 我们将定义服务器和目录路径,然后访问服务器。所有 FTP 服务器都需要用户名和密码。大多数公共服务器都有一个名为 anonymous 的用户,其密码为 anonymous,就像这个服务器一样。
- 使用 Python 的
ftplib,只需调用login()方法,无需任何参数即可作为默认匿名用户登录。否则,您可以添加用户名和密码作为字符串参数。 - 登录后,我们将转到包含 DART 数据文件的目录。
- 为了下载文件,我们将打开一个被调用的本地文件,并将其
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 上需要。
如果你只是从一个文件传输协议服务器下载一个简单的文件,许多文件传输协议服务器也有一个网络接口。在这种情况下,您可以使用urllib读取文件。FTP 网址使用以下格式访问数据:
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 归档文件进行写入时,我们为 gzipped 压缩指定写入模式为w:gz。我们还将文件扩展名指定为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()
我们将通过结合本章中学习的元素以及第 2 章、学习地理空间数据的向量数据部分中的元素,来研究另一个示例。我们将从hancock.zip文件中读取边界框坐标,而不会将其保存到磁盘中。我们将使用 Python 的类似文件的对象约定来传递数据。然后,我们将使用 Python 的struct模块来读取边界框,就像我们在第 2 章、学习地理空间数据中所做的那样。
在这种情况下,我们将解压后的.shp文件读入一个变量,并通过指定以冒号(:)分隔的数据的开始和结束索引,使用 Python 数组切片来访问数据。我们能够使用列表切片,因为 Python 允许您将字符串视为列表。在本例中,我们还使用 Python 的StringIO模块将数据临时存储在内存中的一个类似文件的对象中,该对象实现了各种方法,包括大多数 Python 网络模块都没有的seek()方法,如下面几行代码所示:
>>> 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 比其他解析器更宽容。通常,可靠性比速度或内存效率更重要。
http://lxml.de/performance.html 提供的分析为不同 Python XML 解析器之间的内存和速度提供了基准。
minidom 模块
Python minidom模块是一个非常古老且使用简单的 XML 解析器。它是 Python 在 XML 包中内置的一组 XML 工具的一部分。它可以解析 XML 文件或作为字符串输入的 XML。minidom模块最适合速度开始下降前小于 20 MB 左右的中小型 XML 文档。
为了演示minidom模块,我们将使用一个示例 KML 文件,它是谷歌 KML 文档的一部分,您可以下载。以下链接中的数据代表从全球定位系统设备传输的时间戳点位置:让我们开始吧:
- 首先,我们将通过从文件中读入数据并创建一个
minidom解析器对象来解析这些数据。该文件包含一系列<Placemark>标签,这些标签包含一个点和收集该点的时间戳。因此,我们将获得文件中所有Placemarks的列表,我们可以通过检查该列表的长度来计数它们,如以下代码行所示:
>>> from xml.dom import minidom
>>> kml = minidom.parse("time-stamp-point.kml")
>>> Placemarks = kml.getElementsByTagName("Placemark")
>>> len(Placemarks)
361
- 如你所见,我们取回了所有
Placemarks,总计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 更具可读性。 -
现在,如果我们想从这个地标中获取坐标呢?坐标被隐藏在
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'
If you're new to Python, you'll notice that the text output in these examples is tagged with the letter u. This markup is how Python denotes Unicode strings that support internationalization to multiple languages with different character sets. Python 3.4.3 changes this convention slightly and leaves Unicode strings unmarked while marking UTF-8 strings with a b.
- 我们可以更进一步,通过拆分字符串并将结果字符串转换为 Python 浮点类型,将这个
point字符串转换为可用数据,如下所示:
>>> 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库所能做的事情的表面。关于这个库的一个很好的教程,请看下面的教程:https://www.edureka.co/blog/python-xml-parser-tutorial/。
elemonttree
minidom模块是纯 Python,容易上手,从 Python 2.0 开始就有了。然而,Python 2.5 在名为ElementTree的标准库中添加了一个更高效但更高级的 XML 解析器。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 的信息:https://www.w3schools.com/xml/xpath_intro.asp。
这个特性的一个缺点是,如果文档像大多数 XML 文档一样指定了一个名称空间,那么您必须将该名称空间插入到查询中。ElementTree不会自动为你处理命名空间。您可以手动指定它,或者尝试使用字符串解析从根元素的标记名中提取它。
我们将使用ElementTree重复minidomXML解析示例:
- 首先,我们将解析文档,然后手动定义 KML 命名空间;稍后,我们将使用 XPath 表达式和
find()方法来查找第一个Placemark元素。 - 最后,我们将找到坐标和子节点,然后抓取包含纬度和经度的文本。
在这两种情况下,我们都可以直接搜索coordinates标签。但是,通过抓取Placemark元素,它为我们提供了稍后抓取相应时间戳子元素的选项,如果我们选择这样做的话,如下面几行代码所示:
>>> 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 语言中的字符串格式概念。当我们为地标变量定义 XPath 表达式时,我们使用了%占位符来指定字符串的插入。然后,在字符串之后,我们使用%操作符后跟一个变量名来插入占位符所在的ns名称空间变量。在coordinates变量中,我们两次使用了ns变量,因此我们在字符串后两次指定了包含ns的元组。
String formatting is a simple yet extremely powerful and useful tool in Python that's worth learning. You can find more information in Python's documentation online at the following link: https://docs.python.org/3.4/library/string.html.
使用元素树和迷你 dom 构建 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模块有一个类似的界面,这在马克·皮尔格林所著的《潜入 Python》一书中有记载,在我们刚刚看到的minidom例子中也有引用。
像minidom和ElementTree这样的 XML 解析器在格式完美的 XML 文档上工作得非常好。不幸的是,绝大多数的 XML 文档不遵循这些规则,并且包含格式错误或无效字符。您会发现,您经常被迫处理这些数据,必须借助非凡的字符串解析技术来获取您实际需要的一小部分数据。但是多亏了 Python 和美丽的汤,您可以优雅地处理糟糕甚至糟糕的基于标签的数据。
美丽的汤是一个模块,专门设计来稳健地处理破碎的 XML。它面向 HTML,HTML 因格式不正确而臭名昭著,但也适用于其他 XML 方言。PyPI 上有美人汤,所以使用easy_install或pip安装,如下命令所示:
easy_install beautifulsoup4
或者,您可以执行以下命令:
pip install beautifulsoup4
然后,要使用它,您只需导入它:
>>> from bs4 import BeautifulSoup
为了尝试一下,我们将使用智能手机应用程序中的全球定位系统交换格式 ( 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 都是完美的,只有一个例外。进入美丽的汤。这个库毫不犹豫地将糟糕的 XML 分割成可用的数据,它可以处理比丢失标签更糟糕的缺陷。尽管缺少标点符号或其他语法,它仍能工作,并能给你最好的数据。它最初是为解析 HTML 而开发的,HTML 格式不佳是出了名的可怕,但它也能很好地处理 XML,如下所示:
>>> from bs4 import BeautifulSoup
>>> gpx = open("broken_data.gpx")
>>> soup = BeautifulSoup(gpx.read(), features="xml")
>>>
没有美丽汤的抱怨!为了确保数据实际可用,让我们尝试访问一些数据。《美丽的汤》的一个奇妙的特性是它将标签变成了解析树的属性。如果有多个同名标签,它会抓取第一个标签。我们的样本数据文件有数百个<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
如果我们将解析后的数据写回到一个文件中,美丽的汤会输出正确的版本。我们将使用美丽的汤模块的prettify()方法将固定数据保存为一个新的 GPX 文件,用漂亮的缩进来格式化 XML,如下面几行代码所示:
>>> fixed = open("fixed_data.gpx", "w")
>>> fixed.write(soup.prettify())
>>> fixed.close()
美丽的汤是一个非常丰富的图书馆,有更多的功能。如需进一步了解,请访问在线美人汤文档。
虽然minidom、ElementTree和cElementTree都有 Python 标准库,但是还有一个更强大、更流行的 Python XML 库叫做lxml。lxml模块使用ElementTree应用编程接口为libxml2和libxslt C 库提供了一个 Pythonic 接口。一个更好的事实是lxml也与美丽的汤一起解析基于标签的坏数据。在某些安装中,beautifulsoup4可能需要lxml.``lxml模块可通过 PyPI 获得,但需要一些额外的 C 库步骤。更多信息可在lxml主页以下链接获得:http://lxml.de/。
知名文本(WKT)
WKT 格式已经存在多年,是一种简单的基于文本的格式,用于表示几何图形和空间参考系统。它主要被实现 OGC 简单功能的系统用作数据交换格式。看看下面多边形的 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 为我们在第 3 章、地理空间技术景观中描述的几何引擎-开源 ( GEOS )库提供了非常面向 Python 或 Python 的接口。
您可以使用easy_install或pip安装 Shapely。您也可以使用我们在上一节中提到的站点上的轮子。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 的二进制对应物 W 众所周知的二进制 ( WKB ),它用于将 WKT 字符串作为二进制对象存储在数据库中。Shapely 使用其wkb模块加载 WKB 的方式与wkt模块相同,它可以通过调用该对象的wkb属性来转换几何图形。
Shapely 是处理 WKT 数据最常用的 Python 方式,但是您也可以使用 Python 绑定到 OGR 库,我们在本章前面已经安装了该库。
对于这个例子,我们将使用一个简单多边形的 shapefile,它可以作为 ZIP 文件下载。可通过以下链接获得:https://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 )正迅速成为许多领域的头号数据交换格式。轻量级的语法及其与现有数据结构的相似性,无论是 Python 借用的 JavaScript 数据结构,还是 JavaScript 本身,都使其成为 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"
}
}
}
此示例只是一个具有新属性的简单点,这些属性将存储在几何图形的属性数据结构中。在前面的示例中,标识、坐标和 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 应用编程接口的样本点形状文件。示例 shapefile 可以在这里作为 ZIP 文件下载:https://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 会变得相当冗长。接下来,我们将研究一种更简单的方法来处理 shapefiles。
PyShp
PyShp 是一个简单、纯粹的 Python 库,可以读写 shapefiles。它不执行任何几何操作,只使用 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文件,因为它们是 shapefile 规范的一部分。.dbf文件包含形状文件的属性和字段。然而,这两个库都有非常基本的.dbf支持。偶尔,你需要做一些繁重的 DBF 工作。dbfpy3模块是一个纯 Python 模块,专门用于处理.dbf文件。它目前托管在 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
下面的 shapefile 有 600 多条.dbf记录,代表美国人口普查局的大片,是试用dbfpy : 的好样本。
让我们打开这个 shapefile 的.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 都可以执行相同的过程,但是dbfp3y如果您只是对进行了大量的更改,就会变得稍微容易一些。dbf 文件。
形状美观的
在知名文字 ( WKT )一节中提到了匀称的进口和出口。然而,它的真正目的是作为一个通用几何图形库。Shapely 是 GEOS 库的高级 Pythonic 接口,用于几何操作。事实上,Shapely 有意避免读取或写入文件。它完全依赖于其他模块的数据导入和导出,并保持对几何操作的关注。
让我们做一个快速的 Shapley 演示,我们将定义一个单一的 WKT 多边形,然后将其导入 Shapley。然后,我们将测量面积。我们的计算几何将包括用五个任意单位的度量来缓冲该多边形,这将返回一个新的、更大的多边形,我们将为其测量面积:
>>> 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 一样干净的 Pythonic API 来打这样的拳肯定是下一个最好的事情。
菲奥纳
Fiona 库提供了一个围绕 OGR 库的简单 Python API,用于数据访问等等。这种方法使用起来很容易,并且在使用 Python 时没有 OGR 那么冗长。默认情况下,Fiona 输出 GeoJSON。你可以在http://www.lfd.uci.edu/~gohlke/pythonlibs/#fiona找到菲奥娜的车轮档案。
作为一个例子,我们将使用我们在本章前面看到的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 shape file(ESRI shape file)
接下来,我们将检查其坐标参考系统,并获得数据边界框,如下所示:
>>> 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 是栅格数据的主要地理空间库。它的栅格功能非常重要,几乎是任何语言的每个地理空间工具包的一部分,Python 也不例外。要了解 GDAL 如何在 Python 中工作的基本知识,请下载以下示例光栅卫星图像作为 ZIP 文件并解压缩:https://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 获得,也可以作为轮子文件(可在http://www.lfd.uci.edu/~gohlke/pythonlibs/#numpy获得)获得,安装起来很方便。除了惊人的速度,NumPy 的魔力还包括它与其他库的交互。NumPy 可以与 GDAL、Shapely、 Python 成像库 ( PIL )以及其他领域的许多其他科学计算 Python 库交换数据。
作为 NumPy 能力的一个快速例子,我们将把它与 GDAL 结合起来,读入我们的样本卫星图像,然后创建它的直方图。GDAL 和 NumPy 之间的接口是一个名为gdal_array的 GDAL 模块,它有 NumPy 作为依赖。NumPy 模块的旧名称是 NumPy。gdal_array模块导入 NumPy。
在以下示例中,我们将使用导入 NumPy 的gdal_array,以数组形式读取图像,抓取第一个波段,并将其保存为 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 中,您可能希望使用枕头模块,这是 PIL 的升级版本。正如您将在下面的示例中看到的,我们可以使用 Python try 语句,根据您的安装方式,使用两种可能的变体来导入 PIL。
在这个例子中,我们将结合 PyShp 和 PIL 来光栅化前面例子中的hancock形状文件,并将其保存为图像。我们将使用类似于第 1 章、中的简单地理信息系统的世界到像素坐标转换,学习使用 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,因为您没有管理权限来安装用 c 语言创建和编译的 Python 模块。在这种情况下,您通常可以使用轻量级纯 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 没有内置的填充方法:

地球 andas
Pandas 是一个高性能的 Python 数据分析库,可以处理大型数据集,包括表格(类似于数据库)、有序/无序、标记矩阵或未标记的统计数据。GeoPandas 只是 Pandas 的地理空间扩展,它建立在 Shapely、Fiona、PyProj、Matplotlib 和笛卡尔的基础上,所有这些都必须安装。它允许您用 Python 轻松地执行操作,否则需要空间数据库,如 PostGIS。你可以从http://www.lfd.uci.edu/~gohlke/pythonlibs/#panda下载一个地球 andas 的轮子文件。
下面的脚本打开一个 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(可在http://dev.mysql.com/downloads获得)数据库正在逐步发展空间功能。它支持 OGC 几何和一些空间功能。它还有一个在 PyMySQL 库中可用的纯 Python 应用编程接口。有限的空间函数使用平面几何和边界矩形,而不是球形几何和形状。MySQL 的最新开发版本包含一些改进这一功能的附加功能。
在下面的例子中,我们将在 MySQL 中创建一个名为spatial_db的数据库。然后,我们将添加一个名为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,因为它是名为fpdf的 PHP 语言模块的一部分。本模块使用一个名为单元格的概念来布局页面上特定位置的项目。作为一个快速的例子,我们将把我们从 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 阅读器(如苏门答腊 PDF)中打开名为map.pdf的 PDF 文件,您会看到图像现在位于 A4 页面的中心。地理空间产品通常作为大型报告的一部分,PyFPDF 模块简化了自动生成 PDF 格式的报告。
地理空间 PDF
便携式文档格式或 PDF 是一种文件格式,用于以跨平台和独立于应用程序的方式存储和呈现数字格式的文本和图像。PDF 是一种广泛使用的文档格式,也已经扩展到存储地理空间信息。
从版本 1.7 开始的 PDF 规范包括地理空间 PDF 的扩展,它将文档的部分映射到物理空间,也称为地理配准。您可以创建点、线或多边形作为地理空间几何图形,这些几何图形也可以具有属性。
有两种方法可以对 PDF 中的地理空间信息进行编码。一家名为 TerraGo 的公司创建了一个规范,该规范已被开放地理空间联盟采纳为最佳实践,但不是标准。这种格式被称为地球测向。Adobe Systems 提出的扩展创建了名为 ISO 32000 的 PDF 规范,目前正在被纳入 2.0 版本的规范中。
TerraGo 的地理空间 PDF 产品符合 OGC 最佳实践文档和 Adobe PDF 扩展。但是 TerraGo 超越了这些特性,包括图层和其他地理信息系统功能。但是,您必须使用 TerraGo 的 Adobe Acrobat 插件或其他软件来访问该功能。至少,TerraGo 支持至少在任何 PDF 软件中显示所需的功能。
在 Python 中,有一个名为geopdf的库,它与 TerraGo 无关,但支持 OGC 最佳实践。这个库最初是由突出边缘的泰勒·加纳(https://prominentedge.com/)为 Python 2 开发的。已经移植到 Python 3 了。
从 GitHub 安装geopdf就像运行以下程序一样简单:
pip install https://github.com/GeospatialPython/geopdf-py3/archive/master.zip
以下示例将我们在第 1 章、中使用 Python 学习地理空间分析中创建的地图作为地理空间 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库结合了开放街道地图 ( 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 的介绍:https://Jupyter-notebook-初学者-guide . read the docs . io/en/latest/what _ is _ Jupyter . html。
伯爵夫人
Conda 是一个开源的包管理系统,使得安装和更新复杂的库变得更加容易。它可以与几种语言一起工作,包括 Python。Conda 对于建立库并测试它们非常有用,这样我们就可以在开发环境中尝试新事物。定制配置生产环境通常更好,但是 Conda 是一个原型化新想法的好方法。
你可以在https://conda.io/en/latest/开始康达。
摘要
在本章中,我们调查了用于地理空间分析的 Python 专用工具。这些工具中的许多都包括绑定到我们在第 3 章、地理空间技术景观中讨论的库,用于特定操作的最佳解决方案,例如 GDAL 的栅格访问功能。我们还尽可能地包含了纯 Python 库,并且在接下来的章节中,我们将继续包含纯 Python 算法。
在下一章中,我们将开始将所有这些工具应用于地理信息系统分析。
进一步阅读
以下链接将允许您进一步探讨本章中的主题。第一个链接是关于 XPath 查询语言,我们使用它来使用 Elementree 过滤 XML 元素。第二个链接是 Python 字符串库的文档,这在本书中对于操作数据至关重要。第三,我们有lxml库,一个更强大、更快速的 XML 库。最后,我们还有 Conda,它为 Python 中的科学操作提供了一个全面、易于使用的框架,包括地理空间技术:
- 有关 XPath 的更多信息,请查看以下链接:http://www.w3schools.com/xsl/xpath_intro.asp
- 有关 Python
string模块的更多详细信息,请查看以下链接:https://docs.python.org/3.4/library/string.html - 关于 LXML 的文档可以在以下链接找到:http://lxml.de/
- 您可以通过以下链接了解更多关于康达的信息:https://conda.io/en/latest/
五、Python 与地理信息系统
本章将重点介绍如何将 Python 应用于通常由地理信息系统 ( 地理信息系统)执行的功能,例如 QGIS 或 ArcGIS。这些功能是地理空间分析的核心和灵魂。我们将继续在 Python 本身之外使用尽可能少的外部依赖项,以便您拥有在不同环境中尽可能可重用的工具。在本书中,我们从编程的角度将地理信息系统分析和遥感分开,这意味着在本章中,我们将主要关注向量数据。
与本书的其他章节一样,这里介绍的项目是核心功能,作为构建模块,您可以重新组合来解决您将在本书之外遇到的挑战。本章的主题包括以下内容:
- 测量距离
- 转换坐标
- 向量数据的重新投影
- 测量区域
- 编辑形状文件
- 从更大的数据集中选择数据
- 创建专题地图
- 使用电子表格
- 非地理信息系统数据类型转换
- 地理编码
- 多重处理
本章包含许多代码示例。除了文本之外,示例中还包含代码注释作为指南。这一章比这本书的任何一章都涉及更多的内容。它涵盖了从测量地球到编辑数据和创建地图,再到使用按比例放大的多处理技术进行更快的分析的所有内容。到本章结束时,您将成为一名地理空间分析师,准备好学习本书其余部分中更先进的技术。
技术要求
对于本章,您将需要以下内容:
- Python 3.7
- Python UTM 库
- Python OGR 库
- Python 形状文件库
- Python 菲奥娜库
- Python PNGCanvas 库
- Python 枕头库(Python 图像库)
- Python 叶图书馆
- Python Pymea 库
- Python 地理编码器库
- Python 地理库
测量距离
地理空间分析的本质是发现地球上物体之间的关系。距离较近的项目往往比距离较远的项目关系更紧密。这个概念被称为托布勒地理第一定律。因此,测量距离是地理空间分析的关键功能。
正如我们所知,每张地图都是地球的模型,它们在某种程度上都是错误的。因此,坐在电脑前测量地球上两点之间的精确距离是不可能的。即使是专业的土地测量员(他们带着传统的观测设备和非常精确的全球定位系统设备在野外工作)也无法解释 A 点和 b 点之间地球表面的每一个异常。因此,为了测量距离,我们必须考虑以下问题:
- 我们在测量什么?
- 我们测量多少?
- 我们需要多少精确度?
现在,为了计算距离,我们可以使用三种地球模型:
- 平面
- 球面的
- 椭圆体
在平面模型中,使用标准欧几里得几何。地球被认为是一个没有曲率的平面,如下图所示:

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

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

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

In the following example, the double-asterisk (**) in Python is the syntax for exponents, which we'll use to square the distances.
我们将导入 Python 数学模块的平方根函数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 英里。该计算相当准确,因为该投影针对使用笛卡尔坐标测量密西西比距离和面积进行了优化。
我们也可以使用十进制度来测量距离,但是我们必须执行一些额外的步骤。要使用度数来测量,我们必须将角度转换为弧度,弧度表示坐标之间的曲面距离。我们还将以弧度为单位的输出乘以以米为单位的地球半径,从弧度转换回来。
You can read more about radians at http://en.wikipedia.org/wiki/Radian.
当我们计算 x 和 y 距离时,我们将使用下面代码中的 Python math.radians()方法执行此转换:
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 公里。所以,如你所见,你对测量算法和地球模型的选择会有重大影响。根据我们对坐标系和地球模型的选择,使用相同的方程,我们得出了完全不同的答案。
You can read more about Euclidean distance at http://mathworld.wolfram.com/Distance.html.
接下来让我们来看看哈弗辛公式。
使用哈弗辛公式
用毕达哥拉斯定理来测量地球(一个球体)上的距离的部分问题是大圆距离的概念。大圆是球面上两点之间的最短距离。定义大圆的另一个重要特征是,圆如果一直围绕球体,会将球体一分为二,形成相等的两半,如下图所示:

那么,在弯曲球体上测量直线的正确方法是什么呢?最流行的方法是使用哈弗斯公式,该公式使用三角学,使用以十进制度定义的坐标作为输入来计算大圆距离。哈弗辛公式为哈弗辛(θ) = 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 椭球上的 Vincenty 公式进行测量时,距离是多少:
- 首先,我们将导入
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 米之遥。印象深刻!虽然数学上比哈弗辛公式复杂很多倍,但你可以看到它也要精确得多。
The pure Python geopy module includes an implementation of the Vincenty formula and has the ability to geocode locations by turning place names into latitude and longitude coordinates: http://geopy.readthedocs.org/en/latest/.
这些例子中使用的点相当接近赤道。随着你向两极移动,或者在更大的距离或极小的距离下工作,你所做的选择变得越来越重要。如果你只是想在一个城市周围划出一个半径来选择宣传音乐会的营销活动的地点,那么几公里的误差可能是可以的。然而,如果你试图估算一架飞机在两个机场之间飞行所需的燃料,那么你想被当场发现!
如果您想了解更多关于测量距离和方向的问题,以及如何通过编程解决这些问题,请访问以下网站:http://www.movable-type.co.uk/scripts/latlong.html。
在这个网站上,Chris Veness 详细介绍了这个主题,并提供了在线计算器,以及用 JavaScript 编写的示例,这些示例可以很容易地移植到 Python 中。我们刚才看到的 Vincenty 公式实现是从这个网站的 JavaScript 移植过来的。
你可以在这里看到文森特公式的完整纯数学符号:https://en.wikipedia.org/wiki/Vincenty's_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是逆向工程一个直角三角形,然后算出三角形的锐角。下面的网址解释了这个公式的元素,并在最后提供了一个互动的例子:https://www.mathsisfun.com/sine-cosine-tangent.html。
我们现在知道如何计算特征在地球上的位置。接下来,我们将学习如何集成来自不同来源的数据,从坐标转换开始。
理解坐标转换
坐标转换允许您在不同坐标系之间转换点坐标。当您开始处理多个数据集时,您不可避免地会得到不同坐标系和投影中的数据。您可以使用名为utm的纯 Python 模块,在两个最常见的坐标系(UTM 和地理坐标(纬度和经度))之间来回转换。您可以使用 PyPI 的easy_install或pip:https://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 在南半球。以下截图来自网站图册欧洲植物区系,展示了欧洲上空的 UTM 区域:

从纬度和经度转换更容易。我们只是将纬度和经度传递给from_latlon()方法,该方法返回一个元组,该元组具有与to_latlon()方法接受的参数相同的参数:
import utm
utm.from_latlon(48.55199390882121, 8.725555729071763)
# (479747.04524576373, 5377691.373080335, 32, 'U')
The algorithms that were used in this Python implementation are described in detail at http://www.uwgb.edu/dutchs/UsefulData/UTMFormulas.HTM.
在 UTM 和纬度/经度之间进行转换只是触及了转换来自不同来源的数据集的表面,这样它们就可以很好地覆盖在地图上。为了超越基础,我们需要执行地图投影。
现在我们知道如何计算直线方向,让我们看看如何重新投影。
理解再投射
在地理信息系统中,重投影就是将数据集中的坐标从一个坐标系更改为另一个坐标系。虽然由于更先进的数据分发方法,重新投影现在不太常见,但有时您需要重新投影一个 shapefile。纯 Python utm模块用于参考系统转换,但是对于完整的重投影,我们需要 OGR Python API 的一些帮助。包含在osgeo模块中的 OGR 应用编程接口还提供了开放空间参考模块,也称为osr,我们将使用它进行重新投影。
作为一个例子,我们将在兰伯特共形投影中使用一个包含纽约市博物馆和画廊位置的点形状文件。我们将把它重新投影到 WGS84 地理(或者说,不投影它)。你可以在 https://git.io/vLbT4 下载这个压缩的形状文件。
下面的极简脚本重新投影了 shapefile。几何图形被转换,然后写入新文件,但是.dbf文件只是被复制到新名称,因为我们没有改变它。标准 Python shutil模块,外壳实用程序的缩写,用于复制.dbf。源和目标 shape 文件名是脚本开头的变量。目标投影也在顶部附近,这是使用 EPSG 码设置的。脚本假设有一个.prj投影文件,它定义了源投影。如果没有,您可以使用与目标投影相同的语法手动定义它。我们将逐步完成数据集的投影。每个部分都标有注释:
- 首先,我们导入我们的库:
from osgeo import ogr
from osgeo import osr
import os
import shutil
- 接下来,我们将形状文件名定义为变量:
srcName = 'NYC_MUSEUMS_LAMBERT.shp'
tgtName = 'NYC_MUSEUMS_GEO.shp'
- 现在,我们使用
osr模块作为 EPSG 代码4326创建我们的目标空间参考,它是 WGS84 地理:
tgt_spatRef = osr.SpatialReference()
tgt_spatRef.ImportFromEPSG(4326)
- 然后,我们使用
ogr设置我们的形状文件Reader对象,并获得空间参考:
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)
- 现在,我们可以开始构建 shapefile 的目标层了:
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:https://jswhit.github.io/pyproj/重新投影形状文件。
除了将坐标转换成不同的投影之外,您还经常需要在不同的格式之间转换它们,我们接下来将讨论这一点。
理解坐标格式转换
传统上,对于海上导航,地图坐标表示为度、分和秒。然而,在地理信息系统(基于计算机)中,纬度和经度被表示为十进制数,称为十进制度。仍然使用度、分钟和秒的格式。有时,您必须在该格式和十进制度之间进行转换,才能执行计算和输出报告。
在本例中,我们将创建两个函数,可以将其中一种格式转换为另一种格式:
- 首先,我们导入
math模块进行转换,re正则表达式模块解析坐标字符串:
import math
import re
- 我们的功能是将十进制度数转换成
degrees、minutes和seconds字符串:
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 中最有用的操作之一。但是在地理信息系统中,面积计算超出了基本几何。多边形位于地球上,地球是一个曲面。多边形必须被投影以说明曲率。
幸运的是,有一个简单称为area的纯 Python 模块可以为我们处理这些复杂情况。并且因为它是纯 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)
现在,您可以使用工具来计算地理空间数据的距离和大小。
在下一节中,我们将研究用最流行的地理信息系统数据格式之一——shape file 来编辑数据集。
编辑形状文件
形状文件是地理信息系统中最常见的数据格式之一,既用于交换数据,也用于执行地理信息系统分析。在本节中,我们将学习如何广泛地使用这些文件。在第 2 章、学习地理空间数据中,我们讨论了形状文件作为一种可以有许多不同文件类型与之相关联的格式。对于编辑 shapefiles 和大多数其他操作,我们只关心两种文件类型:
.shp文件.dbf文件
.shp文件包含几何图形,而.dbf文件包含相应几何图形的属性。形状文件中的每个几何记录都有一个.dbf记录。这些记录没有任何编号或标识。这意味着,在从 shapefile 中添加和删除信息时,必须小心地删除或添加记录到每个文件类型中以匹配。
正如我们在 第 4 章地理空间 Python 工具箱中所讨论的,有两个库可以用来编辑 Python 中的 shapefiles:
- 一个是 Python 到 OGR 库的绑定。
- 另一个是 PyShp 库,用纯 Python 编写。
我们将使用 PyShp,以便尽可能坚持本书的纯 Python主题。要安装 PyShp,请使用easy_install或pip。
要开始编辑形状文件,我们将从包含密西西比州城市的点形状文件开始,您可以将其作为 ZIP 文件下载。将以下文件下载到你的工作目录并解压:http://git.io/vLbU4。
我们正在处理的点可以在下图中看到:

访问 shapefile
要使用 shapefile 做任何事情,我们需要将它作为数据源进行访问。要访问 shapefile,我们将使用 PyShp 打开它。在 PyShp 中,我们将添加以下代码:
import shapefile
r = shapefile.Reader('MSCities_Geo_Pts')
r
<shapefile.Reader instance at 0x00BCB760>
我们创建了一个形状文件Reader对象实例,并将其设置为r变量。请注意,当我们将文件名传递给Reader类时,我们没有使用任何文件扩展名。请记住,我们正在处理至少两个以.shp和.dbf结尾的不同文件。因此,我们真正需要的只是这两个文件共有的不带扩展名的基本文件名。
但是,您可以使用文件扩展名。PyShp 将忽略它并使用基本文件名。那么,你为什么要增加一个扩展名呢?大多数操作系统允许文件名中有任意数量的句点。例如,您可能有一个具有以下基本名称的 shape file:myShapefile.version.1.2。
在这种情况下,PyShp 会尝试将最后一个句点之后的字符解释为文件扩展名,即.2。此问题将阻止您打开 shapefile。因此,如果您的 shapefile 在基本名称中有句点,您需要在文件名中添加一个文件扩展名,如.shp或.dbf。
一旦您打开了一个形状文件并创建了一个Reader对象,您就可以获得一些关于地理数据的信息。在下面的示例中,我们将从Reader对象中获取边界框、形状类型和形状文件中的记录数:
r.bbox
[-91.38804855553174, 30.29314882296931, -88.18631833931401,
34.96091138678437]
r.shapeType
# 1
r.numRecords
# 298
存储在r.bbox属性中的边界框作为包含最小 x 值、最小 y 值、最大 x 值和最大 y 值的列表返回。形状类型可用作shapeType属性,是由官方形状文件规范定义的数字代码。在这种情况下,1代表点形状文件,3代表线,5代表多边形。最后,numRecords属性告诉我们这个 shapefile 中有298记录。因为是简单的点形状文件,我们知道有298个点,每个点都有自己的.dbf记录。
下表显示了 shapefiles 的不同几何类型及其对应的数字代码:
| 几何图形 | 数字代码 |
| NULL | Zero |
| POINT | one |
| POLYLINE | three |
| POLYGON | five |
| MULTIPOINT | eight |
| POINTZ | Eleven |
| POLYLINEZ | Thirteen |
| POLYGONZ | Fifteen |
| MULTIPOINTZ | Eighteen |
| POINTM | Twenty-one |
| POLYLINEM | Twenty-three |
| POLYGONM | Twenty-five |
| MULTIPOINTM | Twenty-eight |
| MULTIPATCH | Thirty-one |
现在我们知道了如何访问它,让我们看看如何读取这些文件。
正在读取 shapefile 属性
.dbf文件是一种简单的数据库格式,其结构类似于一个包含行和列的电子表格,每一列都是一个标签,定义了它包含的信息。我们可以通过检查Reader对象的字段属性来查看该信息:
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属性返回了相当多的信息。这些字段包含每个字段的信息列表,称为字段描述符。对于每个字段,显示以下信息:
- 字段名:这是文本形式的字段名,对于 shapefiles,长度不能超过 10 个字符。
- 字段类型:这是字段的类型,可以是文本、数字、日期、浮点数,也可以是分别表示为 C、N、D、F、L 的布尔值。shapefile 规范称其使用指定为 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,因为这是该形状文件的 2010 版本,它是作为每次人口普查的一部分创建的。由于字段名的10字符限制,这类缩写在 shapefile .dbf文件中很常见。
接下来,让我们检查这些字段描述的一些记录。我们可以使用r.record()方法查看单个记录。从第一个例子我们知道有298记录。因此,让我们以第三条记录为例进行研究。使用列表索引访问记录。在 Python 中,索引从0开始,所以我们必须从期望的记录数中减去一才能得到索引。对于记录 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记录。在本例中,我们将遍历records()方法返回的列表,但是将使用 Python 数组切片的结果限制在前三条记录中。正如我们前面提到的,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']
这种枚举技巧是大多数地理信息系统软件包在表中显示记录时使用的。许多地理信息系统分析师认为形状文件存储记录号,因为每个地理信息系统程序显示一个。但是如果您删除一条记录,例如 ArcGIS 或 QGIS 中的记录编号 5,并保存该文件,当您再次打开它时,您会发现以前的记录编号 6 现在是记录编号 5。一些空间数据库可能会为记录分配唯一的标识符。通常,唯一的标识符是有帮助的。您始终可以在.dbf中创建另一个字段和列,并分配自己的编号,即使删除记录,该编号也保持不变。
如果您正在处理非常大的形状文件,PyShp 有迭代器方法,可以更有效地访问数据。默认的records()方法一次将所有记录读入内存,这对小的.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 对,因此如果使用该坐标系,经度优先于纬度。
形状文件规范也允许三维形状。高程值沿 z 轴,通常称为 z 值。因此,3D 点通常被描述为 x 、 y 、 z 。在 shapefile 格式中, z 值存储在单独的 z 属性中,如果形状类型允许的话。如果形状类型不允许 z 值,那么当 PyShp 读取记录时,该属性永远不会被设置。带有 z 值的 Shapefiles 也包含测量值或 m 值,这些值很少使用,在本例中也不使用。
度量是用户分配的值,可以与形状相关联。一个例子是在给定位置记录的温度。还有另一类形状类型允许为每个形状添加 m 值,但不允许添加 z 值。这类形状类型称为 M 形状类型。就像 z 值一样,如果有数据,则创建 m 属性;否则,就不是了。您通常不会遇到带有 z 值的形状文件,也很少遇到设置了 m 值的形状文件。但有时你会,所以意识到它们是很好的。就像我们的字段和记录.dbf示例一样,如果您不喜欢将 z 和 m 值存储在单独的列表中,从点列表中,您可以使用zip()方法将它们组合起来。zip方法可以将多个列表作为用逗号分隔的参数,就像我们之前遍历记录并连接字段名称和属性时所演示的那样。
用 PyShp 创建Reader对象时,它是只读的。您可以更改Reader对象中的任何值,但它们不会写入原始形状文件。在下一小节中,我们将看到如何在原始 shapefile 中进行更改。
更改形状文件
要创建一个 shapefile,还需要创建一个Writer对象。您可以在Reader或Writer对象中更改值;它们只是动态的 Python 数据类型。但在某些时候,你必须将数值从Reader复制到Writer。PyShp 自动处理所有的标题信息,例如边界框和记录计数。你只需要担心几何和属性。你会发现这个方法比我们之前使用的 OGR 例子简单得多。然而,这也仅限于 UTM 的预测。
为了演示这个概念,我们将读入一个包含以度数为单位的点的形状文件,并在保存它之前将其转换为Writer对象中的 UTM 参考系统。我们将使用我们在本章前面讨论过的 PyShp 和 UTM 模块。我们将使用的 shapefile 是纽约市博物馆 shapefile,我们将其重新投影到 WGS84 地理。您也可以将其作为 ZIP 文件下载,该文件可在https://git.io/vLd8Y获得。
在下面的示例中,我们将读入 shapefile,为转换后的 shapefile 创建一个编写器,复制字段,然后复制记录,最后在保存转换后的 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')
如果要打印出第一个形状的第一个点,您会看到以下内容:
print(w.shapes()[0].points[0])
# [4506346.393408813, 583315.4566450359, 0, 0]
该点作为包含四个数字的列表返回。前两个是 x 和 y 值,而后两个是占位符,在本例中分别是高程值和测量值,它们在您编写这些类型的形状文件时使用。此外,我们没有像前面的重投影示例那样编写 PRJ 投影文件。这里有一个简单的方法,使用来自 https://spatialreference.org/的 EPSG 代码创建一个 PRJ 文件。上例中的zone变量告诉我们,我们在 UTM 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 添加一个新特性。在本例中,我们将向代表热带风暴的形状文件中添加第二个多边形。你可以在这里下载这个例子的压缩形状文件:https://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 中添加新字段。
添加字段
对 shapefiles 的一个非常常见的操作是向它们添加额外的字段。这个操作很简单,但是有一个重要的元素需要记住。添加字段时,还必须遍历记录,并为该列创建一个空单元格或添加一个值。例如,让我们在 UTM 版本的纽约市博物馆形状文件中添加一个参考纬度和经度列:
- 首先,我们将打开 shapefile 并创建一个新的
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)
- 接下来,我们将打开 shapefile 的地理版本,并从每个记录中获取坐标。我们将把这些添加到 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)
在下一小节中,我们将看到如何合并多个 shapefiles。
合并形状文件
将多个相同类型的相关 shapefile 聚合成一个更大的 shape file 是另一种非常有用的技术。你可能是一个团队的一员,这个团队划分了一个感兴趣的领域,然后在一天结束时收集数据。或者,您可以从现场的一系列传感器(如气象站)中收集数据。
在本例中,我们将使用一组用于一个县的建筑足迹,这些足迹分别在四个不同的象限(西北、东北、西南和东南)中维护。你可以在http://git.io/vLbUE下载这些形状文件作为一个单独的 ZIP 文件。
当您解压缩这些文件时,您会看到它们是按象限命名的。以下脚本使用 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)
如您所见,合并一组 shapefiles 非常简单。但是,我们没有进行任何健全性检查来确保 shapefiles 都是相同的类型,如果这个脚本用于重复的自动化过程,而不是一个快速的一次性过程,您可能会想要这样做。
关于这个例子的另一个注意事项是我们如何调用Writer对象。在其他示例中,我们使用数字代码来定义形状类型。您可以直接定义该数字(例如,指针形状文件为 1),或者调用 PyShp 常量之一。常量是所有大写字母的 shapefile 类型。例如,多边形如下所示:
shapefile.POLYGON
在这种情况下,该常数的值为 5。将数据从Reader复制到Writer对象时,您会注意到形状类型定义被简单引用,如本例所示:
r = shapefile.Reader('myShape')
w = shapefile.Writer("myShape", r.shapeType)
此方法使您的脚本更加健壮,因为如果您以后更改脚本或数据集,脚本中需要更改的变量就少了一个。在合并示例中,当我们调用Writer时,我们没有Reader对象可用的好处。
我们可以打开列表中的第一个 shapefile 并检查它的类型,但是这将增加几行代码。更简单的方法是省略形状类型。如果Writer形状类型没有保存,PyShp 将忽略它,直到您保存形状文件。届时,它将检查几何记录的单个标题,并根据该标题进行确定。
虽然您可以在特殊情况下使用此方法,但为了清楚起见,最好尽可能明确地定义形状类型,以防出现任何异常情况错误。下图是这个数据集的一个示例,以便您更好地了解数据的样子,因为我们接下来会更多地使用它:

现在,让我们看看如何使用.dbfpy文件来实现这一点。
将 shapefiles 与 dbfpy 合并
PyShp 的.dbf部分偶尔会遇到由某些软件生成的.dbf文件的问题。幸运的是,PyShp 允许您分别操作不同的 shapefile 类型。还有一个更强大的.dbf库,名为dbfpy3,我们在第 4 章地理空间 Python 工具箱中讨论过。您可以使用 PyShp 处理.shp和.shx文件,而.dbfpy处理更复杂的.dbf文件。可以在这里下载模块:https://github . com/GeospatialPython/dbfp 3/archive/master . zip。
这种方法需要更多的代码,但是当 PyShp 单独处理.dbf问题失败时,它通常会成功。本示例使用与上一示例相同的 shapefiles。在下面的示例中,我们将仅使用 shapefile 的属性来合并它:
- 首先,我们导入我们需要的库,使用 glob 模块获得一个 shapefile 列表,并使用 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文件,并将几何图形复制到编写器中。稍后,我们将使用dbypy3模块返回并获取属性,以演示如何单独使用 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())))
- 一旦所有几何图形被复制到写入器,我们可以保存
.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,以使您更容易关注感兴趣的子集。这种拆分或子集化可以在空间上完成,也可以通过属性来完成,具体取决于数据的哪个方面是感兴趣的。
空间子集化
提取数据集一部分的一种方法是使用空间属性,如大小。在下面的示例中,我们将对合并的东南象限文件进行子集化。我们将按面积过滤建筑足迹多边形,并将任何 100 平方米或更小(约 1000 平方英尺)的建筑轮廓导出到新的形状文件中。我们将为此使用footpints_se形状文件。
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 之间记录数量的差异:
r = shapefile.Reader('footprints\_se')
subset = shapefile.Reader('footprints\_185')
print(r.numRecords)
# 26447
print(subset.numRecords)
# 13331
我们现在有了一些用于向量数据和属性的地理空间分析的基本构件。
执行选择
前面的子集设置示例是选择数据的一种方式。还有许多其他方法可以对数据进行子集化,以便进一步分析。在本节中,我们将研究如何选择对高效数据处理至关重要的数据子集,以将大数据集的大小缩小到我们对给定数据集感兴趣的区域。
多边形中点公式
我们在第 1 章中简要讨论了多边形中点公式,这是一种常见的地理空间操作。你会发现这是最有用的公式之一。公式相对简单。
以下功能使用光线投射方法执行该检查。该方法从测试点一直画一条线穿过多边形,并计算它穿过多边形边界的次数。如果计数为偶数,则该点在多边形之外。如果很奇怪,那它就在里面。这个特殊的实现还检查点是否在多边形的边缘:
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 中。在这个例子中,我们将把波多黎各岛上的道路从美国大陆的主要道路形状文件中划分出来。可以在这里下载 shape file:https://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')
既然我们已经使用几何来选择要素,那么让我们使用属性以另一种方式来选择。
属性选择
我们现在已经看到了两种不同的方法来对较大的数据集进行子集化,从而根据空间关系生成较小的数据集。但是我们也可以使用属性字段来选择数据。因此,让我们研究一种使用属性表对向量数据进行子集化的快速方法。在这个例子中,我们将使用密西西比州人口密集的城市地区的多边形形状文件。你可以从 http://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')
属性选择通常很快。由于浮点计算,空间选择的计算成本很高。只要有可能,请确保您不能先使用属性选择来子集化。下图显示了开始的 shapefile,它包含左侧有州边界的所有城市地区,以及右侧少于 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)
现在,我们知道如何组合离散数据集,以及如何将较大的数据集分开。我们还能做什么?我们可以在数据集中聚合要素。
聚合几何
地理信息系统向量数据集通常由点、线或面要素组成。地理信息系统的原则之一是地理上靠得更近的东西比离得更远的东西更相关。当你有一组相关的特性时,对于你试图完成的分析来说,细节太多了。对它们进行归纳有助于加快处理速度或简化地图。这种操作称为聚合。聚合的一个常见示例是将一组本地政治边界合并为一个更大的政治边界,例如将一个县合并为一个州,将一个州合并为一个国家,或将多个国家合并为大陆。
在这个例子中,我们将这样做。我们将把包含美国密西西比州所有县的数据集转换成代表整个州的单个多边形。Python Shapely 库非常适合这种操作;但是,它只能操作几何图形,不能读取或写入数据文件。为了读写数据文件,我们将使用 Fiona 库。如果您没有安装 Shapely 或 Fiona,请使用pip安装它们。您可以在此下载各县数据集:https://git.io/fjt3b。
下图显示了县数据集的外观:

以下步骤将向您展示如何将单个县多边形合并为单个多边形:
- 在下面的代码中,我们导入我们需要的库,包括
shapely库的不同部分。 - 然后,我们将打开县 GeoJSON 文件。
- 接下来,我们将复制源文件的模式,它定义了数据集的所有元数据。
- 然后,我们需要更改元数据副本来更改属性,以便为状态名定义一个属性。我们还需要将几何类型从多多边形更改为多边形。
- 然后,我们将打开名为
combined.geojson的输出数据集 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})
输出数据集看起来类似于下图:

既然我们已经了解了阅读、编辑和编写地理信息系统数据的所有知识,我们可以在接下来的章节中开始可视化它。
创建可视化图像
现在,我们正从计算和数据编辑转向我们能看到的东西!我们将从创建不同类型的地图开始。在第 1 章中,我们学习了 Python 的地理空间分析,我们使用 Python 附带的 Tkinter 模块可视化了我们的 SimpleGIS 程序。在第 4 章、地理空间 Python 工具箱中,我们研究了其他一些创建图像的方法。现在,我们将通过创建两种特定类型的专题地图来更深入地研究这些工具。第一个是点密度图,第二个是点密度图。
首先,让我们从点密度图开始。
点密度计算
点密度图显示给定区域内受试者的浓度。如果一个区域被划分为包含统计信息的多边形,则可以使用该区域内随机分布的点,并在整个数据集内使用固定的比率来建模该信息。这种类型的地图通常用于人口密度地图。
第 1 章用 Python 学习地理空间分析中的猫图,是点密度图。让我们使用纯 Python 从头开始创建一个点密度图。纯 Python 允许您使用更轻量级的库,这些库通常更容易安装并且更具可移植性。在这个例子中,我们将使用美国人口普查局沿着美国墨西哥湾海岸的一个包含人口数据的区域形状文件。我们还将使用多边形中点算法来确保随机分布的点在适当的普查区域内。最后,我们将使用PNGCanvas模块写出我们的图像。
PNGCanvas模块优秀快速。然而,它没有能力填充简单矩形以外的多边形。您可以实现填充算法,但是在纯 Python 中它非常慢。然而,对于快速的大纲和点的情节,它做得很好。
您还将看到world2screen()方法,该方法类似于我们在第 1 章中使用的坐标到映射算法,了解使用 Python 进行地理空间分析。在本例中,我们将读入一个 shapefile,并将其作为图像写回来:
- 首先,我们导入我们需要的库,包括
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)
- 接下来,我们读入 shapefile 并设置输出地图图像的大小:
# 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())
该脚本输出了人口普查区域的轮廓以及密度点,以非常有效地显示人口集中度:

现在,让我们来看看第二种类型的地图:choropleth 地图。
合唱团地图
图表使用阴影、颜色或符号来显示一个区域内的平均值或数量。它们使我们能够轻松地将大量数据可视化为摘要。如果相关数据跨越多个多边形,此方法非常有用。例如,在按国家划分的全球人口密度地图中,许多国家都有断开的多边形(例如,夏威夷是美国的一个岛国)。
在本例中,我们将使用我们在第 3 章、地理空间技术领域中讨论的 Python 图像库 ( PIL )。PIL 不是纯粹的 Python,而是专门为 Python 设计的。我们将把前面的点密度示例重新创建为一个 choropleth 图。我们将根据每平方公里的人口数量计算每个普查区域的密度比率,并使用该值来调整颜色。深色更密集,而浅色更少。请遵循以下步骤:
- 首先,我们将导入我们的库:
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)
- 现在,我们打开 shapefile 并设置输出图像大小:
# 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 变量来调整颜色:

现在我们可以显示 shape file 中的统计数据,我们可以看看比 shape file 更常见的统计数据源:电子表格。
使用电子表格
诸如微软办公 Excel 和 Open Office Calc 等电子表格价格低廉(甚至免费),无处不在,易于使用,非常适合记录结构化数据。由于这些原因,电子表格被广泛用于收集数据以输入地理信息系统格式。作为一名分析师,你会发现自己经常使用电子表格。
在前面的章节中,我们讨论了 CSV 格式,它是一个文本文件,具有与电子表格相同的基本行和列数据结构。对于 CSV 文件,可以使用 Python 内置的csv模块。但大多数情况下,人们不会费心将真正的电子表格导出到通用的 CSV 文件中。这就是纯 Python xlrd模块发挥作用的地方。xlrd是 Excel Reader 的缩写,可从 PyPI 获得。还有一个附带的模块,xlwt (Excel Writer)模块,用于编写电子表格。这两个模块使阅读和编写 Excel 电子表格变得轻而易举。将其与 PyShp 结合,您可以轻松地在电子表格和形状文件之间来回移动。此示例演示如何将电子表格转换为 shapefile。我们将使用 https://git.io/Jemi9 的纽约市博物馆点数据的电子表格版本。
电子表格包含属性数据,后面是带有经度的 x 列和带有纬度的 y 列。要将其导出到 shapefile,我们将执行以下步骤:
- 打开电子表格。
- 创建一个形状文件
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')
将 shapefile 转换成电子表格是一个不太常见的操作,虽然并不难。要将形状文件转换为电子表格,您需要使用本章编辑形状文件部分中的添加字段示例来确保您有一个 x 和 y 列。您可以循环遍历这些形状,并将 x 、 y 值添加到这些列中。然后,您可以使用csv模块将字段名称和列值从dbf读入xlwt电子表格对象或 CSV 文件。坐标列在下面的截图中标记:

在下一节中,我们将使用电子表格作为输入数据源。
创建热图
热图用于使用显示密度的栅格图像来显示数据的地理聚类。还可以通过使用数据中的字段来加权聚类,不仅显示地理密度,还显示强度因子。在本例中,我们将使用 CSV 数据集中包含的熊目击数据,该数据将数据存储为点,以创建密西西比州不同地区熊目击频率的热图。这个数据集非常简单,所以我们将把 CSV 文件视为文本文件,这是 CSV 文件的一个很好的特性。
你可以在这里下载数据集:https://git.io/fjtGL。
输出将是一个简单的 HTML 网络地图,可以在任何网络浏览器中打开。网络地图将基于优秀的传单 JavaScript 库。除此之外,我们将使用 Python leaf 库,它使我们能够轻松创建 leaf 网络地图,以便生成 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的文件。在任何网络浏览器中打开它以查看类似的图像:

接下来,我们将学习如何使用全球定位系统生成的数据来收集现场数据,例如前面热图中的信息。
使用全球定位系统数据
目前最常见的全球定位系统数据类型是加尔明 GPX 格式。我们在第 4 章、地理空间 Python 工具箱中介绍了这种 XML 格式,它已经成为非官方的行业标准。因为它是一种 XML 格式,所以所有有据可查的 XML 规则都适用于它。然而,还有另一种早于 XML 和 GPX 的 GPS 数据,叫做国家海洋电子协会 ( NMEA )。这些数据是为流式传输而设计的 ASCII 文本句子。
你偶尔会遇到这种格式,因为即使它是古老而深奥的,它仍然非常活跃,特别是通过自动识别系统 ( 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模块并下载完整的示例文件,您可以查看以下网址:http://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 秒。你可以在http://aprs.gids.nl/nmea/找到更多关于 NMEA 格式的信息。
全球定位系统数据是一个重要的位置数据源,但是我们还有另一种方法可以使用街道地址来描述地球上的一个点。在地球上定位街道地址的方法称为地理编码。
地理编码
地理编码是将街道地址转换为纬度和经度的过程。该操作对于车载导航系统和在线驾驶方向网站至关重要。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。其中一些服务需要应用编程接口密钥,并且可能有请求限制。
- 现在,我们来看看
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 内置的多处理模块可以在您的计算机上产生多个进程,以利用所有可用的处理器。
与多处理模块配合使用的一项操作是地理编码。在这个例子中,我们将对一个城市列表进行地理编码,并在您的机器上的所有处理器上分割处理。我们将使用与以前相同的地理编码技术,但这一次,我们将添加多处理模块,以提高速度和可扩展性的潜力。以下代码将跨多个处理器同时对城市列表进行地理编码:
- 首先,我们导入我们需要的模块:
# 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))]
这项技术可能非常强大,但不是每种类型的处理都可以这样执行。您使用的处理类型必须支持可以分解为离散计算的操作。但是当你能把问题分解开来,就像我们在这个例子中做的那样,结果会快几个数量级。
摘要
本章涵盖了地理信息系统分析的关键组成部分。我们研究了使用不同方法在地球曲面上进行测量的挑战。我们研究了使用 OGR 和菲奥娜的坐标转换和完全重投影的基础,前者是带有 PyShp 的utm模块,后者简化了 OGR。我们编辑了形状文件,并执行了空间和属性选择。我们仅使用 Python 从头开始创建专题地图。我们还从电子表格中导入数据。然后,我们解析来自 NMEA 数据流的全球定位系统数据。最后,我们使用地理编码将街道地址转换为位置并返回。
作为一名地理空间分析师,您可能对地理信息系统和遥感都很熟悉,但大多数分析师都擅长某个领域。这就是为什么这本书在单独的章节中探讨这些领域——这样我们就可以关注它们的不同之处。正如我们在介绍中提到的,本章中的技术是所有地理空间分析的构建模块,并将为您提供所需的工具,以便您可以了解该领域的任何方面。
在第六章Python 和遥感中,我们将讨论遥感。在地理信息系统中,我们已经能够使用纯 Python 模块来探索这个领域。在遥感领域,由于数据的巨大规模和复杂性,我们将变得更加依赖于对用 C 语言编写的编译模块的绑定。
六、Python 与遥感
在这一章中,我们将讨论遥感。遥感是收集关于地球的信息,而不与地球发生物理接触。通常,这意味着必须使用卫星或航空图像、光探测和测距 ( 激光雷达),它测量从飞机到地球的激光脉冲,或者合成孔径雷达。遥感还可以指处理已经收集的数据,这就是我们在本章中使用这个术语的方式。随着越来越多的卫星发射和数据分发变得更加容易,遥感每天都在以更令人兴奋的方式发展。卫星和航空图像的高可用性,以及每年发射的令人感兴趣的新型传感器,正在改变遥感在了解我们的世界中发挥的作用。
在遥感中,我们遍历图像中的每个像素,并执行某种形式的查询或数学处理。图像可以被认为是一个大的 NumPy 数组。在遥感中,这些数组可能相当大,大小在几十兆字节到几千兆字节的数量级。虽然 Python 速度很快,但只有基于 C 的库才能提供以可容忍的速度遍历数组所需的速度。
我们将使用 Python 图像库 ( PIL )进行图像处理和 NumPy,后者提供多维数组数学。虽然用 C 语言编写是为了提高速度,但这些库是为 Python 设计的,并提供了一个 Python 应用编程接口。
在本章中,我们将涵盖以下主题:
- 交换图像波段
- 创建图像直方图
- 执行直方图拉伸
- 裁剪和分类图像
- 从图像中提取特征
- 变化检测
首先,我们将从基本的图像操作开始,然后在每个练习的基础上进行构建,一直到自动变化检测。这些技术将补充前面的章节,为我们的工具箱增加处理卫星数据和其他遥感产品的能力。
技术要求
- Python 3.6 或更高版本
- 内存:最小 6 GB (Windows),8 GB (macOS),推荐 8 GB
- 存储:最低 7200 转/分的 SATA,可用空间为 20gb;推荐的具有 40 GB 可用空间的固态硬盘
- 处理器:最低英特尔酷睿 i3 2.5 GHz 推荐的英特尔酷睿 i5
交换图像波段
我们的眼睛只能看到可见光谱中的颜色,如红、绿、蓝 ( RGB )的组合。空气和太空传感器可以收集可见光谱之外的能量波长。为了查看这些数据,我们将代表不同波长光反射率的图像移入和移出 RGB 通道,以生成彩色图像。
这些图像经常以奇怪和陌生的颜色组合结束,这使得视觉分析变得困难。典型卫星图像的一个例子显示在以下位于墨西哥湾沿岸密西西比州的美国宇航局斯坦尼斯航天中心附近的陆地卫星 7 号卫星场景中,该中心是遥感和地理空间分析的主要中心:

大部分植被呈现红色,水几乎呈现黑色。该图像是一种假彩色图像,这意味着图像的颜色不是基于 RGB 光的。然而,我们可以改变波段的顺序或交换某些波段,以创建另一种类型的假彩色图像,看起来更像我们习惯看到的世界。为此,您首先需要从这里以 ZIP 文件的形式下载此图像:https://git.io/vqs41。
我们在第 4 章、地理空间 Python 工具箱的安装 GDAL 和 NumPy 部分安装了带有 Python 绑定的 GDAL 库。GDAL 库包括一个名为gdal_array的模块,该模块将遥感图像加载到 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 指定另一个图像,从中复制空间参考信息和一些其他图像参数。如果没有这个论点,我们最终会得到一幅没有地理参考信息的图像,而这些信息是无法在地理信息系统中使用的。在这种情况下,我们指定我们的输入图像文件名,因为图像是相同的,除了波段顺序。在这个方法中,您可以看出 Python GDAL API 是一个 C 库的包装器,并不像 Python 设计的库那样是 Python 化的。例如,一个纯 Python 库会将SaveArray()方法写成save_array(),以遵循 Python 标准。
这个例子的结果产生了swap.tif图像,这是一个有绿色植物和蓝色水的视觉上更吸引人的图像:

这张图片只有一个问题:它有点暗,很难看到。让我们看看在下一节中是否能找出原因。
创建直方图
直方图显示数据集中数据分布的统计频率。在遥感的情况下,数据集是一幅图像。数据分布是在 0 到 255 范围内的像素频率,这是用于在计算机上存储图像信息的 8 字节数字的范围。
在 RGB 图像中,颜色被表示为 3 位元组,其中 (0,0,0,0,0) 为黑色, (255,255,255) 为白色。我们可以用 y 轴上每个值的频率和 x 轴上 256 个可能像素值的范围来绘制图像的直方图。
还记得在第 1 章、学习使用 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,我们可以在stretched.tif上运行turtle图形直方图脚本:
im = "stretched.tif"
运行前面的代码会得到下面的直方图:

如你所见,这三个波段现在分布均匀。它们彼此之间的相对分布是相同的,但是,在图像中,它们现在遍布整个光谱。
现在,我们可以更改图像以获得更好的显示效果,让我们看看如何对它们进行剪裁,以检查特定的感兴趣区域。
剪切图像
分析师很少对整个卫星场景感兴趣,因为它可以轻松覆盖数百平方英里。鉴于卫星数据的规模,我们非常有动力将图像缩小到我们感兴趣的区域。实现这一缩减的最佳方法是将图像裁剪到定义我们研究区域的边界。我们可以使用 shapefiles(或其他向量数据)作为我们的边界定义,并基本上消除该边界之外的所有数据。
以下图像包含我们的stretched.tif图像,上面分层有一个县边界文件,在量子地理信息系统 ( 量子地理信息系统)中可视化:

要剪切图像,我们需要遵循以下步骤:
- 使用
gdal_array将图像加载到数组中。 - 使用 PyShp 创建一个 shapefile 读取器。
- 将形状文件光栅化为地理参考图像(将其从向量转换为光栅)。
- 将 shapefile 图像转换为二进制掩码或过滤器,以便只抓取 shapefile 边界内我们想要的图像像素。
- 透过遮罩过滤卫星图像。
- 丢弃遮罩外的卫星图像数据。
- 将截取的卫星图像保存为
clip.tif。
我们在第四章、地理空间 Python 工具箱中安装了 PyShp,所以你应该已经从 PyPi 安装了。我们还将在这个脚本中添加一些有用的新实用函数。第一个是world2pixel(),用 GDAL GeoTransform 对象为我们做世界坐标到图像坐标的转换。
It's still the same process we've used throughout this book, but it's integrated better with GDAL.
我们还添加了imageToArray()函数,它将 PIL 图像转换成 NumPy 数组。县界形状文件是我们在前面章节中使用的hancock.shp边界,但是如果您需要,也可以在这里下载:http://git.io/vqsRH。
我们使用 PIL,因为这是最简单的方式光栅化我们的形状文件作为一个掩模图像,以过滤出超出形状文件边界的像素。让我们开始吧:
- 首先,我们将加载我们需要的库:
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 的裁剪过程中使用它:
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)
- 现在,我们可以将源图像加载到一个
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]
- 接下来,我们将为输出图像创建新的 geomatrix 数据:
# 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值对于没有完全填充图像的数据是常见的。
现在,您已经完成了全球地理空间分析师每天使用的整个工作流程,以准备用于地理信息系统的多光谱卫星和航空图像。在下一节中,我们将研究如何将图像作为信息进行分析。
分类图像
自动遥感 ( ARS )在可见光谱中很少进行。ARS 在没有任何人工输入的情况下处理图像。可见光谱之外最常见的波长是红外和近红外。
下图是从路易斯安那州新奥尔良到阿拉巴马州莫比尔的美国海湾海岸最近的陆地卫星 8 号天桥的热图像(波段 10)。图像中的主要自然特征已被标记,这样您就可以确定自己的方向:

因为图像中的每个像素都有一个反射值,所以它是信息,而不仅仅是颜色。反射率的类型可以明确地告诉我们一个特征是什么,而不是我们通过看它来猜测。Python 可以看到这些值,并通过对相关像素值进行分组,以我们直观的方式挑选特征。我们可以根据像素之间的关系为像素着色,以简化图像和视图相关的特征。这种技术叫做分类。
分类可以是相当简单的分组,仅基于从直方图中导出的一些值分布算法,也可以是涉及训练数据集甚至计算机学习和人工智能的复杂方法。最简单的形式称为无监督分类,其中除了图像本身之外,没有给出额外的输入。涉及某种训练数据来指导计算机的方法称为监督分类。应该注意的是,分类技术被用于许多领域,从医生在患者身体扫描中搜索癌细胞,到赌场在安全视频中使用面部识别软件自动识别 21 点赌桌上已知的“T4”骗子。
为了介绍遥感分类,我们将只使用直方图来分组具有相似颜色和强度的像素,并看看我们得到了什么。首先,您需要从这里下载 Landsat 8 场景:http://git.io/vByJu。
我们将使用 NumPy 附带的版本来代替前面示例中的histogram()函数,该版本允许您轻松指定仓位数量,并返回两个带有频率的数组,以及仓位值的范围。我们将使用带有范围的第二个数组作为图像的类定义。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 格式:

我们在将它保存为图像时没有指定原型参数,因此它没有地理参考信息,尽管我们可以很容易地将输出保存为地理识别。
这个结果对于一个非常简单的无监督分类来说还不错。岛屿和沿海的平地呈现出不同的绿色。云层被隔离成橙色和深蓝色的阴影。我们在内陆确实有一些困惑,那里的地貌颜色和墨西哥湾一样。我们可以通过手动定义类范围来进一步细化这个过程,而不仅仅是使用直方图。
现在,我们已经能够分离图像中的要素,我们可以尝试将要素提取为向量数据,以包含在地理信息系统中。
从图像中提取特征
对图像进行分类的能力将我们引向另一种遥感能力。既然你已经在过去的几章中使用了 shapefiles,你有没有想过它们是从哪里来的?形状文件等向量地理信息系统数据通常是从遥感图像中提取的,例如我们到目前为止看到的例子。
提取通常包括分析师点击图像中的每个对象,并绘制特征以将其保存为数据。但是有了良好的遥感数据和适当的预处理,就有可能从图像中自动提取特征。
对于这个例子,我们将从陆地卫星 8 号的热图像中提取一个子集,以隔离墨西哥湾的一组屏障岛屿。由于沙子很热,岛屿呈现白色,较冷的水呈现黑色(您可以在此下载此图片:http://git.io/vqarj):

这个例子的目标是自动提取图像中的三个岛屿作为一个 shapefile。但在此之前,我们需要屏蔽掉任何我们不感兴趣的数据。例如,水的像素值范围很广,岛屿本身也是如此。如果我们只想提取岛屿本身,我们需要将所有像素值推入两个面元中,以使图像黑白化。这种技术被称为阈值化。图像中的岛屿与背景中的水有足够的对比度,阈值应该很好地隔离它们。
在下面的脚本中,我们将把图像读入一个数组,然后只使用两个面元对图像进行直方图。然后,我们将使用黑色和白色给两个箱子上色。这个脚本只是我们分类脚本的修改版本,输出非常有限。让我们看看以下步骤:
- 首先,我们导入我们需要的一个库:
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)
输出看起来很棒,如下图所示:

这些岛屿显然是孤立的,所以我们的提取脚本将能够识别它们为多边形,并将它们保存到一个 shapefile 中。GDAL 库有一个名为Polygonize()的方法,它就是这么做的。它将图像中的所有孤立像素集合分组,并将其保存为要素数据集。我们将在这个脚本中使用的一个有趣的技术是使用我们的输入图像作为遮罩。
Polygonize()方法允许您指定一个遮罩,该遮罩将使用黑色作为过滤器,以防止水被提取为多边形,我们最终只会得到岛屿。脚本中需要注意的另一个方面是,我们将源图像中的地理参考信息复制到 shapefile 中,以便对其进行正确的地理定位。让我们看看以下步骤:
- 首先,我们导入我们的库:
import gdal
from gdal import ogr, osr
- 接下来,我们设置输入和输出图像和 shapefile 变量:
# 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
- 现在,我们准备好设置我们的 shapefile:
# Set up the output shapefile
driver = ogr.GetDriverByName("ESRI Shapefile")
shp = driver.CreateDataSource(tgt)
- 然后,我们需要将我们的空间参考信息从源图像复制到 shapefile,以在地球上定位它:
# Copy the spatial reference
srs = osr.SpatialReference()
srs.ImportFromWkt(srcDS.GetProjectionRef())
layer = shp.CreateLayer(tgtLayer, srs=srs)
- 现在,我们可以设置我们的 shapefile 属性:
# 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)
输出的 shapefile 简称为extract.shp。您可能还记得第 4 章、地理空间 Python 工具箱中,我们使用 PyShp 和 PNG Canvas 创建了一个快速的纯 Python 脚本来可视化形状文件。我们将把这个脚本带回这里,这样我们就可以查看我们的 shapefile 了,但是我们会给它添加一些额外的东西。最大的岛屿有一个小泻湖,在多边形中显示为一个洞。为了正确地渲染它,我们必须处理 shapefile 记录中的部分。
使用该脚本的前一个示例没有做到这一点,因此我们将在以下步骤中循环访问 shapefile 特性时添加该部分:
- 首先,我们需要导入我们需要的库:
import shapefile
import pngcanvas
- 接下来,我们从 shapefile 中获取空间信息,这将允许我们将坐标映射到像素:
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 = []
- 然后,我们将遍历 shapefile 并收集我们的多边形:
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 脚本和一些开源包能走多远仍然很有趣,也很有力量。在很多情况下,你可以做任何你需要做的事情。
最西边的岛包含多边形洞,如下图所示,并在该区域放大:

If you want to see what would happen if we didn't deal with the polygon holes, then just run the version of the script from Chapter 4, Geospatial Python Toolbox, on this same shapefile to compare the difference. The lagoon is not easy to see, but you will find it if you use the other script.
自动特征提取是地理空间分析中的一个圣杯,因为手动提取特征需要成本和繁琐的工作。特征提取的关键是正确的图像分类。自动特征提取适用于水体、岛屿、道路、农田、建筑物和其他往往与其背景具有高对比度像素值的特征。
现在,您已经很好地掌握了使用 GDAL、NumPy 和 PIL 来处理遥感数据。是时候进入我们最复杂的例子了:变化检测。
理解变化检测
变化检测是从两个不同的日期拍摄完全相同区域的两幅地理配准图像并自动识别差异的过程。它实际上只是图像分类的另一种形式。就像我们之前的分类例子一样,它可以从这里使用的琐碎技术,到提供惊人精确结果的高度复杂的算法。
在这个例子中,我们将使用来自沿海地区的两幅图像。这些图像显示了一场大飓风前后的人口密集区域,因此存在显著差异,其中许多很容易在视觉上发现,这使得这些样本有利于学习变化检测。我们的技术是简单地从第二幅图像中减去第一幅图像,使用 NumPy 得到一个简单的图像差。这是一种有效且常用的技术。
优点是全面,非常可靠。这种过于简单的算法的缺点是它不能隔离变化的类型。许多变化对于分析来说是微不足道的,比如海洋上的波浪。在这个例子中,我们将相当有效地掩盖水,以避免分散注意力,并且只关注差图像直方图右侧的较高反射率值。
You can download the baseline image from http://git.io/vqa6h.
You can download the changed image from http://git.io/vqaic.
Note these images are quite large – 24 MB and 64 MB, respectively!
基线图像是全色的,而改变的图像是假彩色的。全色图像是由捕获所有可见光的传感器创建的,并且是典型的高分辨率传感器,而不是捕获包含受限波长的波段的多光谱传感器。
通常情况下,您会使用两个相同的波段组合,但这些样本将适用于我们的目的。我们可以用来评估变化检测的视觉标记包括图像东南象限中的一座桥,该桥从半岛延伸到图像边缘。这座桥在之前的图像中清晰可见,被飓风夷为平地。另一个标记是西北象限中的一艘船,它在后面的图像中显示为白色踪迹,但不在前面的图像中。
一个中性标志是水和国家公路,它穿过城镇并连接到桥梁。这个特征很容易看得见,在两幅图像之间没有明显的变化。以下是基线图像的屏幕截图:

要近距离查看这些图像,您应该使用 QGIS 或 OpenEV (FWTools),如 第 3 章地理空间技术景观中量子 GIS 和 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 中的“之前”或“之后”图像上,并将黑色设置为透明,如下图所示:

您可以将这种变化检测分析与要素提取示例相结合,将变化提取为可在地理信息系统中有效分析的向量数据。
摘要
在本章中,我们介绍了遥感的基础,包括波段交换、直方图、图像分类、特征提取和变化检测。像在其他章节中一样,我们尽可能地接近纯 Python,在我们为了处理速度而妥协的地方,我们尽可能地限制软件库以保持简单。但是,如果你安装了本章中的工具,你就真的有了一个完整的遥感包,它只受你学习欲望的限制。
本章中的技术是所有遥感过程的基础,将允许您构建更复杂的操作。
在下一章中,我们将研究高程数据。高程数据不完全适合地理信息系统或遥感,因为它具有两种类型的处理元素。
进一步阅读
GDAL 的作者有一组 Python 示例,涵盖了许多您可能感兴趣的高级主题。可以在https://github . com/OSGeo/gdal/tree/master/gdal/swig/python/samples找到。
七、Python 和高程数据
高程数据是最迷人的地理空间数据类型之一。它代表许多不同类型的数据源和格式。它可以显示向量和栅格数据的属性,从而产生独特的数据产品。高程数据可用于地形可视化、土地覆盖分类、水文建模、运输路线、特征提取和许多其他目的。
栅格数据和向量数据不能执行所有这些选项,但是由于高程数据是三维的,由于包含 x 、 y 和 z 坐标,您通常可以从这些数据中获得比任何其他类型更多的信息。
在本章中,我们将涵盖以下主题:
- 使用 ASCII 网格高程数据文件进行简单的高程处理
- 创建阴影浮雕图像
- 创建高程等高线
- 激光雷达数据网格化
- 创建三维网格
在本章中,您将学习如何读写栅格和向量格式的高程数据。我们还将创造一些衍生产品。
访问 ASCII 网格文件
在本章的大部分内容中,我们将使用 ASCII 网格文件。这些文件是一种栅格数据,通常与高程数据相关联。这种网格格式将数据以文本形式存储在大小相等的方形行和列中,并带有简单的标题。行/列中的每个单元格存储一个数值,该数值可以表示地形的某些特征,如海拔、坡度或流向。简单性使其成为一种易于使用且独立于平台的栅格格式。这种格式在第二章学习地理空间数据的 ASCII 网格一节中有描述。
在本书中,我们一直依赖 GDAL,在某种程度上甚至依赖 PIL 来读写地理空间栅格数据,包括gdalnumeric模块,这样我们就可以将栅格数据加载到 NumPy 数组中。ASCII Grid 允许我们仅使用 Python 甚至 NumPy 读写栅格,因为它是简单的纯文本。
As a reminder, some elevation datasets use image formats to store elevation data. Most image formats only support 8-bit values ranging from between 0 to 255; however, some formats, including TIFF, can store larger values.
Geospatial software can typically display these datasets; however, traditional image software and libraries usually don't. For simplicity, in this chapter, we'll mostly stick to the ASCII Grid format for data, which is both human and machine-readable, as well as widely supported.
阅读网格
NumPy 能够使用其loadtxt()方法直接读取 ASCII Grid 格式,该方法旨在从文本文件中读取数组。前六行由标头组成,标头不是数组的一部分。以下几行是网格标题的示例:
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的参数,它允许您在读取数组值之前指定文件中要跳过的行数。
To try out this technique, you can download a sample grid file called myGrid.asc from http://git.io/vYapU.
因此,对于myGrid.asc,我们将使用以下代码:
myArray = numpy.loadtxt("myGrid.asc", skiprows=6)
这一行导致myArray变量包含一个从 ascigridmyGrid.asc文件派生的numpy数组。ASC 文件扩展名由 ASCIIGRID 格式使用。这段代码很好用,但是有一个问题。NumPy 允许我们跳过标题,但不能保留它。我们需要保持这一点,以便我们的数据有一个空间参考。我们还将使用它来保存这个网格或创建一个新的网格。
为了解决这个问题,我们将使用 Python 内置的linecache模块来抓取头部。我们可以打开文件,遍历这些行,将每一行存储在一个变量中,然后关闭文件。但是,linecache将解决方案简化为一行。下面一行将文件的第一行读入一个名为line1的变量:
import linecache
line1 = linecache.getline("myGrid.asc", 1)
在本章的示例中,我们将使用这种技术来创建一个简单的头处理器,它可以用几行代码将这些头解析成 Python 变量。现在我们知道如何阅读网格,让我们学习如何编写它们。
书写网格
用 NumPy 写网格和读网格一样简单。我们使用相应的numpy.savetxt()函数将一个网格保存到一个文本文件中。唯一的问题是,在将数组转储到文件之前,我们必须构建并添加六行标题信息。对于不同版本的 NumPy,此过程略有不同。在这两种情况下,首先将头构建为字符串。如果您使用的是 NumPy 1.7 或更高版本,savetext()方法有一个名为 header 的可选参数,允许您指定一个字符串作为参数。您可以使用以下命令从命令行快速检查您的 NumPy 版本:
python -c "import numpy;print(numpy.__version__)"
1.8.2
向后兼容的方法是打开一个文件,写入头,然后转储数组。下面是将名为myArray的数组保存到名为myGrid.asc的 ASCIIGRID 文件的 1.7 版本方法示例:
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 中,数组有两个属性:
- 大小:返回一个整数作为数组中值的个数。
- Shape:它返回一个元组,分别包含行数和列数。
因此,在前面的示例中,我们使用 shape 属性 tuple 将行数和列数添加到 ASCII 网格的标题中。请注意,我们还为每一行添加了一个尾随换行符(\n)。没有理由更改x和y值、单元格大小或无数据值,除非我们在脚本中更改它们。
savetxt()方法还有一个fmt参数,它允许您使用 Python 格式字符串来指定数组值的写入方式。在这种情况下,%1.2f值指定至少有一个数字且不超过两位小数的浮点数。1.6 之前的 NumPy 向后兼容版本以相同的方式构建头字符串,但首先创建文件句柄:
with open("myGrid.asc", "w") as f:
f.write(header)
numpy.savetxt(f, str(myArray), fmt="%1.2f")
正如您将在接下来的示例中看到的,这种仅使用 NumPy 生成有效地理空间数据文件的能力非常强大。在接下来的几个例子中,我们将使用加拿大不列颠哥伦比亚省温哥华附近山区的 ascigrid数字高程模型 ( DEM )。
You can download this sample as a ZIP file at the following URL: http://git.io/vYwUX.
下图是使用 QGIS 着色的原始数字高程模型,其色带使较低的高程值为深蓝色,较高的高程值为鲜红色:

虽然我们可以用这种方式从概念上理解数据,但这不是一种直观的数据可视化方式。让我们看看是否可以通过创建阴影浮雕做得更好。
创建阴影浮雕
阴影地形图以这样一种方式对高程进行着色,即地形看起来像是在低角度光线下投射的,这会产生亮点和阴影。这种美学风格创造了一种近乎摄影的错觉,很容易掌握,这样我们就可以理解地形的变化。需要注意的是,这种风格确实是一种错觉,因为从太阳角度来看,光线通常在物理上是不准确的,并且仰角通常被夸大以增加对比度。
在本例中,我们将使用之前引用的 ASCII 数字高程模型来创建另一个网格,该网格表示 NumPy 中地形的阴影浮雕版本。这个地形相当动态,所以我们不需要夸大海拔;但是,该脚本有一个名为z的变量,可以从 1.0 开始增加,以放大高程。
在我们定义了包括输入和输出文件名在内的所有变量之后,我们将看到基于linecache模块的标题解析器,它也使用 Python 列表理解来循环和解析行,然后将这些行从列表中拆分成六个变量。我们还创建了一个名为ycell的y细胞大小,它与常规细胞大小正好相反。如果我们不这样做,得到的网格将被转置。
Note that we define filenames for slope and aspect grids, which are two intermediate products that are combined to create the final product. These intermediate grids are output as well. They can also serve as inputs to other types of products.
该脚本使用三乘三的窗口方法来扫描图像,并平滑这些小网格中的中心值,以有效地处理图像。它是在计算机内存限制的情况下完成的。然而,因为我们使用的是 NumPy,所以我们可以通过矩阵一次处理整个数组,而不是使用一系列冗长的嵌套循环。这项技术是基于一位名叫 Michal Migurski 的开发人员的出色工作,他实现了马修·派瑞 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)
- 我们将逐行、逐列地循环数据,来处理它。但是,请注意,我们将跳过包含 nodata 值的外边缘。我们将把数据分成 3×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×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
- 现在,我们必须重建头部,因为我们忽略了 nodata 值的外边缘,并且数据集较小:
# 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)
- 接下来,我们将把任何 nodata 值设置为我们在开始时在变量中设置的所选 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。您也可以在我们在第 4 章、地理空间 Python 工具箱的安装 GDAL 部分讨论的 FWTools OpenEV 应用程序中打开图像,该应用程序将自动拉伸图像以获得最佳查看效果。
如您所见,前面的图像比我们最初检查的伪彩色表示更容易理解。接下来,让我们看看用于创建着色浮雕的坡度栅格:

斜率显示了数据集所有方向上从高点到低点的高程逐渐下降。对于许多类型的水文模型,坡度是一个特别有用的输入:

坡向显示了从一个像元到其相邻像元的最大下坡变化率。如果将纵横比图像与着色浮雕图像进行比较,您将看到纵横比图像的红色和灰色值对应于着色浮雕中的阴影。因此,坡度主要负责将数字高程模型转换为地形起伏,而坡向负责着色。
现在我们可以用一种有用的方式显示数据,让我们看看我们是否也可以从中创建其他数据。
创建高程等高线
等高线是数据集中沿同一高程的等值线。等高线通常以一定的间隔步进,以创建一种直观的方式来表示高程数据,包括视觉和数字,使用资源高效的向量数据集。现在,让我们看看另一种使用等高线更好地可视化高程的方法。
输入用于在我们的数字高程模型中生成等高线,输出是一个形状文件。用于生成轮廓的算法(行进的正方形:https://en.wikipedia.org/wiki/Marching_squares)相当复杂,并且很难使用 NumPy 的线性代数来实现。在这种情况下,我们的解决方案是依靠 GDAL 库,它有一个通过 Python 应用编程接口可用的轮廓绘制方法。事实上,这个脚本的大部分只是设置输出 shapefile 所需的 OGR 库代码。实际的轮廓绘制是一个名为gdal.ContourGenerate()的单一方法调用。就在这个调用之前,有定义方法参数的注释。最重要的如下:
contourInterval:这是等高线之间的数据集单位距离。contourBase:这是等高线的起始高程。fixedLevelCount:相对于距离,这指定了固定数量的轮廓。idField:这是一个必需的 shapefiledbf字段的名称,通常只叫 ID。elevField:这是高程值所需的 shapefiledbf字段的名称,对于地图中的标注非常有用。
你应该从第四章地理空间 Python 工具箱的安装 GDAL 部分安装 GDAL 和 OGR。我们将实施以下步骤:
- 首先,我们将定义输入的数字高程模型文件名。
- 然后,我们将输出 shapefile 的名称。
- 接下来,我们将使用 OGR 创建 shapefile 数据源。
- 然后,我们将得到 OGR 层。
- 接下来,我们将打开数字高程模型。
- 最后,我们将在 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绘制刚刚创建的轮廓形状文件,这是我们在第 4 章地理空间 Python 工具箱的 PNGCanvas 部分中介绍的:
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字段,用高程标记等高线:

这些数字网格示例中使用的技术为各种高程产品提供了构建模块。接下来,我们将使用最复杂的高程数据类型之一:LIDAR 数据。
使用激光雷达数据
LIDAR 代表光探测和测距。它类似于基于雷达的图像,但使用每秒钟撞击地面数十万次的有限激光束来收集大量非常精细的( x 、 y 、 z )位置以及时间和强度。强度值是激光雷达与其他数据类型的真正区别。例如,建筑物的沥青屋顶可能与附近的树顶高度相同,但强度会有所不同。就像遥感一样,多光谱卫星图像中的辐射值允许我们建立分类库。激光雷达数据的强度值允许我们对激光雷达数据进行分类和着色。
激光雷达的高体积和高精度实际上使其难以使用。激光雷达数据集被称为点云,因为数据集的形状通常是不规则的,因为数据是带有外围点的三维数据。没有多少软件包可以有效地可视化点云。
此外,不规则形状的有限点集合很难交互,即使我们使用适当的软件也是如此。
由于这些原因,对激光雷达数据最常见的操作之一是投影数据并将其重新采样到常规网格中。我们将使用一个小型激光雷达数据集来完成这项工作。该数据集大约 7 MB 未压缩,包含超过 600,000 个点。这些数据捕捉了一些容易识别的特征,如建筑物、树木和停车场中的汽车。您可以从http://git.io/vOERW下载压缩数据集。
文件格式是激光雷达特有的一种非常常见的二进制格式,称为 LAS ,是激光的缩写。将此文件解压缩到您的工作目录。为了阅读这种格式,我们将使用一个名为laspy的纯 Python 库。您可以使用以下命令安装 Python 版:
pip install http://git.io/vOER9
安装laspy后,我们准备从 LIDAR 创建一个网格。
从激光雷达数据创建网格
这个脚本相当简单。我们循环遍历 LIDAR 数据中的( x 、 y )点位置,并将它们投影到一米大小的网格中。由于激光雷达数据的精度,我们将在一个单元中有多个点。我们对这些点进行平均,以创建一个共同的高程值。我们必须处理的另一个问题是数据丢失。无论何时对数据进行重新采样,都会丢失信息。
在这种情况下,我们将在光栅的中间出现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 实现激光雷达数据可视化
本章前面的数字高程模型图像是使用 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 季刊,展示了 HSV 的颜色模型:

colorsys模块允许您在 HSV 和 RGB 值之间来回切换。该模块返回 RGB 值的百分比,然后必须将其映射到每种颜色的 0-255 比例。
在下面的代码中,我们将把 ASCII 数字高程模型转换成 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 类型是基于 Delaunay 三角剖分,包括所有没有冗余三角形的点。
德劳奈三角测量非常复杂。我们将使用一个由比尔·西蒙斯编写的纯 Python 库,作为史蒂夫·财富的德劳奈三角测量算法voronoi.py的一部分,来计算我们的激光雷达数据中的三角形。您可以从http://git.io/vOEuJ下载脚本到您的工作目录或site-packages目录。
这个脚本读取 LAS 文件,生成三角形,循环遍历它们,并写出一个形状文件。对于本例,我们将使用激光雷达数据的剪辑版本来减少要处理的区域。如果我们运行 600,000+点的整个数据集,脚本将运行数小时,并生成超过 50 万个三角形。您可以从以下网址下载剪辑的激光雷达数据集作为压缩文件:http://git.io/vOE62。
由于以下示例的时间密集性,可能需要几分钟才能完成,因此脚本运行时会打印几条状态消息。我们将把三角形存储为多边形类型,这允许顶点具有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 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 的放大版本:

网格从点云提供了一个高效、连续的表面,这比点云本身更容易处理。
摘要
高程数据通常可以为分析和衍生产品提供完整的数据集,而无需任何其他数据。在本章中,您学习了如何仅使用 NumPy 读写 ASCII 网格。您还学习了如何创建着色浮雕、坡度网格和坡向网格。我们使用一个鲜为人知的特性创建了高程等高线,这个特性叫做 GDAL 库的等高线,Python 提供了这个特性。
接下来,我们将激光雷达数据转换成易于操作的 ASCII 网格。我们用不同的方法对 PIL 的激光雷达数据进行了可视化实验。最后,我们通过将激光雷达点云转换为多边形的三维形状文件来创建三维表面或三角网。这些是地形分析工具,用于交通规划、建筑规划、水文排水建模、地质勘探等。
在下一章中,我们将结合前三章的构建块来执行一些高级建模,并实际创建一些信息产品。
进一步阅读
您可以通过以下链接找到一些关于 Python 和高程数据的附加教程:https://www . earth data science . org/tutories/Python/elevation/。
八、高级地理空间 Python 建模
在这一章中,我们将在已经学习的数据处理概念的基础上,创建一些全面的信息产品。以前介绍的数据处理方法很少能自己提供问题的答案。您可以结合这些数据处理方法,从多个已处理的数据集构建地理空间模型。地理空间模型是现实世界某个方面的简化表示,有助于我们回答关于项目或问题的一个或多个问题。在本章中,我们将介绍一些在农业、应急管理、物流和其他行业中常用的重要地理空间算法。
我们将创造的产品如下:
- 作物健康地图
- 洪水淹没模型
- 彩色的山影
- 地形路线图
- 街道路线图
- 带有地理位置照片链接的形状文件
虽然这些产品是特定于任务的,但是用于创建它们的算法被广泛应用于地理空间分析。我们将在本章中讨论以下主题:
- 创建归一化差异植被指数
- 创建洪水淹没模型
- 创建彩色山体阴影
- 执行最低成本路径分析
- 将路线转换为形状文件
- 沿着街道走
- 地理定位照片
- 计算卫星图像云量
本章中的例子比前几章更长,涉及面更广。因此,有更多的代码注释来使程序更容易理解。在这些例子中,我们还将使用更多的函数。在前面的章节中,为了清晰起见,大部分都避免了函数,但是这些例子足够复杂,以至于某些函数使得代码更容易阅读。这些示例是作为地理空间分析师,您将在工作中使用的实际流程。
技术要求
对于本章,需要满足以下要求:
- 版本 : Python 3.6 或更高
- RAM :最小 6 GB (Windows),8gb(macOS);推荐 8 GB
- 存储:最低 7,200 RPM SATA,可用空间 20 GB,推荐 SSD,可用空间 40 GB。
- 处理器:最低英特尔酷睿 i3 2.5 GHz,推荐英特尔酷睿 i5。
创建归一化差异植被指数
我们的第一个例子是归一化差异植被指数 ( NVDI )。NDVIs 用于显示感兴趣区域内植物的相对健康状况。一种 NDVI 算法使用卫星或航空图像,通过突出植物中的叶绿素密度来显示相对健康状况。NDVIs 只使用红色和近红外波段。NDVI 公式如下:
NDVI = (Infrared – Red) / (Infrared + Red)
这项分析的目标是,首先生成一个包含红外和红色波段的多光谱图像,最后生成一个使用七个类别的伪彩色图像,这些类别将健康的植物染成深绿色,不太健康的植物染成浅绿色,裸露的土壤染成棕色。
因为健康指数是相对的,所以定位感兴趣的区域很重要。您可以对整个地球执行相对索引,但是广阔的区域,例如低植被极端的撒哈拉沙漠和茂密的森林区域,例如亚马逊丛林,会扭曲中等范围内植被的结果。然而,话虽如此,气候科学家还是会定期创建全球 NDVIs 来研究全球趋势。不过,更常见的应用是用于管理区域,如森林或农田,如本例所示。
我们将首先分析密西西比三角洲的一个农场。为此,我们将从相当大面积的多光谱图像开始,并使用 shapefile 来隔离单个场。下面截图中的图像是我们的广阔区域,感兴趣的领域用黄色突出显示:

您可以从http://git.io/v3fS9下载该图像和农场区域的形状文件作为 ZIP 文件。
对于这个例子,我们将使用 GDAL,OGR,gdal_array / numpy,以及 Python 图像库 ( PIL )来剪辑和处理数据。在本章的其他示例中,我们将只使用简单的 ASCII 网格和 NumPy。因为我们将使用 ASCII 高程网格,所以不需要 GDAL。在所有示例中,脚本使用以下约定:
- 导入库。
- 定义函数。
- 定义全局变量,如文件名。
- 执行分析。
- 保存输出。
我们对作物健康示例的方法分为两个脚本。第一个脚本创建索引图像,这是一个灰度图像。第二个脚本对索引进行分类,并输出彩色图像。在第一个脚本中,我们将执行以下步骤来创建索引映像:
- 读取红外波段。
- 读取字段边界形状文件。
- 将形状文件光栅化为图像。
- 将 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,它勾勒出我们将要分析的特定区域的边界。该区域在更大的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 的最后过程是执行方程式红外-红色/红外+红色。我们执行的第一步是消除任何非数字,也称为 NaN ,NumPy 中可能在除法运算中出现的值。在保存输出之前,我们将把任何 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 进行分类
我们现在有一个有效的索引,但是不容易理解,因为它是一个灰度图像。如果我们用直观的方式给图像上色,那么即使是一个孩子也能识别出更健康的植物。在下一节附加功能中,我们读入这个灰度索引,并使用七个类别将其从棕色分类为深绿色。分类和图像处理例程,例如直方图和拉伸功能,几乎与我们在第 6 章、 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
这是我们输出的图像:

这是我们这个例子的最终产品。农民可以利用这些数据来确定如何以有针对性、更有效和更环保的方式有效灌溉和喷洒化肥和农药等化学品。事实上,这些类甚至可以转化为向量形状文件,然后加载到田间喷雾器上的全球定位系统驱动的计算机中。然后,当喷雾器在田地周围行驶时,或者在某些情况下,甚至在带有喷雾器附件的飞机上飞过田地时,这将自动在正确的位置应用正确数量的化学品。
还要注意,即使我们将数据剪切到字段中,图像仍然是一个正方形。黑色区域是已转换为黑色的 nodata 值。在显示软件中,您可以使 nodata 颜色透明,而不影响图像的其余部分。
虽然我们创建了一个非常特殊的产品类型,一个分类的 NDVI,这个脚本的框架可以被改变,以实现许多遥感分析算法。NDVIs 有不同的类型,但通过相对较小的更改,您可以将此脚本变成一个工具,用于查找海洋中的有害藻类水华,或者森林中间的烟雾,表明发生了森林火灾。
This book attempts to limit the use of GDAL as much as possible in order to focus on what can be accomplished with pure Python and tools that can easily be installed from PyPI. However, it is helpful to remember that there is a wealth of information on using GDAL and its associated utilities to carry out similar tasks. For another tutorial on clipping a raster with GDAL via its command-line utilities, see https://joeyklee.github.io/broc-cli-geo/guide/XX_raster_cropping_and_clipping.html.
现在我们已经研究了土地,让我们研究水,以便创建洪水淹没模型。
创建洪水淹没模型
在下一个例子中,我们将开始进入水文学的世界。洪水是最常见和最具破坏性的自然灾害之一,影响到全球几乎每一个人口。地理空间模型是评估洪水影响并在洪水发生前减轻其影响的有力工具。我们经常在新闻上听到一条河流正在达到洪水阶段,但是如果我们不能理解其影响,这些信息就没有意义。
水文洪水模型的开发成本很高,而且可能非常复杂。这些模型对工程师建造防洪系统至关重要。然而,第一反应者和潜在的洪水受害者只对即将到来的洪水的影响感兴趣。
我们可以使用一个非常简单易懂的工具洪水淹没模型开始了解一个地区的洪水影响。这个模型从一个点开始,在一个特定的洪水阶段,用一个洪水盆地所能容纳的最大水量淹没一个区域。通常,这种分析是最坏的情况。成百上千的其他因素被用来计算有多少水将从河流顶部的洪水阶段进入流域。但是我们仍然可以从这个简单的一阶模型中学到很多。
As mentioned in the Elevation data section in Chapter 1, Learning about Geospatial Analysis with Python, the Shuttle Radar Topography Mission (SRTM) dataset provides a nearly-global DEM that you can use for these types of models. More on SRTM data can be found here: http://www2.jpl.nasa.gov/srtm/.
你可以从 http://git.io/v3fSg 下载 EPSG 的 ASCII 网格数据:4326 和一个包含该点的形状文件作为.zip文件。shapefile 仅供参考,在这个模型中没有任何作用。下图是一个数字高程模型 ( DEM )源点显示为德克萨斯州休斯顿附近的一颗黄色恒星。在现实世界的分析中,这个点可能是一个测流计,在这里你可以得到河流水位的数据:

我们在这个例子中介绍的算法叫做洪水填充算法。该算法在计算机科学领域众所周知,在经典计算机游戏扫雷中使用,当用户点击方块时清除棋盘上的空方块。也是众所周知的油漆桶工具在 Adobe Photoshop 等图形程序中使用的方法,用于用不同的颜色填充同一颜色相邻像素的区域。
有很多方法可以实现这个算法。最古老和最常见的方法之一是递归地遍历图像的每个像素。递归的问题在于,你最终会不止一次地处理像素,并产生不必要的工作量。递归泛洪填充的资源使用很容易使程序崩溃,即使是中等大小的图像。
该脚本使用基于队列的四路泛洪填充,可以多次访问一个单元格,但确保我们只处理一次单元格。通过使用 Python 的内置集合类型,队列只包含唯一的、未处理的单元,该集合类型只保存唯一的值。我们使用两组:填充,包含我们需要填充的单元格,填充,包含处理后的单元格。
本示例执行以下步骤:
- 从 ASCII 数字高程模型中提取标题信息。
- 打开数字高程模型作为
numpy数组。 - 将我们的起点定义为数组中的行和列。
- 声明洪水高程值。
- 将地形过滤到所需的高程值及以下。
- 处理过滤后的数组。
- 创建一个 1,0,0 数组(即二进制数组),泛洪像素为 1。
- 将洪水淹没数组保存为 ASCII 网格。
This example can take a minute or two to run on a slower machine; we'll use the print statements throughout the script as a simple way to track progress. Once again we'll break this script up with explanations, for clarity.
现在我们有了数据,我们可以开始我们的洪水填充功能。
注水功能
我们在这个例子中使用了 ASCII 网格,这意味着这个模型的引擎完全在 NumPy 中。我们首先定义floodFill()函数,这是这个模型的核心和灵魂。这篇维基百科关于洪水填充算法的文章提供了不同方法的极好概述:http://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!")
下图显示了分类版数字高程模型的洪水淹没输出,其中较低的高程值为棕色,中等范围的值为绿色,较高的值为灰色和白色:

洪水栅格包括所有小于 70 米的区域,颜色为蓝色。此图像是用 QGIS 创建的,但它可以在 ArcGIS 中显示为 EPSG:4326。您也可以使用 GDAL 将洪水栅格保存为 8 位 TIFF 文件或 JPEG 文件,就像 NDVI 示例一样,以便在标准图形程序中查看它。
下面截图中的图像几乎完全相同,除了被过滤的蒙版,它显示为黄色。这是通过为名为wet的数组而不是fld生成一个文件来完成的,以显示不连续的区域,这些区域不包括在洪水中。这些区域没有连接到源点,因此在洪水期间不太可能到达:

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

90 米的洪水是浅蓝色的多边形。你可以采取更大或更小的步骤,并以不同的层显示不同的影响。
这个模型是一个优秀的和有用的可视化。但是,您可以通过在洪水遮罩上使用 GDAL 的polygonize()方法来进一步进行分析,就像我们在第 6 章、 Python 和遥感中的从图像中提取特征部分中对岛屿所做的那样。这个操作会给你一个向量淹没多边形。然后,您可以使用我们在第 5 章、 Python 和地理信息系统中执行选择部分讨论的原则,使用多边形选择建筑物以确定人口影响。您也可以将洪水多边形与第 5 章、 Python 和地理信息系统的点密度计算部分中的点密度示例结合起来,以评估洪水对人口的潜在影响。可能性是无穷的。
创建彩色山体阴影
在本例中,我们将结合以前的技术,将来自第 7 章、 Python 和的地形山体阴影与我们在 LIDAR 上使用的颜色分类相结合。对于这个例子,我们将需要命名为dem.asc和relief.asc的 ASCII 网格 DEMs,我们在上一章中使用了它们。
我们将创建一个彩色的数字高程模型和一个山体阴影,然后使用 PIL 将它们混合在一起,以增强高程可视化。代码注释将指导您完成示例,因为您已经熟悉其中的许多步骤:
- 首先,我们导入我们需要的库:
import gdal_array as gd
try:
import Image
except ImportError:
from PIL import Image
对于下一部分,您将需要以下两个文件:https://github . com/GeospatialPython/Learn/raw/master/relief . zip和https://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)
- 然后,我们将加载数字高程模型图像,这样我们将获得高程数据:
# 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
- 然后,我们可以将我们的着色浮雕数组转换为图像,以及我们的彩色数字高程模型:
# 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)
下图显示了输出,这为地理信息系统地图提供了很好的背景:

既然我们可以模拟地形,让我们学习如何在上面导航。
执行最低成本路径分析
计算行车方向是世界上最常用的地理空间功能。通常,这些算法计算点 A 和 B 之间的最短路径,或者它们可以考虑道路的速度限制,甚至当前的交通状况,以便通过行驶时间来选择路线。
但是如果你的工作是修建一条新路呢?或者,如果你负责决定在偏远地区的哪里铺设输电线路或输水线路呢?在基于地形的环境中,最短的路径可能会穿过一座困难的山,或者穿过一个湖。在这种情况下,我们需要考虑障碍,并尽可能避免它们。然而,如果避开一个小障碍让我们走得太远,实施这条路线的成本可能比仅仅翻越一座山还要高。
这种类型的高级分析称为最小成本路径分析。我们在一个区域内寻找一条路线,这条路线是距离与遵循这条路线的成本之间的最佳折衷。我们用于此过程的算法称为 A 星或 A* 算法。最古老的路由方法被称为迪克斯特拉算法,它计算网络中的最短路径,例如道路网络。A*方法也可以做到这一点,但它也更适合遍历网格状的数字高程模型。
You can find out more about these algorithms on the following web pages:
- 迪克斯特拉算法:http://en.wikipedia.org/wiki/Dijkstra's_algorithm。
- A*算法:http://en.wikipedia.org/wiki/A-star_algorithm。
这个例子是本章中最复杂的。为了更好地理解它,我们有一个简单版本的程序,它是基于文本的,在一个 5×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,我们在创建阴影地形*部分的第 7 章、 Python 和高程数据中使用了该 DEM。该网格的空间参考是 EPSG:26910 NAD 83/UTM 10N 区。您可以从http://git.io/v3fpL下载形状文件的数字高程模型、地形起伏以及起点和终点作为压缩包。
我们将实际使用阴影浮雕进行可视化。我们在本练习中的目标是以尽可能低的成本从起点到终点:

单看地形,有两条路径走低海拔路线,方向没有太大变化。下面的截图说明了这两条路线:

所以,我们期望当我们使用 A*算法时,它会很接近。请记住,该算法只查看紧邻区域,因此它不能像我们一样查看整个图像,也不能根据前面已知的障碍物在路线的早期进行调整。
我们将从我们的简单示例中扩展这个实现,并使用欧几里德距离,或乌鸦飞翔时的测量,我们还将允许搜索向八个方向而不是四个方向看。我们将优先考虑地形作为主要决策点。为了确保我们朝着目标前进,并且不会偏离轨道太远,我们还会把从终点到起点的距离作为较低的优先级。除了这些区别之外,步骤与简单的示例相同。输出将是一个栅格,路径值设置为 1,其他值设置为零。
*既然理解了问题,那就来解决吧!
正在加载网格
在这一节和接下来的几节中,我们将创建可以在地形上创建路线的脚本。剧本一开始很简单。我们将网格从 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的重要函数,它根据邻居和当前小区之间的海拔变化以及到目的地的距离,返回相邻小区的分数。
这个功能比单独的距离或高度更好,因为它减少了两个单元之间存在联系的机会,从而更容易避免回溯。这个评分公式大致基于一个名为尼森评分的概念,该概念通常用于这些类型的算法中,并在本章前面提到的维基百科文章中引用。这个函数的伟大之处在于,它可以用任何你想要的值给相邻的单元格打分。您还可以使用实时提要来查看相邻单元格中的当前天气,并避开有雨或雪的单元格。
下面的代码将创建我们穿越地形所需的距离函数和权重函数:
- 首先,我们将创建一个欧几里德距离函数,它将给出点之间的距离:
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.
"""
- 我们从
0的score开始,检查节点距离终点和起点的距离:
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
- 我们将第一个选项作为
best选项,并处理其他选项,边走边升级:
# 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 []
既然我们有了路由算法,我们就可以生成真实世界的路径。
生成真实世界的路径
最后,我们将现实世界的路径创建为零网格中的一串 1。然后,可以将该栅格引入到 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模块将列表对象保存到磁盘。我们将在下一节中使用这些数据来创建路线的向量形状文件。因此,我们将路径数据保存为一个腌制的 Python 对象,以后可以重用,而无需运行整个程序:
print("Saving path data...")
with open("path.p", "wb") as pathFile:
pickle.dump(p, pathFile)
print("Done!")
以下是我们搜索的输出路径:

如您所见,A*搜索非常接近我们手动选择的路线之一。在一些情况下,算法选择处理一些地形,而不是试图绕过它。有时,轻微的地形被认为比绕过它的距离花费更少。您可以在路线右上角的放大部分看到这种选择的示例。红线是我们的程序通过地形生成的路线:

我们只使用了两个值:地形和距离。但是你也可以添加数百个因素,如土壤类型、水体和现有道路。所有这些项目都可以作为阻抗或直接墙。您只需修改示例中的评分函数,以考虑任何附加因素。请记住,您添加的因素越多,就越难追踪 A实现在选择路线时在想什么*。
这种分析的一个明显的未来方向是将这条路线创建为一条线的向量版本。该过程将包括将每个单元映射到一个点,然后在将其保存为 shapefile 或 GeoJSON 文件之前,使用最近邻分析对这些点进行正确排序。
将路线转换为形状文件
最低成本路径路线的栅格版本对于可视化很有用,但它对于分析来说并不太好,因为它嵌入在栅格中,因此很难与其他数据集相关联,就像我们在本书中多次做的那样。我们的下一个目标是使用我们在创建路径时保存的路径数据来创建一个 shapefile,因为保存的数据是以正确的顺序保存的。下面的代码将把我们的栅格路径转换成一个更容易在地理信息系统中用于分析的形状文件:
- 首先,我们将导入我们需要的模块,这些模块并不多。我们将使用
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
- 现在,我们将从腌制对象中恢复
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))
- 最后,只需几行,我们就可以写出一个线条形状文件:
with shapefile.Writer("path", shapeType=shapefile.POLYLINE) as w:
w.field("NAME")
w.record("LeastCostPath")
w.line([coords])
干得好!您已经创建了一个程序,该程序可以根据一组规则自动通过障碍物,并将其导出到一个文件中,您可以在地理信息系统中显示和分析该文件!我们只使用了三个规则,但是您可以通过添加其他数据集,例如天气或水体,或者您可以想象的任何其他东西,来添加对程序如何选择路径的附加限制。
既然我们理解了在任意表面上开辟一条路径,我们就来看看通过网络的路由。
计算卫星图像云量
卫星图像给我们提供了一个强有力的地球鸟瞰图。它们有多种用途,我们在第 6 章、Python 和遥感中看到过。然而,它们有一个缺点——云。当卫星绕地球运行并收集图像时,它不可避免地会对云成像。除了阻碍我们对地球的观察,云数据还会通过在无用的云数据上浪费 CPU 周期对遥感算法产生不利影响,或者通过引入不需要的数据值来扭曲结果。
解决方案是创建云遮罩。云遮罩是将云数据隔离在单独栅格中的栅格。然后,您可以在处理图像时使用该栅格作为参考,以避免云数据,或者您甚至可以使用它从原始图像中移除云。
在本节中,我们将使用rasterio模块和rio-l8qa插件为 Landsat 图像创建一个云遮罩。云遮罩将被创建为单独的图像,仅包含云:
- 首先,我们需要从http://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允许您从头开始计算云遮罩,而无需使用质量保证数据。但是它需要一些额外的预处理步骤。您可以在这里了解更多信息:https://github.com/mapbox/rio-cloudmask.
沿着街道走
沿着街道走的路线使用一个由线组成的连接网络,这被称为图形。图中的线可以有阻抗值,这阻碍了路由算法将它们包括在路由中。阻抗值的例子通常包括交通量、速度限制甚至距离。布线图的一个关键要求是所有的线(称为边)都必须连接起来。为映射而创建的道路数据集通常具有节点不相交的线。
在这个例子中,我们将根据距离计算通过图表的最短路径。我们将使用起点和终点,它们不是图中的节点,这意味着我们必须首先找到离我们的起点和终点最近的图节点。
为了计算最短路线,我们将使用一个强大的纯 Python 图形库,称为网络。NetworkX 是一个通用的网络图形库,可以创建、操作和分析复杂的网络,包括地理空间网络。如果pip没有在你的系统上安装 NetworkX,那么你可以在http://networkx.readthedocs.org/en/stable/找到不同操作系统的 NetworkX 下载安装说明。
您可以从http://git.io/vcXFQ下载位于美国墨西哥湾沿岸的公路网以及起点和终点的 ZIP 文件。然后,您可以按照以下步骤操作:
- 首先,我们需要导入将要使用的库。除了网络之外,我们还将使用 PyShp 库来读写形状文件:
import networkx as nx
import math
from itertools import tee
import shapefile
import os
- 接下来,我们将当前目录定义为我们将要创建的 route shapefile 的输出目录:
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)
- 现在,我们将定义我们的道路网络形状文件。该道路网是美国地质调查局的美国州际公路文件 shapefile】美国地质勘探局)的子集,该文件已经过编辑,以确保所有道路都已连接:
shp = "road_network.shp"
- 接下来,我们将使用 NetworkX 创建一个图形,并将 shapefile 段添加为图形边:
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"))
下面的截图显示了浅灰色的道路网,起点和终点,以及黑色的路线。您可以看到,路线穿过道路网络,以便在最短的距离内到达距离终点最近的道路:

现在我们知道了如何创建各种类型的路线,我们可以看看如何定位您在路线上旅行时可能拍摄的照片。
地理定位照片
使用支持全球定位系统的相机拍摄的照片,包括智能手机,以一种称为 EXIF 标签的格式将位置信息存储在文件的标题中。这些标签主要基于 TIFF 图像标准使用的相同标题标签。在本例中,我们将使用这些标签创建一个形状文件,其中包含照片的点位置和照片的文件路径作为属性。
我们将在这个例子中使用 PIL,因为它能够提取 EXIF 数据。大多数用智能手机拍摄的照片都是带有地理标签的图像;但是,您可以从https://git.io/vczR0下载本例中使用的器械包:
- 首先,我们将导入我们需要的库,包括用于图像元数据的 PIL 和用于形状文件的 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 数据将 GPS 数据存储为 DMS 坐标)。第三个函数提取全球定位系统数据并执行坐标转换:
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]
- 现在,我们将照片信息保存为形状文件:
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 的截图显示,其中一张照片在使用运行要素操作工具点击关联点后打开:

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

系统允许您选择几个参数来学习下一站的当前位置和时间预测。在屏幕的右侧,有一个指向谷歌地图混搭的链接,显示了特定路线的交通跟踪数据,如下图所示:

这是一个非常有用的网站,但它不能让我们控制数据如何显示和使用。让我们直接使用 Python 和 NextBus REST API 访问原始数据,开始处理实时数据。
对于本章中的示例,我们将使用在此找到的有文档记录的 next bus API:http://www.nextbus.com/xmlFeedDocs/NextBusXMLFeed.pdf。
从这个例子开始,我们需要一个所需总线的列表。
下一个巴士公司名单
NextBus 的客户被称为机构。在我们的例子中,我们将在前往加州洛杉矶的路线上跟踪公共汽车。首先,我们需要获得一些关于该机构的信息。下一个总线应用编程接口由一个名为publicXMLFeed的网络服务组成,在这个服务中你可以设置一个名为command的参数。我们将在浏览器中调用agencyList命令,使用以下 REST URL 获取包含机构信息的 XML 文档:http://webservices.nextbus.com/service/publicXMLFeed?命令=机构列表。
当我们在浏览器中访问该链接时,它会返回一个包含<agency/>标签的 XML 文档。洛杉矶的标签如下所示:
<agency tag="lametro" title="Los Angeles Metro" regionTitle="California-Southern"/>
既然我们有了公共汽车的清单,我们需要知道它们可以行驶的路线。
下一班车路线列表
tag属性是雷霆湾的 ID,我们需要它来执行其他的 NextBus API 命令。其他属性是人类可读的元数据。我们需要的下一条信息是关于路线 2 巴士路线的细节。为了获取这些信息,我们将使用机构标识和routeList REST 命令,通过将网址粘贴到我们的网络浏览器中来获取另一个 XML 文档。
Note that the agency ID is set to the parameter in the REST URL: http://webservices.nextbus.com/service/publicXMLFeed?command=routeList&a=lametro.
当我们在浏览器中调用这个网址时,我们会得到以下 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 日,世界协调时午夜以来的毫秒数。纪元日期只是机器用来跟踪时间的计算机标准。在 NextBus API 内最容易做的事情就是为这个值指定0,它返回最后 15 分钟的数据。
有一个可选的direction标签,允许您在一条路线上有多辆相反方向行驶的公共汽车的情况下指定一个终止公共汽车站。但是,如果我们不指定,API 将返回第一个,这符合我们的需要。获取洛杉矶地铁主线路线的 REST 网址如下:http://webservices.nextbus.com/service/publicXMLFeed?命令=车辆位置& 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 点(22 点)关门。如果您在脚本中遇到错误,请使用 NextBus 网站找到正在运行的系统,并更改代理和路由变量到该系统。
我们现在可以编写一个 Python 脚本,返回给定路线上的公共汽车位置。如果我们不指定direction标签,NextBus 将返回第一个标签。在本例中,我们将通过使用前面章节中演示的内置 Python urllib库调用 REST URL 来轮询 NextBus 跟踪 API。
我们将使用简单的内置minidom模块解析返回的 XML 文档,也显示在迷你文档模块部分、第 4 章T5、地理空间 Python 工具箱中。这个脚本只是输出路线 2 公共汽车的最新纬度和经度。您将在顶部附近看到代理和路由变量。为此,我们需要遵循以下步骤:
- 首先,我们导入我们需要的库:
import urllib.request
import urllib.parse
import urllib.error
from xml.dom import minidom
- 现在,我们为应用编程接口模式以及要查询的客户和路线设置变量:
# 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"
- 现在,我们需要构建用于访问应用编程接口的查询网址:
# 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。输出应该是纬度和经度的坐标值。
现在我们准备使用这些位置值来创建我们自己的地图。
映射下一个总线位置
免费获取街道地图数据的最佳来源是开放街道地图 ( OSM )项目:http://www.openstreetmap.org。OSM 还有一个可公开获取的 REST API,用于创建静态地图图像,名为 StaticMapLite : 。
OSM 静态地图应用编程接口提供了一个基于谷歌静态地图应用编程接口的GET应用编程接口,以创建具有有限数量的点标记和线的简单地图图像。一个GET API,与 REST 相反,API 允许你在网址上的问号后面附加名称/值参数对。REST 应用编程接口将参数作为网址路径的一部分。我们将使用该应用编程接口按需创建我们自己的下一个总线应用编程接口地图,带有一个红色的图钉图标用于总线位置。
在下一个例子中,我们已经将之前的脚本压缩为一个名为nextbus()的紧凑函数。nextbus()函数接受代理、路线、命令和纪元作为参数。该命令默认为vehicleLocations,纪元默认为0,以获取最后 15 分钟的数据。在这个脚本中,我们将传入 LA route-2 路由信息,并使用默认命令返回最近的总线纬度/经度。
我们有第二个名为nextmap()的函数,它会创建一个地图,在每次调用公共汽车时的当前位置都有一个紫色标记。地图是通过为 OSM StaticMapLite应用编程接口建立一个GET网址创建的,该网址以公共汽车的位置为中心,并使用 1-18 和地图大小之间的缩放级别来确定地图范围。
You can access the API directly in a browser to see an example of what the nextmap() function does. You will need a free MapQuest Developer API key available by registering here: https://developer.mapquest.com/plan_purchase/steps/business_edition/business_edition_free/register. Once you have the key, insert it in the key parameter where it says YOUR_API_KEY_HERE. Then, you can test the following example 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()函数获取纬度/经度对。该程序的执行以定时间隔循环进行,在第一次通过时创建一个地图,然后在后续通过时覆盖该地图。该程序还会在每次保存地图时输出时间戳。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"
- 现在,我们可以通过调用网址来创建图像并保存它:
# 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
该脚本会保存一个类似于以下内容的地图图像,具体取决于运行该脚本时公共汽车的位置:

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

另一方面,OSM 网站不再通过 WMS 提供街道地图——只提供瓷砖。但是,它们允许其他组织下载切片或原始数据来扩展免费服务。美国国家海洋和大气管理局 ( 美国国家海洋和大气管理局)已经做到了这一点,并为他们的 OSM 数据提供了一个 WMS 接口,允许请求检索我们的公交路线所需的单个底图图像:

我们现在有了获取底图和天气数据的数据源。我们想把这些图像结合起来,画出公共汽车的当前位置。这次,我们将不再使用简单的点,而是变得更加复杂,并添加以下总线图标:

您需要从这里将此图标busicon.png下载到您的工作目录中:https://github . com/GeospatialPython/Learn/blob/master/busicon . png?原始=真实。
现在,我们将结合以前的脚本和新的数据源来创建实时天气巴士地图。因为我们要混合街道图和天气图,我们需要前几章使用的 Python 图像库 ( PIL )。我们将用一个简单的wms()函数替换前面例子中的nextmap()函数,该函数可以通过任何 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。但是该应用编程接口有几个第三方 Python 模块,包括 PyPI 上的一个,简称为nextbus,它允许您为所有 NextBus 命令处理更高级别的对象,并提供本章简单示例中没有的更健壮的错误处理。
现在我们已经学会了如何检查天气,让我们使用 Python、HTML 和 JavaScript 将离散的实时数据源组合成更有意义的产品。
来自现场的报告
在这一章的最后一个例子中,我们将下车到野外。现代智能手机、平板电脑和笔记本电脑允许我们更新地理信息系统,并从任何地方查看这些更新。我们将使用 HTML、GeoJSON、小叶 JavaScript 库和名为 leaf 的纯 Python 库来创建一个客户端-服务器应用程序,该应用程序允许我们将地理空间信息发布到服务器,然后创建一个交互式网络地图来查看这些数据更新。
首先,我们需要一个 web 表单,显示您的当前位置,并在您提交表单时更新服务器,其中包含关于您的位置的注释。你可以在这里找到表格:http://geospatialpython.github.io/Learn/fieldwork.html。
下面的截图显示了表单:

您可以查看该表单的源代码,了解其工作原理。映射是使用传单库完成的,并将地理信息发布到 myjson.com 的一个唯一的网址上。您可以在移动设备上使用此页面,将其移动到任何 web 服务器,甚至可以在本地硬盘上使用。
该表单在myjson.com上公开发布到以下网址:https://api.myjson.com/bins/467pm。您可以在浏览器中访问该网址来查看原始地理信息。
接下来,您需要从 PyPI 安装叶库。leaf 为创建 leaf 网络地图提供了一个简单的 Python 应用编程接口。你可以在这里找到更多关于叶的信息:https://github.com/python-visualization/folium。
leaf 使得制作一张 leaf 地图变得非常简单。这个脚本只有几行,将输出一个名为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 文件。
摘要
实时数据是进行新类型地理空间分析的一种令人兴奋的方式,只是最近几项不同技术的进步才使其成为可能,包括网络地图、全球定位系统和无线通信。在本章中,您学习了如何访问实时位置数据的原始源,如何获取实时栅格数据的子集,如何仅使用 Python 将不同类型的实时数据组合到自定义地图分析产品中,以及如何构建客户端-服务器地理空间应用程序来实时更新地理信息系统。
与前几章一样,这些示例包含构建块,允许您使用 Python 构建新类型的应用程序,这些应用程序远远超出了典型的流行且无处不在的基于 JavaScript 的混搭。
在下一章中,我们将把迄今为止所学的一切结合成一个完整的地理空间应用程序,在现实场景中应用算法和概念。
十、把这一切放在一起
在整本书中,我们已经触及了地理空间分析的所有重要方面,并在 Python 中使用了各种不同的技术来分析不同类型的地理空间数据。在这最后一章中,我们将利用几乎所有我们已经讨论过的主题来制作一个非常受欢迎的现实世界产品:全球定位系统路线分析报告。
这些报告常见于数十种移动应用服务、全球定位系统手表、车载导航系统和其他基于全球定位系统的工具。全球定位系统通常记录位置、时间和高度。从这些值中,我们可以得出大量的辅助信息,这些信息与记录数据的路线沿线发生的情况有关。包括 RunKeeper、MapMyRun、Strava 和 Nike Plus 在内的健身应用程序都使用类似的报告来呈现来自跑步、徒步旅行、骑自行车和步行的全球定位系统跟踪的锻炼数据。
我们将使用 Python 创建其中一个报告。这个程序有将近 500 行代码,是我们迄今为止最长的,所以我们将一点一点地来看。我们将结合以下技术:
- 理解典型的全球定位系统报告
- 构建全球定位系统报告工具
当我们逐步完成这个程序时,所有使用的技术都将是熟悉的,但是我们将以新的方式使用它们。
技术要求
本章我们需要以下内容:
- Python 3.6 或更高版本
- 内存:最小–6gb(Windows),8gb(macOS);推荐 8 GB
- 存储:最低 7200 转/分的 SATA,20 GB 可用空间,推荐的固态硬盘,40 GB 可用空间
- 处理器:最低英特尔酷睿 i3 2.5 GHz,推荐英特尔酷睿 i5
- PIL:Python 图像库
- 多维数组处理库
pygooglechart:优秀的谷歌图表 API 的 Python 包装器- FPDF:一个简单而纯粹的 Python PDF 作者
理解典型的全球定位系统报告
典型的全球定位系统报告具有共同的元素,包括路线图、高程剖面图和速度剖面图。以下截图是通过 RunKeeper(https://runkeeper.com/index)记录的典型路线的报告:

我们的报告将是相似的,但我们将增加一个转折。我们将像这项服务一样包括路线图和海拔剖面图,但我们还将添加记录路线时发生的天气情况以及在路线上拍摄的地理位置照片。
现在我们已经知道了什么是全球定位系统报告,让我们学习如何构建它。
构建全球定位系统报告工具
我们节目的名字是GPX-Reporter.py。如果你还记得第 2 章学习地理空间数据中的标记和基于标记的格式一节, GPX 格式是存储全球定位系统路线信息的最常见方式。几乎每一个依靠全球定位系统数据的程序和设备都可以在 GPX 之间进行转换。
对于本例,您可以从:http://git.io/vl7qi下载一个示例 GPX 文件。此外,您还需要安装一些来自 PyPI 的 Python 库。
你应该简单地使用easy_install或pip来安装这些工具。我们还将使用一个名为SRTM.py的模块。该模块用于处理奋进号航天飞机在 2000 年为期 11 天的航天飞机雷达地形任务 ( SRTM )中收集的近全球高程数据。使用pip安装 SRTM 模块:
pip install srtm.py
或者,您也可以下载压缩文件,将其解压缩,并将srtm文件夹复制到您的 Python site-packages目录或您的工作目录:http://git.io/vl5Ls。
您还需要注册一个免费的黑暗天空应用编程接口。这项免费服务提供独特的工具。这是唯一一项为几乎任何地点提供全球历史天气数据的服务,每天最多可免费提供 1000 个请求:https://darksky.net/dev。
黑暗天空将为您提供一个文本键,您可以在运行 GPX-记者程序之前将其插入到名为api_key的变量中。最后,根据黑暗天空的服务条款,您需要下载一个徽标图像以插入报告中:https://raw . githubusercontent . com/GeospatialPython/Learn/master/darksky . png。
You can review the Dark Sky Terms of Service here: https://darksky.net/dev/docs/terms.
现在,我们准备通过 GPX-记者项目开展工作。像本书中的其他脚本一样,这个程序试图最小化函数,这样您就可以更好地跟踪程序,并更轻松地修改它。以下列表包含程序中的主要步骤:
- 设置 Python
logging模块 - 建立我们的助手函数
- 解析 GPX 数据文件
- 计算路线边界框
- 缓冲边界框
- 将盒子转换为仪表
- 下载底图
- 下载高程数据
- 对高程数据进行山体阴影
- 增加山体阴影对比度
- 混合山体阴影和底图
- 在单独的图像上绘制 GPX 轨迹
- 混合轨迹图像和底图
- 绘制起点和终点
- 保存地图图像
- 计算路线英里标记
- 构建高程剖面图
- 获取路线时间段的天气数据
- 生成 PDF 报告
下一小节将带您完成第一步。
初始设置
程序的开头是import语句,后面是 Python logging模块。与简单的print语句相比,logging模块提供了一种更强大的方法来跟踪和记录程序状态。在程序的这一部分,我们按照以下步骤进行配置:
- 我们首先需要安装我们需要的所有库,如下面的代码所示:
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 中,并记录在这里:https://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
- 现在我们有了哈弗斯距离函数和简单的
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
- 接下来,我们有我们的程序变量。我们将访问由一家名为的公司免费提供的WMS 开放街道地图服务,以及美国宇航局提供的 SRTM 数据。
为了简单起见,我们使用 Python 的urllib库来访问本书中的 WMS 服务,但是如果您计划频繁使用 OGC web 服务,您应该使用通过 PyPI 提供的 Python 包 OWSLib:https://pypi.python.org/pypi/OWSLib。
现在,让我们执行以下步骤来设置 WMS web 服务:
- 我们将输出几个中间产品和图像。这些变量在这些步骤中使用。
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
现在,我们将使用built-in 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 的全球定位系统条目并解析这些值:
# 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)
时间戳需要一点额外的工作,因为我们必须从格林尼治时间转换为当地时间:
# 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
- 最后,我们在一个变量中设置我们的边界框,并将我们的坐标转换为米,这是 web 服务所需要的:
# 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")
脚本的这一部分还输出了一个中间高程图像,如下图所示:

现在我们有了海拔图像,我们可以把它变成山体阴影。
创建山体阴影
我们可以通过在第 7 章、 Python 和高程数据中创建阴影浮雕部分使用的相同山体阴影算法来运行该数据。为此,让我们遵循以下步骤:
- 首先,我们打开我们的高程图像,并将其读入一个
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)
- 现在,我们准备好在混合地图上绘制全球定位系统轨迹,首先将我们的点转换为像素:
# 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")
现在我们已经画出了我们的轨迹,我们准备放置我们的地理标记的照片。
定位照片
我们将使用添加了全球定位系统位置坐标的手机拍摄的照片。你可以从
下载。
将图像放在与脚本相同级别的名为photos的目录中。我们将只使用一张照片,但脚本可以处理你想要的男人图像。我们将在地图上绘制并放置一个照片图标,然后保存完成的底图,如以下步骤所示:
- 首先,我们获得一个包含以下代码的图像列表:
# Photo icon
images = glob.glob("photos/*.jpg")
- 接下来,我们遍历每张图像并获取其全球定位系统信息:
for i in images:
e = exif(i)
- 然后,我们使用全球定位系统函数解析位置信息,如下所示:
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))
虽然没有保存到文件系统中,但山坡高程如下所示:

混合地形图如下图所示:

虽然山体阴影制图让我们了解了海拔高度,但它没有给我们任何定量数据。为了获得更详细的信息,我们将创建一个简单的立面图。
测量高度
使用出色的谷歌地图应用编程接口,我们可以快速构建一个漂亮的高程剖面图,显示路线上的高程变化:
- 首先,我们将为我们的高程剖面创建
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))
我们的图表现在应该如下图所示:

我们的第一张图表完成了。现在,让我们看看沿途的天气数据。
检索天气数据
在本节中,我们将检索我们的最终数据元素:天气。如前所述,我们将使用黑暗天空服务,该服务允许我们收集世界上任何地方的历史天气报告。天气 API 是基于 REST 和 JSON 的,所以我们将使用urllib模块请求数据,使用json库解析数据。本节中值得注意的是,我们在本地缓存数据,因此如果需要,您可以离线运行脚本进行测试。在这一部分的早期,您可以放置由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)
- 现在,我们按如下方式设置免费的黑暗应用编程接口密钥,这样我们就可以检索天气数据:
# 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 报告中。
在某些情况下,除了 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 文档,其中包含您的成品。它应该看起来像下面截图中显示的图像:

有了这个,我们使用了我们在这本书中学到的所有技术,并建立了一个全球定位系统报告工具。
摘要
恭喜你!在这本书里,你汇集了成为一名现代地理空间分析师所需的最基本的工具和技能。无论您是偶尔使用地理空间数据还是一直使用它,您都将能够更好地充分利用地理空间分析。这本书的重点是使用几乎完全在 PyPI 目录中找到的开源工具,以便于安装和集成。但是,即使您使用 Python 作为商业地理信息系统包或流行库(如 GDAL)的驱动程序,用纯 Python 测试新概念的能力也总是会派上用场。
进一步阅读
Python 为可视化数据提供了一套丰富的库。其中最突出的是 Matplotlib ,可以生成无数类型的图表和地图,并保存为 PDF。Packt 有几本关于 Matplotlib 的书,包括 Matplotlib 30 食谱:https://www . packtpub . com/大数据与商业智能/matplotlib-30 食谱。
第一部分:行业的历史和现状
本节首先使用插图、基本公式、简单代码和 Python 演示常见的地理空间分析过程。在此基础上,您将学习如何使用地理空间数据——获取数据并为各种分析做准备。之后,您将了解地理空间技术生态系统中使用的各种软件包和库。在本节的最后,您将学习如何评估任何地理空间工具。
本节包括以下章节:
第二部分:地理空间分析概念
本节代表了本书的主要构建模块,在这里您将通过不同的代码示例和数据编辑概念了解 Python 在行业中的作用。您将了解地理空间产品以及如何应用它们来解决问题。接下来,您将看到如何使用 Python 实际处理遥感数据。在本节的最后,您将了解如何以任何地理空间格式使用高程数据来分析三维要素。
本节包括以下章节:
第三部分:实用地理空间处理技术
这部分是高级水平,它将需要你以前学习的所有技能。它从学习如何创建地理空间模型来回答特定的问题开始。此外,它将向您展示一些构建地理空间模型的技术,以及如何使用可视化概念帮助预测未来。我们将继续访问和处理实时数据。在这一节的最后,我们将结合前面几节所学的知识,实现一个系统,根据全球定位系统数据和地理标记的照片创建一个户外跑步或徒步报告。
本节包括以下章节:


浙公网安备 33010602011771号