Python-地理空间开发实例-全-
Python 地理空间开发实例(全)
原文:
zh.annas-archive.org/md5/7e71ad12605b22e21e6a5eb4df0dec58译者:飞龙

通过示例使用 Python 进行地理空间开发
Python
目录
通过示例使用 Python 进行地理空间开发
致谢
关于作者
关于审稿人
www.PacktPub.com
支持文件、电子书、折扣优惠等
为什么订阅?
Packt 账户持有者的免费访问
前言
本书涵盖的内容
本书所需内容
本书面向的对象
约定
读者反馈
客户支持
下载示例代码
下载本书的颜色图像
勘误表
盗版
问题
- 准备工作环境
安装 Python
Windows
Ubuntu Linux
Python 包和包管理器
Windows 上 Python 包的存储库
安装包和所需软件
OpenCV
Windows
Ubuntu Linux
安装 NumPy
Windows
Ubuntu Linux
安装 GDAL 和 OGR
Windows
Ubuntu Linux
安装 Mapnik
Windows
Ubuntu Linux
安装 Shapely
Windows
Ubuntu Linux
直接从 pip 安装其他包
Windows
Ubuntu Linux
安装 IDE
Windows
Linux
创建书籍项目
编程和运行第一个示例
转换坐标系并计算所有国家的面积
按面积大小排序国家
摘要
- 地理藏宝应用
构建基本应用程序结构
创建应用程序树结构
函数和方法
记录你的代码
创建应用程序入口点
下载地理藏宝数据
地理藏宝数据源
从 REST API 获取信息
从 URL 下载数据
手动下载数据
打开文件并获取其内容
为分析准备内容
将函数组合到应用程序中
设置您的当前位置
找到最近点
摘要
- 结合多个数据源
表示地理数据
表示几何形状
使数据同质化
抽象的概念
抽象化地理藏宝点
抽象化地理藏宝数据
导入地理藏宝数据
读取 GPX 属性
返回同质化数据
将数据转换为 Geocache 对象
合并多个数据源
将新功能集成到应用程序中
摘要
- 提高应用搜索能力
处理多边形
了解已知文本
使用 Shapely 处理几何形状
导入多边形
获取属性值
导入线条
转换空间参考系统和单位
几何关系
触摸
交叉
包含
在...内
等于或几乎等于
相交
不相交
按属性和关系过滤
按多个属性过滤
链式过滤
与应用集成
总结
- 制作地图
了解 Mapnik
使用纯 Python 制作地图
使用样式表制作地图
创建生成地图的实用函数
在运行时更改数据源
自动预览地图
设置地图样式
地图样式
多边形样式
线条样式
文本样式
向地图添加图层
点样式
使用 Python 对象作为数据源
导出地理对象
创建 Map Maker 应用
使用 Python 数据源
使用带有过滤器的应用
总结
- 处理遥感图像
理解图像的表示方式
使用 OpenCV 打开图像
了解数值类型
处理遥感图像和数据
图像镶嵌
调整图像的值
裁剪图像
创建阴影地形图
构建图像处理流水线
创建 RasterData 类
总结
- 从栅格数据中提取信息
获取基本统计信息
准备数据
打印简单信息
格式化输出信息
计算四分位数、直方图和其他统计数据
将统计数据做成懒属性
创建彩色分类图像
为地图选择合适的颜色
混合图像
用颜色显示统计数据
使用直方图对图像进行着色
总结
- 数据挖掘应用
测量执行时间
代码分析
在数据库中存储信息
创建对象关系映射
准备环境
改变我们的模型
自定义管理器
生成表格并导入数据
过滤数据
导入大量数据
优化数据库插入
优化数据解析
导入 OpenStreetMap 的兴趣点
移除测试数据
用真实数据填充数据库
搜索数据和交叉信息
使用边界过滤
总结
- 处理大图像
处理卫星图像
获取 Landsat 8 图像
内存和图像
分块处理图像
使用 GDAL 打开图像
遍历整个图像
创建图像合成
真彩色合成
处理特定区域
假彩色合成
总结
- 并行处理
多进程基础
块迭代
提高图像分辨率
图像重采样
全色增强
总结
索引
通过示例进行地理空间开发
Python
通过示例进行地理空间开发
Python
版权所有 © 2016 Packt Publishing
版权所有。未经出版者事先书面许可,本书的任何部分不得以任何形式或任何方式复制、存储在检索系统中或通过任何手段传输,除非在评论或评论中嵌入的简短引用。
在准备本书的过程中,我们已尽最大努力确保所提供信息的准确性。然而,本书中的信息销售不附带任何明示或暗示的保证。作者、Packt Publishing 及其经销商和分销商不对由此书直接或间接造成的任何损害承担责任。
Packt Publishing 已尽力通过适当使用大写字母提供本书中提到的所有公司和产品的商标信息。
然而,Packt Publishing 不能保证此信息的准确性。
首次出版:2016 年 1 月
生产参考:1250116
由 Packt Publishing Ltd. 出版
Livery Place
35 Livery Street
英国伯明翰 B3 2PB。
ISBN 978-1-78528-235-5
鸣谢
作者
Pablo Carreira
审稿人
Brylie Christopher Oxley
Vivek Kumar Singh
Claudio Sparpaglione
委托编辑
Sarah Crofton
采购编辑
Meeta Rajani
内容开发编辑
Rashmi Suvarna
技术编辑
Shivani Kiran Mistry
校对编辑
Akshata Lobo
项目协调员
Judie Jose
校对
Safis 编辑
索引编制者
Hemangini Bari
图形设计
Disha Haria
生产协调员
Nilesh Mohite
封面设计
Nilesh Mohite
关于作者
Pablo Carreira 是一位居住在巴西圣保罗州的 Python 程序员和全栈开发者。他现在是精准农业先进网络平台的负责人,并积极将 Python 作为后端解决方案用于高效的地理处理。
1980 年出生于巴西,Pablo 以农业工程师的身份毕业。作为一个自幼对编程充满热情且自学成才的人,他将编程作为爱好,后来为了解决工作任务而提升了自己的技术。
在地理处理领域拥有 8 年的专业经验,他使用 Python 以及地理信息系统来自动化流程并解决与精准农业、环境分析和土地划分相关的问题。
我想感谢我的父母,感谢他们在我一生中给予的支持。我还想感谢我的妻子,感谢她在所有写作过程中对我的帮助和耐心。
我感谢我的大学老师 José Paulo Molin,是他首先向我介绍了地理处理和精准农业,并在这一领域激发了我浓厚的兴趣,并为我提供了发展的机会。最后,我要感谢我的好朋友 Gerardo F. E. Perez,感谢他为我提供的所有机会,以及在无数个技术讨论中的陪伴。
关于审稿人
布赖利·克里斯托弗·奥克利喜欢从事改善人类和环境共同体的技术项目。他致力于为开源、开放 Web 和开放知识运动工作。
他定期为使用 Web 平台技术构建的开源项目做出贡献,包括为老龄化人口的健康状况可视化应用程序和为难民支持的社区门户。
感谢 Elvin、Lauri、Antti、Marjo 和 Lynne,你们是我生命中的璀璨明星。
维韦克·库马尔·辛格是印度理工学院德里分校大气科学中心的科研学者。他在印度遥感研究所,印度空间研究组织(ISRO)位于乌塔拉坎德德里的分校完成了遥感与地理信息系统技术的硕士学位。在研究生期间,他专注于使用卫星观测的地理计算模型来研究遥感与 GIS 的不同应用。他的主要研究专长在于应用卫星遥感进行空气质量监测和评估、城市热岛和遥感,以及全球的地理信息系统。他还对研究成长中的大城市的空气质量、城市地区气溶胶的空间和时间趋势、气溶胶的辐射效应、开发统计模型来估计地表颗粒物空气质量、气溶胶和云数据验证,以及从紫外卫星测量中检索云产品感兴趣。他也是 BhuNak 科学团队的一员,参与开发和验证用于气候研究的新 GIS 产品。
他目前还参与了 BhuNak 项目的技术研讨会和能力建设活动的开发和实施,在那里他教授使用卫星图像进行环境决策活动的应用,重点关注城市生活质量。
我要感谢瓦伊布哈夫·库马尔,印度理工学院孟买分校(IIT-Bombay)城市科学与工程学院的博士研究生,他为我的贡献(BhuNak 研究小组的共同创始人)。特别感谢 ML 辛格(父亲)、普拉尚特·库马尔·辛格(兄弟)和基梅拉·图马拉(朋友)在我生活中的贡献。
克劳迪奥·斯帕帕格利奥内是意大利初创公司 WalletSaver 的首席技术官,该公司在移动电话资费比较领域独树一帜。他的工作经验包括在线广告行业中地理空间应用和面向 Web 的系统设计和构建。
作为一位充满激情的 Python 程序员和开源倡导者,他是 PyOWM 的维护者。
项目,并积极参与社区,为 Python-Requests 和 Reactive Manifesto 等项目做出贡献。他的主要兴趣包括高可扩展的 Web 架构、API 设计以及云计算。
www.PacktPub.com

支持文件、电子书、折扣优惠以及
更多
有关您书籍的支持文件和下载,请访问 www.PacktPub.com。
您知道 Packt 为每本书都提供电子书版本,包括 PDF 和 ePub 文件吗?您可以在 www.PacktPub.com 升级到电子书版本,并且作为印刷版书籍的顾客,您有权获得电子书副本的折扣。有关更多详情,请联系我们 service@packtpub.com。
在 www.PacktPub.com,您还可以阅读一系列免费的技术文章,注册各种免费通讯,并享受 Packt 书籍和电子书的独家折扣和优惠。
www2.packtpub.com/books/subscription/packtlib
您需要即时解决您的 IT 问题吗?PacktLib 是 Packt 的在线数字图书库。在这里,您可以搜索、访问和阅读 Packt 的整个图书库。
为什么要订阅?
在 Packt 出版的每本书中都可以全文搜索。
复制粘贴、打印和收藏内容。
按需访问,可通过网页浏览器访问。
Packt 账户持有者的免费访问
如果您在 www.PacktPub.com 有 Packt 账户,您可以使用它今天访问 PacktLib 并查看 9 本完全免费的书籍。只需使用您的登录凭证即可立即访问。
前言
从 Python 编程的良好实践到分析包的高级使用,本书教授如何编写能够执行复杂地理处理任务的应用程序,这些任务可以复制和重用。本书包含三个示例应用程序。第一章
展示了如何准备开发环境。从 第二章 到 第四章,读者深入使用类、继承和其他资源来阅读、操作、组合和搜索矢量数据中的信息。第五章 到
第七章 介绍了渲染美丽地图和处理分析栅格数据的技术。在最后三章中,本书探讨了代码优化,并提出了处理地理处理任务中常见的大数据集的解决方案。所有示例都是模块化的,可以重新排列以实现无数不同的结果。在本书中,代码逐步推导,直至达到最终形式。读者被引导编辑、更改和改进代码,尝试不同的解决方案和组织方式,微妙地学习地理处理应用程序开发的心理过程。
本书涵盖的内容
第一章,准备工作环境,展示了安装所有必需库的过程,以便通过书中的示例,以及如何设置一个集成开发环境(IDE),这将有助于组织代码并避免错误。最后,它将介绍与地理空间库的第一次接触。
第二章,地理藏宝应用,将介绍地理处理应用中的重要步骤,例如打开文件、读取数据以及使用手头工具准备分析。通过这些步骤,用户将学习如何组织和利用语言提供的资源来编写一致的应用程序。
第三章,结合多个数据源,将涵盖结合数据源的过程以及如何使用 Python 类来创建自己的地理空间数据表示。地理数据往往具有异质性,因此编写能够结合多个数据源的程序是地理处理中的基本主题。
第四章,提高应用搜索能力,将为应用程序添加新功能。用户将编写一个能够通过地理边界和任何数据字段过滤特征的代码。在这个过程中,他们将了解如何处理多边形以及如何在地理处理应用程序中分析几何形状之间的关系。
第五章,制作地图,将启动一个新应用,该应用能够从数据向量生成漂亮的地图。Mapnik,最常用的地图包之一,将被使用。用户将了解其工作原理以及如何将其适应以消费前几章中展示的数据。
第六章,处理遥感图像,将展示一个演绎过程,这将导致一个灵活且强大的软件结构,能够结合、裁剪和调整图像的值,以准备它们进行展示。
第七章,从栅格数据中提取信息,将探讨从栅格数据中提取信息的过程,这些信息可以进行分析以产生有价值的信息。它将超越简单的数值,展示如何将此信息显示在漂亮的彩色地图上。
第八章,数据挖掘应用,将展示如何使用数据库以及如何将其导入以最小化处理时间,并允许处理大量数据集。地理空间数据往往很庞大,其处理需要大量的计算机功率。为了使代码更高效,读者将学习代码分析优化技术。
第九章,处理大图像,将展示如何处理大型卫星图像。它将重点介绍如何进行可持续的图像处理,以及如何在保持内存消耗低的同时,使用高效代码打开和计算许多大图像。
第十章,并行处理,将教会读者如何充分利用计算机的全部可用性能。为了加快任务,它将展示如何将任务分配到
用于并行处理的处理器核心。
本书所需内容
要运行本书的示例,您只需要一台至少有 4 GB RAM 的计算机,并安装了 Ubuntu Linux 或 Microsoft Windows 操作系统。我们将使用的所有程序和库要么是免费的,要么是开源的。
本书面向对象
本书旨在为想要处理地理数据的 Python 初学者或高级开发者提供帮助。本书适合新接触地理空间开发的职业开发者、爱好者,或希望进入简单开发的科学家。
约定
在本书中,您将找到多种文本样式,用于区分不同类型的信息。以下是一些这些样式的示例及其含义的解释。
文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称的显示方式如下:“一个 Python 包是一个包含一个或多个 Python 文件(即模块)和一个 init.py 文件的目录。”
代码块设置为如下:
import ogr
第一章:打开 shapefile 并获取第一层。
datasource = ogr.Open("../data/world_borders_simple.shp") layer = datasource.GetLayerByIndex(0)
print("特征数量: {}".format(layer.GetFeatureCount())) 当我们希望引起您对代码块中特定部分的注意时,相关的行或项目将被设置为粗体:
if name == 'main':
gdal.PushErrorHandler('CPLQuietErrorHandler')
vector_data = PointCollection("../data/geocaching.gpx") vector_data.print_information()
任何命令行输入或输出都写成如下形式:
收集 django
下载 Django-1.9-py2.py3-none-any.whl (6.6MB)
100% |################################| 6.6MB 43kB/s 安装收集的包:django
成功安装 django-1.9
新术语和重要词汇将以粗体显示。屏幕上看到的单词,例如在菜单或对话框中,在文本中显示如下:“通过点击下一步按钮继续使用默认选项。”
注意
警告或重要注意事项将以如下框的形式出现。
提示
小技巧和窍门看起来像这样。
读者反馈
我们欢迎读者的反馈。请告诉我们您对本书的看法——您喜欢或不喜欢的地方。读者反馈对我们非常重要,因为它帮助我们开发出您真正能从中受益的标题。
要向我们发送一般反馈,只需发送电子邮件到feedback@packtpub.com,并在邮件主题中提及书籍的标题。
如果您在某个主题上具有专业知识,并且您对撰写或为书籍做出贡献感兴趣,请参阅我们的作者指南www.packtpub.com/authors。
客户支持
现在您是 Packt 书籍的骄傲拥有者,我们有多个方法可以帮助您从您的购买中获得最大收益。
下载示例代码
您可以从www.packtpub.com的账户下载示例代码文件。
为您购买的所有 Packt Publishing 书籍。如果您在其他地方购买了这本书,您可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给您。
下载本书的颜色图像 我们还为您提供了一个包含本书中使用的截图/图表颜色图像的 PDF 文件。这些颜色图像将帮助您更好地理解输出中的变化。您可以从
www.packtpub.com/sites/default/files/downloads/GeospatialDevelopmentByExampleWithPython_ColorImages.pdf
勘误
尽管我们已经尽最大努力确保内容的准确性,但错误仍然可能发生。如果您在我们的书中发现错误——可能是文本或代码中的错误——如果您能向我们报告这一点,我们将不胜感激。通过这样做,您可以避免其他读者的挫败感,并帮助我们改进本书的后续版本。如果您发现任何勘误,请通过访问www.packtpub.com/submit-errata,选择您的书籍,点击勘误提交表单链接,并输入您的勘误详情来报告它们。一旦您的勘误得到验证,您的提交将被接受,勘误将被上传到我们的网站或添加到该标题的勘误部分现有的勘误列表中。
要查看之前提交的勘误,请访问
www.packtpub.com/books/content/support并在搜索字段中输入书籍名称。所需信息将出现在勘误部分下。
盗版
互联网上对版权材料的盗版是一个跨所有媒体的持续问题。在 Packt,我们非常重视我们版权和许可证的保护。如果您在互联网上发现任何形式的非法副本,请立即向我们提供位置地址或网站名称,以便我们可以寻求补救措施。
请通过copyright@packtpub.com与我们联系,并提供涉嫌盗版材料的链接。
我们感谢您在保护我们的作者以及为我们提供有价值内容的能力方面的帮助。
问题
如果您在本书的任何方面遇到问题,您可以联系
联系邮箱>, 我们将尽力解决问题。
第一章. 准备工作
环境
将编程语言作为地理处理工具使用提供了构建个性化应用程序的机会,该应用程序可以更优化地执行用户所需的任务。这意味着重复性任务可以自动化,文件输入输出可以定制,并且过程可以调整以执行您想要完成的精确操作。
Python 是一种强大的编程语言,作为地理处理和科学分析的工具有着特殊的关注度。许多因素可能促成了它的普及,其中三个值得提及:它是一种脚本语言,它灵活且易于学习,并且它拥有广泛的库作为开源软件可用。
可用的库和软件包数量允许用户在编程基本功能上花费更少的时间,更多的时间用于构建流程和工作流以达到他们的目标。
在本章中,我们将介绍安装所有您将需要通过示例使用的库的过程;这些相同的库也可能满足您在现实世界应用中的大部分需求。然后,我们将设置一个 集成 开发环境 (IDE),这将有助于组织代码并避免错误。
最后,我们将使用其中一个库编写一个示例程序。因此,以下是我们将要涉及的主题:
安装 Python 以及本书示例所需的软件包
安装 IDE 以编写和组织您的代码
为本书创建项目
编写您的第一段代码
安装 Python
对于本书,我们建议使用 Python 2.7;这个版本的 Python 与我们将要在示例中使用的库和包完全兼容,并且对于 Windows 用户,互联网上还有可用的预编译二进制文件。我们将尽可能保持所有示例与 Python 3.4 兼容,以便于将来的升级迁移。
Windows 用户可能会发现 64 位软件包存在兼容性问题,因此我们建议他们使用 32 位版本的 Python。
对于 Linux 用户,我们将展示 Ubuntu Linux 分发的安装程序,并使用软件包管理器,这样您就不必担心版本和需求;软件包管理器会为您处理这些。
您将要安装的库是用 Python 和其他语言编写的,最常见的是 C 和 C++。这些库可以将类、方法和函数抽象为 Python 对象,或者有一个额外的层来建立连接;当这种情况发生时,我们说该库有Python 绑定。
Windows
在 Windows 上安装 Python 的步骤如下:1. 访问www.python.org/downloads/windows/并点击下载 Windows 的最新 Python 2.7 版本。
-
在下一页,向下滚动,您将找到一个文件列表;请确保您下载Windows x86 MSI 安装程序。
-
文件下载完成后,通过点击文件并按照说明打开安装程序。通过点击下一步按钮继续使用默认选项。
Ubuntu Linux
Ubuntu 已经预装了 Python,因此无需安装。如果由于任何原因它不可用,您可以使用以下命令进行安装:sudo apt-get install python
Python 2.7.9 自带 Pip,但如果您使用的是较旧版本,则需要使用以下命令安装 Pip:
sudo apt-get install python-pip
Python 包和包管理器
一个 Python 包是一个包含一个或多个 Python 文件(即模块)和一个 init.py 文件的目录(这可以是一个空文件)。此文件告诉 Python 解释器该目录是一个包。
在编写 Python 代码时,我们可以导入包和模块并在我们的代码中使用它们。Python 社区经常这样做;许多包使用其他包,等等,形成一个复杂的依赖关系网络。
为了方便安装包及其运行所需的所有依赖项,Python 有一个名为 pip 的包管理器。
Pip 会在中央仓库(或用户定义的位置)中查找包,然后下载它,接着下载其依赖项,并安装它们。一些包还使用其他语言的库,例如 C。在这些情况下,这些库需要在安装期间编译。Ubuntu 用户不会遇到这个问题,因为系统上已经安装了许多编译器,但在 Windows 上默认情况下不会这样做。
Windows 的 Python 包仓库 Python 通过 pip 使安装库和包变得容易。然而,由于 Windows 默认不包含任何编译器,需要编译库的包的安装会失败。为了避免安装编译器的过程,这超出了本书的范围,我们可以获取准备好使用的包。
这些包为各种类型的系统预先构建,不需要编译其库。这种类型的包被称为 wheel。
注意
Christoph Gohlke 通过构建这些包并将它们提供下载,为我们大家做了件好事,下载地址为 www.lfd.uci.edu/~gohlke/pythonlibs/.。
安装包和所需软件
在本主题中,我们将介绍书中使用的每个包的安装过程。
OpenCV
OpenCV 是一个针对视频和图像处理的优化 C/C++ 库,拥有从简单的图像缩放到对象识别、人脸检测等功能。OpenCV 是一个庞大的库,我们将使用其读取、转换和写入图像的功能。它是一个不错的选择,因为其开发活跃,拥有庞大的用户社区和非常好的文档。
Windows
这里是 Windows 的安装步骤:
-
按 Ctrl + F 打开浏览器搜索对话框,然后搜索 OpenCV。
-
你将找到一个文件列表;选择 opencv_python-2.4.11-cp27-none-win32.whl 或任何包含 cp27 和 win32 的 OpenCV 版本。这意味着这是 Python 2.7 的 32 位版本。
-
将下载的文件保存到已知位置。
-
打开 Windows 命令提示符并运行以下命令:c:\Python27\scripts\pip install path_to_the_file_you_downloaded.whl 6. 你应该会看到一个输出告诉你安装成功,如下所示:Processing c:\downloads\opencv_python-2.4.12-cp27-none-win32.whl Installing collected packages: opencv-python
Successfully installed opencv-python-2.4.12
提示
你可以将文件拖放到命令提示符中,以输入其完整路径。
Ubuntu Linux
这里是 Ubuntu Linux 的安装过程:
-
使用 Ctrl + T 打开一个新的终端。
-
然后,输入以下命令:
sudo apt-get install python-opencv
安装 NumPy
NumPy 是一个用于科学计算的 Python 包。它以非常高效的方式处理多维数组操作。NumPy 是 OpenCV 运行所必需的,并将被我们在示例中执行的大多数栅格操作所使用。NumPy 也是一个高效的数据容器,并将是我们计算大量图像数据的工具。
Windows
重复安装 OpenCV 的相同步骤;然而,这次,搜索 NumPy 并选择一个名为 numpy-1.9.2+mkl-cp27-none-win32.whl 的文件。
Ubuntu Linux
NumPy 在 Ubuntu 上作为 OpenCV 的依赖项自动安装,但如果你想在没有 OpenCV 的情况下安装它,请按照以下步骤操作:
-
使用 Ctrl + T 打开一个新的终端。
-
然后,输入以下命令:
sudo pip install numpy
安装 GDAL 和 OGR
GDAL(地理空间数据抽象库)由两个结合在一起的包组成:OGR 处理地理空间矢量文件格式,包括坐标系转换和矢量操作。GDAL 是库的栅格部分,在 1.11 版本中,它包含了 139 个驱动程序,可以读取,其中一些甚至可以创建栅格。
GDAL 还包含用于栅格转换和计算的函数,例如调整大小、裁剪、重新投影等。
在以下表中,列出了 GDAL 和 OGR 驱动程序列表的摘录,这些驱动程序支持您可能找到的最常见的格式:
长格式名称
代码
创建
Arc/Info ASCII Grid
AAIGrid
是
Arc/Info 导出 E00 GRID
E00GRID
否
ENVI .hdr 标签化栅格
ENVI
是
通用二进制 (.hdr 标签化)
GENBIN
否
Oracle Spatial GeoRaster
GEORASTER
是
GSat 文件格式
GFF
否
图像交换格式 (.gif)
GIF
是
GMT 兼容的 netCDF
GMT
是
GRASS ASCII Grid
GRASSASCIIGrid 否
Golden Software ASCII Grid
GSAG
是
Golden Software 二进制网格
GSBG
是
Golden Software Surfer 7 二进制网格
GS7BG
是
TIFF / BigTIFF / GeoTIFF (.tif)
GTiff
是
GXF (网格交换文件)
GXF
否
Erdas Imagine (.img)
HFA
是
JPEG JFIF (.jpg)
JPEG
是
NOAA 极地轨道器 1b 数据集(AVHRR) L1B
否
NOAA NGS 地球椭球高度网格
NGSGEOID
否
NITF
NITF
是
NTv2 基准网格偏移
NTv2
是
PCI .aux 标签化
PAux
是
PCI Geomatics 数据库文件
PCIDSK
是
PCRaster
PCRaster
是
地理空间 PDF
是
NASA 行星数据系统
PDS
否
可移植网络图形 (.png)
PNG
是
R 对象数据存储
R
是
栅格矩阵格式 (*.rsw, .mtw)
RMF
是
RadarSat2 XML (product.xml)
RS2
否
Idrisi 栅格
RST
是
SAGA GIS 二进制格式
SAGA
是
USGS SDTS DEM (*CATD.DDF)
SDTS
否
SGI 图像格式
SGI
是
SRTM HGT 格式
SRTMHGT
是
Terragen 高程场 (.ter)
TERRAGEN
是
USGS ASCII DEM / CDED (.dem)
USGSDEM
是
ASCII 格网 XYZ
XYZ
是
以下表格描述了 OGR 驱动程序:
格式名称
代码
创建
Arc/Info 二进制覆盖
AVCBin
否
Arc/Info .E00 (ASCII) 覆盖 AVCE00
否
AutoCAD DXF
DXF
是
逗号分隔值 (.csv) CSV
是
ESRI Shapefile
ESRI Shapefile 是
GeoJSON
GeoJSON
是
Géoconcept 导出
Geoconcept
是
GeoRSS
GeoRSS
是
GML
GML
是
GMT
GMT
是
GPSBabel
GPSBabel
是
GPX
GPX
是
GPSTrackMaker (.gtm, .gtz)
GPSTrackMaker
是
水文传输格式
HTF
否
Idrisi 向量 (.VCT)
Idrisi
否
KML
KML
是
Mapinfo 文件
MapInfo 文件
是
Microstation DGN
DGN
是
OpenAir
OpenAir
否
ESRI FileGDB
OpenFileGDB
否
PCI Geomatics 数据库文件
PCIDSK
是
地理空间 PDF
是
PDS
PDS
否
PostgreSQL SQL 转储
PGDump
是
美国人口普查 TIGER/Line
TIGER
否
注意
您可以在gdal.org/python/找到完整的 GDAL 和 OGR API 文档和驱动程序完整列表。
Windows
再次,我们将使用轮子进行安装。重复之前的相同步骤:1. 访问www.lfd.uci.edu/~gohlke/pythonlibs/。
- 现在,搜索 GDAL 并下载名为
GDAL-1.11.3-cp27-none-win32.whl。
- 最后,使用 pip 安装它,就像我们之前做的那样。
Ubuntu Linux
执行以下步骤:
-
前往终端或打开一个新的终端。
-
然后,输入以下命令:
sudo apt-get install python-gdal
安装 Mapnik
Mapnik 是一个地图渲染包。它是一个用于开发地图应用的免费工具包。它生成高质量的地图,并被用于许多应用中,包括 OpenStreetMaps。
Windows
Mapnik 与其他库不同,不可直接安装。相反,你需要前往
mapnik.org/并按照下载链接:1. 下载 Mapnik 2.2 的 Windows 32 位包。
-
将 mapnik-v2.2.0 解压到 C:\文件夹。
-
然后,将解压的文件夹重命名为 c:\mapnik。
-
现在,将 Mapnik 添加到你的PATH。
-
打开控制面板并转到系统。
-
点击左侧列中的高级系统设置链接。
-
在系统属性窗口中,点击高级选项卡。
-
接下来,点击环境变量按钮。
-
在系统变量部分,选中PATH变量并点击编辑。将以下路径添加到列表末尾,每个路径之间用分号分隔,如下所示:c:\mapnik\bin;c:\mapnik\lib
-
现在,点击新建按钮;然后,将变量名设置为 PYTHONPATH,值设置为 c:\mapnik\python\2.7\site-packages。
Ubuntu Linux
对于此操作,请执行以下步骤:
-
前往终端或打开一个新的终端。
-
然后,输入以下命令:
sudo apt-get install mapnik
安装 Shapely
Shapely 是一个用于二维几何操作和分析的包。它可以执行诸如几何的并集和减法等操作。它还可以执行测试和比较,例如当几何体与其他几何体相交时。
Windows
这里你需要做的是:
-
如前所述,下载预构建的 wheel 文件;这次,寻找一个名为 Shapely-1.5.13-cp27-none-win32.whl 的文件。
-
然后,使用 pip 安装它。
Ubuntu Linux
这里你需要执行的步骤是:
-
前往终端或使用Ctrl + T打开一个新的终端。
-
输入以下命令:
sudo pip install shapely
直接从以下位置安装其他包
pip
一些包不需要编译步骤。对于 Windows 用户来说,这些包更容易安装,因为它们可以直接通过 pip 使用单个命令获取和安装。
Windows
你只需要在命令提示符中输入以下命令:c:\Python27\scripts\pip install django tabulate requests xmltodict psycopg2
Ubuntu Linux
在终端中,输入以下命令:
sudo pip install django tabulate requests xmltodict psycopg2
对于每个包,你应该能看到安装的进度,类似于以下内容:收集 django
下载 Django-1.9-py2.py3-none-any.whl (6.6MB)
100% |################################| 6.6MB 43kB/s 安装收集的包:django
成功安装 django-1.9
安装 IDE
IDE 是带有工具和编程语言检查的时尚文本编辑器。
你当然可以使用你偏好的任何文本编辑器或 IDE;本书中的所有任务都不依赖于 IDE,但 IDE 将极大地简化我们的工作,因为建议的配置可以帮助你避免错误并节省在输入、运行和调试代码上的时间。IDE 会为你检查代码并检测潜在的错误;它甚至可以猜测你正在输入的内容并为你完成语句,通过简单的命令运行代码,如果出现异常,它还会提供指向异常发生位置的链接。对于 Windows 或 Linux,请访问www.jetbrains.com/pycharm/
点击大橙色的按钮立即获取 Pycharm。在下一页,选择免费社区版。
Windows
这里是你需要执行的步骤:
-
下载完成后,打开下载的文件;安装向导将会弹出。
-
点击下一步,在安装选项中,勾选两个复选框:创建 桌面快捷方式和创建关联。
-
点击下一步继续安装。
Linux
执行以下步骤:
-
在一个目录中解压下载的文件。
-
要打开 PyCharm,从 bin 子目录运行 pycharm.sh。如果你愿意,可以为其创建快捷方式。
提示
下载示例代码
你可以从你购买的所有 Packt 书籍的账户中下载示例代码文件。www.packtpub.com。如果你在其他地方购买了这本书,你可以访问www.packtpub.com/support并注册以直接将文件通过电子邮件发送给你。

创建书籍项目
执行以下步骤:
-
安装完成后,打开 Pycharm,你将被提示创建你的第一个项目:
-
点击创建新项目,然后选择 c:\geopy 作为你的项目位置。在 Linux 中,你可以将项目放在你的家目录中——例如,
/home/myname/geopy. 点击创建以创建项目。

- 在 Windows 中,你会收到一个安全警报;这是 Pycharm 试图访问互联网。建议你允许它,这样你以后可以检查更新或下载插件:


- 最后,你应该在你的项目工作区看到以下窗口。花点时间探索菜单和按钮,尝试在你的项目文件夹上右键点击以查看选项:
编写和运行你的第一个
示例
现在我们已经安装了所有需要的软件,我们将通过第一个示例进行测试。在这个示例中,我们将测试安装,然后看看 OGR 功能的一瞥。
为了做到这一点,我们将打开一个包含世界上所有国家边界的矢量文件,并创建一个国家名称列表。这个简单示例的目的是展示 OGR 对象和函数背后的逻辑,并了解地理空间文件是如何表示的。以下是方法:
- 首先,你需要将书中提供的样本数据复制到你的项目文件夹中。
你可以通过将数据文件夹拖放到 geopy 文件夹中来实现这一点。确保数据文件夹命名为 data,并且它位于 geopy 文件夹内。
-
现在,在 PyCharm 中为此章节代码创建一个新的目录。在打开 geopy 项目的情况下,右键单击项目文件夹并选择 新建 | 目录。将其命名为 Chapter1\。
-
创建一个新的 Python 文件。为此,右键单击 Chapter1 文件夹并选择 新建 | Python 文件。将其命名为 world_borders.py,PyCharm 将自动打开文件进行编辑。
-
在此文件中输入以下代码:
import ogr
打开 shapefile 并获取第一层。
datasource = ogr.Open("../data/world_borders_simple.shp") layer = datasource.GetLayerByIndex(0)
print("特征数量: {}".format(layer.GetFeatureCount())) 5. 现在,运行代码;在菜单栏中,导航到 运行 | 运行,并在对话框中选择 world_borders。屏幕底部将打开一个输出控制台,如果一切顺利,你应该看到以下输出:
C:\Python27\python.exe C:/geopy/world_borders.py
特征数量:246
进程已正常结束,退出代码为 0
恭喜!你成功打开了 Shapefile 并计算了其中的特征数量。现在,让我们了解这段代码的作用。
第一行导入 ogr 包。从这一点开始,所有函数都可以作为 ogr.FunctionName() 使用。请注意,ogr 不遵循 Python 的函数命名约定。
注释之后的行打开 OGR 数据源(这打开了包含数据的 shapefile)并将对象分配给数据源变量。请注意,路径,即使在 Windows 上,也使用正斜杠 (/) 而不是反斜杠。
下一行通过索引(0)获取数据源的第一层。某些数据源可以有多个层,但 Shapefile 的情况并非如此。所以,当处理 Shapefile 时,我们始终知道感兴趣的层是层 0。
有许多层,但 Shapefile 的情况并非如此,它只有一个层。因此,当处理 Shapefile 时,我们始终知道感兴趣的层是层 0\。
在最后一行,print 语句打印出 layer.GetFeatureCount() 返回的特征数量。在这里,我们将使用 Python 的字符串格式化,其中大括号将被传递给 format() 的参数所替换。
现在,执行以下步骤:
- 在同一文件中,让我们输入我们程序的下一部分:
检查层中可用的字段。
feature_definition = layer.GetLayerDefn()
for field_index in range(feature_definition.GetFieldCount()): field_definition = feature_definition.GetFieldDefn(field_index) print("\t{}\t{}\t{}".format(field_index,
field_definition.GetTypeName(),
field_definition.GetName()))
- 重新运行代码;你可以使用 Shift + F10 快捷键。现在,你应该看到要素数量与之前相同,并且显示 shapefile 中所有字段信息的漂亮表格,如下所示:
要素数量:246
0 字符串 FIPS
1 字符串 ISO2
2 字符串 ISO3
3 整数 UN
4 字符串 NAME
5 整数 POP2005
6 整数 REGION
7 整数 SUBREGION
进程结束,退出代码 0
这段代码中发生的事情是 feature_definition =
layer.GetLayerDefn() 获取包含要素定义的对象。
此对象包含每个字段的定义和几何类型。
在 for 循环中,我们将获取每个字段定义并打印其索引、名称和类型。
注意,layer.GetLayerDefn() 返回的对象不可迭代,我们不能直接使用它。因此,首先,我们将获取字段数量,并在 range() 函数中使用它,以便我们可以遍历字段的索引:3. 现在,输入最后一部分,如下所示:
打印国家名称列表。
layer.ResetReading()
for feature in layer:
print(feature.GetFieldAsString(4))
- 再次运行代码并检查输出结果中的数量:要素数量:246
0 字符串 FIPS
1 字符串 ISO2
2 字符串 ISO3
3 整数 UN
4 字符串 NAME
5 整数 POP2005
6 整数 REGION
7 整数 SUBREGION
安提瓜和巴布达
阿尔及利亚
阿塞拜疆
阿尔巴尼亚
亚美尼亚
安哥拉
...
圣巴泰勒米
根西岛
泽西岛
南乔治亚和南桑威奇群岛
台湾
进程结束,退出代码 0
图层是可迭代的,但首先,我们需要确保我们位于图层列表的起始位置,使用 layer.ResetReading()(这是 OGR 的“陷阱”之一)。
The feature.GetFieldAsString(4) 方法返回字段 4 的值,作为一个 Python 字符串。有两种方式可以知道国家名称是否在字段 4 中:查看数据的 DBF 文件(通过使用 LibreOffice 或 Excel 打开)查看我们在代码第一部分打印的表格。你的完整代码应类似于以下内容:
import ogr
打开 shapefile 并获取第一个图层。
datasource = ogr.Open("../data/world_borders_simple.shp") layer = datasource.GetLayerByIndex(0)
print("Number of features: {}".format(layer.GetFeatureCount()))
检查图层中可用的字段。
feature_definition = layer.GetLayerDefn()
for field_index in range(feature_definition.GetFieldCount()): field_definition = feature_definition.GetFieldDefn(field_index) print("\t{}\t{}\t{}".format(field_index,
field_definition.GetTypeName(),
field_definition.GetName()))
打印国家名称列表。
layer.ResetReading()
for feature in layer:
print(feature.GetFieldAsString(4))
转换坐标系和
计算所有国家的面积
现在,目标是了解每个国家占用的面积。然而,国家边界的坐标是以纬度和经度表示的,我们无法在这个坐标系中计算面积。我们希望面积在公制系统中,因此首先需要将几何形状的空间参考系统进行转换。
让我们在编程技术方面更进一步,开始使用函数来避免代码重复。执行以下步骤:
- 在 Chapter1 目录中创建一个新文件,将此文件命名为 world_areas.py,并编写这个第一个函数:
import ogr
def open_shapefile(file_path):
"""打开 shapefile,获取第一层并返回 ogr 数据源。
在单词之间。一个命名的好提示是遵循动词 _ 名词规则。
datasource = ogr.Open(file_path)
layer = datasource.GetLayerByIndex(0)
print("正在打开 {}".format(file_path))
print("要素数量:{}".format(
layer.GetFeatureCount()))
return datasource
- 运行代码,在菜单中选择运行 | 运行…,然后选择 world_areas。如果一切正常,不应该发生任何事。这是因为我们没有调用我们的函数。
在函数外部添加此行代码:
你已经熟悉了这段代码的工作方式,但这里有几个新特性值得解释。def 语句使用 def function_name(arguments):语法定义了一个函数。
要素数量:246
进程已正常结束
datasource = open_shapefile("../data/world_borders_simple.shp") 3. 现在,再次使用 Shift + F10 运行代码,并检查输出,如下所示:正在打开 ../data/world_borders_simple.shp
"""
记得我告诉过你 OGR 不遵循 Python 的命名约定吗?好吧,约定是函数名应该全部小写,单词之间用下划线分隔。一个命名的好提示是遵循动词 _ 名词规则。
www.python.org/dev/peps/pep-0008/
非常好!你刚刚创建了一段非常有用且可重用的代码。现在,你有一个可以打开任何 shapefile、打印要素数量并返回 ogr 数据源的函数。从现在起,你可以在任何项目中重用这个函数。
这些约定在名为PEP-8的文档中描述,其中PEP代表Python 增强计划。您可以在以下位置找到此文档:
提示
在函数定义之后,您可以看到三引号之间的描述;这是一个文档字符串,用于记录代码。它是可选的,但了解函数的功能非常有用。
现在,让我们回到我们的代码。需要指出的是第二个重要的事情是返回语句。这使得函数返回语句之后列出的变量的值——在这种情况下,是数据源。
非常好!你刚刚创建了一段非常有用且可重用的代码。现在,你有一个可以打开任何 shapefile、打印要素数量并返回 ogr 数据源的函数。从现在起,你可以在任何项目中重用这个函数。
OGR 对象的所有部分都必须通过程序流畅地连接在一起,这非常重要。
在这种情况下,如果我们只返回图层,例如,我们将在程序稍后运行时得到一个运行时错误。这是因为 OGR 内部,图层有一个数据源的引用,当你退出 Python 函数时,所有未退出函数的对象都会被丢弃,这会破坏引用。
现在,下一步是创建一个执行转换的函数。在 OGR 中,转换是在要素的几何上进行,因此我们需要遍历要素,获取几何,并转换其坐标。我们将按照以下步骤进行:1. 将以下函数添加到您的 world_areas.py 文件中,在 open_shapefile 函数之后:
def transform_geometries(datasource, src_epsg, dst_epsg):
"""将第一层中所有几何的坐标进行转换。
"""
第一部分
src_srs = osr.SpatialReference()
src_srs.ImportFromEPSG(src_epsg)
dst_srs = osr.SpatialReference()
dst_srs.ImportFromEPSG(dst_epsg)
transformation = osr.CoordinateTransformation(src_srs, dst_srs) layer = datasource.GetLayerByIndex(0)
第二部分
geoms = []
layer.ResetReading()
for feature in layer:
geom = feature.GetGeometryRef().Clone()
geom.Transform(transformation)
geoms.append(geom)
return geoms
函数接受三个参数: ogr 图层,文件坐标系统的 EPSG 代码,以及转换输出的 EPSG 代码。
在这里,它创建了一个 osr.CoordinateTransformation 对象;此对象包含执行转换的指令。
可能到现在为止,Pycharm 应该会抱怨 osr 是一个未解析的引用;osr 是 GDAL 处理坐标系统的一部分。
- 现在,通过在代码顶部添加此行来导入模块:import osr
在这里,代码遍历所有要素,获取几何引用,并执行转换。由于我们不希望更改原始数据,几何被克隆,转换在克隆上进行。
Python 列表是有序的;这意味着元素是按照它们添加到列表中的顺序排列的,并且这个顺序始终保持不变。这允许我们创建一个几何列表,其顺序与数据源中要素的顺序相同。这意味着列表中的几何和要素具有相同的索引,并且可以通过索引在未来关联。
- 现在,让我们测试代码;在文件末尾添加以下行(第一行是您之前添加的):
datasource = open_shapefile("../data/world_borders_simple.shp") layer = datasource.GetLayerByIndex(0)
feature = layer.GetFeature(0)
print("转换前:")
print(feature.GetGeometryRef())
transformed_geoms = transform_geometries(datasource, 4326, 3395) print("转换后:")
print(transformed_geoms[0])
- 最后,在运行代码之前,在程序开头添加一个额外的导入。它应该是您代码的第一个语句,如下所示:from future import print_function
这个导入允许我们使用 Python 3 的 print() 函数并具有期望的行为,从而保持兼容性。
- 完整的代码应该看起来像这样:
from future import print_function
import ogr
import osr
def open_shapefile(file_path):
...
def transform_geometries(datasource, src_epsg, dst_epsg):
...
datasource = open_shapefile("../data/world_borders_simple.shp") layer = datasource.GetLayerByIndex(0)
feature = layer.GetFeature(0)
print("变换前:")
print(feature.GetGeometryRef())
transformed_geoms = transform_geometries(datasource, 4326, 3395) print("After transformation:")
print(transformed_geoms[0])
- 通过按 Shift + F10 重新运行你的程序。在输出中,注意变换前后的坐标差异:
打开 ../data/world_borders_simple.shp
特征数量:246
变换前:
MULTIPOLYGON (((-61.686668 17.024441000000138… )))
变换后:
MULTIPOLYGON (((-6866928.4704937246… )))
Process finished with exit code 0
- 现在,添加另一个函数。这个函数将计算具有米坐标的几何形状的面积(因为我们将使用具有米坐标的几何形状),将值(或不是)转换为平方公里或英里,并将值存储在另一个列表中,顺序与之前相同。执行以下代码:
def calculate_areas(geometries, unity='km2'):
"""计算 ogr 几何形状列表的面积。”
第一部分
conversion_factor = {
'sqmi': 2589988.11,
'km2': 1000000,
'm': 1}
第二部分
if unity not in conversion_factor:
raise ValueError(
"这个单位未定义:{}".format(unity))
第三部分
areas = []
for geom in geometries:
area = geom.Area()
areas.append(area / conversion_factor[unity])
return areas
首先,请注意,在函数定义中,我们使用 unity='km2';这是一个关键字参数,当你调用函数时,这个参数是可选的。
在第一部分,使用字典定义了一些面积单位的转换系数。
随意添加更多单位,如果需要的话。顺便说一句,Python 不关心你使用单引号还是双引号。
在第二部分,进行验证以检查传递的单位是否存在并且是否在 conversion_factor 中定义。另一种方法是稍后捕获异常;然而,现在我们选择可读性。
在第三部分,代码迭代 ogr 几何形状,计算面积,转换值,并将其放在列表中。
- 现在,为了测试代码,编辑你的第一行,包括将导入语句移到未来。
这将确保所有除法都返回浮点数而不是整数。
它应该看起来像这样:
from future import print_function, division
-
然后,更新您的代码的测试部分,如下所示:datasource = open_shapefile("../data/world_borders_simple.shp") transformed_geoms = transform_geometries(datasource, 4326, 3395) calculated_areas = calculate_areas(transformed_geoms, unity='sqmi') print(calculated_areas)
-
运行它,更改单位,然后再次运行,注意结果如何变化。
很好,单位转换是地理处理中的另一个非常重要的程序,您已经在 calculate_areas 函数中实现了它。
然而,以数字列表作为输出对我们来说并不很有用。因此,现在是时候将我们迄今为止所做的一切结合起来,以便提取有价值的信息。
按面积大小排序国家
你已经编写了三个函数;现在,让我们通过将生成国家名称列表的代码转换为函数并将其添加到 world_areas.py 中,来添加另一个函数到我们的列表中,如下所示:
def get_country_names(datasource):
"""返回一个国家名称列表。"""
layer = datasource.GetLayerByIndex(0)
country_names = []
layer.ResetReading()
for feature in layer:
country_names.append(feature.GetFieldAsString(4))
return country_names
现在,我们有四个函数,它们是:
open_shapefile
transform_geometries
calculate_areas
get_country_names
所有这些函数都返回可迭代对象,每个项目在所有这些对象中共享相同的索引,这使得信息组合变得容易。
因此,让我们利用这个特性按面积大小对国家进行排序,并返回五个最大国家和它们的面积列表。为此,添加另一个函数,如下所示:def get_biggest_countries(countries, areas, elements=5):
"""返回一个按面积大小排序的国家列表。"""
countries_list = [list(country)
for country in zip(areas, countries)]
sorted_countries = sorted(countries_list,
key=itemgetter(0), reverse=True)
return sorted_countries[:5]
在第一行,两个列表被合并在一起,生成一个国家-面积对的列表。
然后,我们使用了 Python 列表的 sorted 方法,但因为我们不希望列表按两个值排序,我们将定义排序的键。最后,列表被切片,只返回所需数量的值。
-
为了运行此代码,您需要导入 itemgetter 函数并将其放在代码的开头,但在 from future 导入之后,如下所示:from operator import itemgetter
-
现在,编辑您的代码的测试部分,使其看起来类似于以下内容:datasource = open_shapefile("../data/world_borders_simple.shp") transformed_geoms = transform_geometries(datasource, 4326, 3395) country_names = get_country_names(datasource)
country_areas = calculate_areas(transformed_geoms)
biggest_countries = get_biggest_countries(country_names,
country_areas) for item in biggest_countries:
print("{}\t{}".format(item[0], item[1]))
- 现在,运行代码并查看结果,如下所示:打开../data/world_borders_simple.shp
特征数量:246
82820725.1423 俄罗斯
51163710.3726 加拿大
35224817.514 格陵兰
21674429.8403 美国
14851905.8596 中国
进程以退出代码 0 完成
总结
在本章中,我们简要介绍了我们将在这本书中使用到的库和包。通过安装这些库,您也学习了如何搜索和安装 Python 包的一般步骤。您可以在需要其他库的情况下使用此过程。
然后,我们编写了利用 OGR 库打开 shapefile 并执行面积计算和排序的代码。这些简单的程序展示了 OGR 的内部组织结构,它如何处理地理数据,以及如何从中提取信息。在下一章中,我们将使用在这里学到的某些技术来读取数据和处理矢量点。
第二章:地理藏宝应用程序
在本章中,我们将构建一个地理藏宝应用程序,它最初将从互联网获取地理藏宝点,并返回用户位置最近的点的坐标和信息。
我们将介绍每个地理处理应用程序中的一些最重要的步骤:我们将讨论打开文件、读取信息、准备数据分析,以及使用数据中的每个对象进行计算。为了实现这一点,您将学习如何使用 Python 组织代码并使用语言提供的资源编写一致的应用程序。
在本章中,我们将开始使用类、方法、函数、装饰器和异常处理,这些将帮助我们构建具有可重用组件和整洁代码的应用程序。如果您对这些术语不熟悉,它们将在示例中解释。简而言之,以下是我们将涵盖的内容:
编程基本应用程序结构
下载地理藏宝数据
打开地理藏宝文件并获取其内容
将函数组合成应用程序
设置您的当前位置
处理异常
寻找最近的点
构建基本应用程序结构 定义我们应用程序的良好基本结构有两个主要原因:它使我们的代码保持组织
它允许我们在后续应用程序中重用代码片段
Python 在代码组织方面是一种灵活的语言,尽管用户被允许在一个文件中编写整个应用程序,但最好是将功能分离成模块和包。
模块是包含可以导入到另一个文件中的类和函数的 Python 文件。包是包含模块的特殊目录(文件夹)。这导致代码组织良好、结构合理,更不容易出现错误,并且更容易维护。
建议的结构是每个章节都有一个文件夹。在其内部,我们可以为每个应用程序创建包或文件;我们将创建一个用于可以导入和重用的通用实用代码的包,以及一个用于进行实验的目录。
创建应用程序树结构 这里是你需要执行的步骤:
如果你已经完成了第一章,准备工作环境,你现在应该有一个名为 geopy 的 PyCharm 项目,在 Windows 中位于 C:\geopy,在 Linux 中位于 ~/geopy。启动 PyCharm 并打开你的项目。
在项目根目录(名为 geopy 的最顶层文件夹),右键单击,选择 新建 |
目录,并将其命名为 Chapter2.
右键单击 Chapter2,选择 新建 | 目录,并将其命名为 experiments。
再次,在 Chapter2 目录内右键单击;这次,选择 新建 | Python 包,并将其命名为 utils。
现在,你应该有一个类似以下的树状结构:
\geopy
+---Chapter1
| world_areas.py
| world_borders.py
|
+---Chapter2
| |
| ---experiments
| ---utils
|
|
+---data
注意
Python 包
包是包含其他包和模块的特殊文件夹。它们是包含特殊文件 init.py 的目录。此文件可能为空,并用于表示该包可以用 import 语句导入。
例如,如果我们有一个名为 foo 的目录(包含 init.py 文件)并且我们在其中创建了一个 bar.py 文件,我们可以在代码中稍后使用 import foo.bar 或 from foo import bar。
函数和方法
函数和方法(类内部的函数)应该简洁,这样当你调用它们时,你可以相信你会得到期望的结果或适当的异常。程序员不希望在每次使用函数时都检查函数的内容;他们希望调用它并得到预期的结果,这被称为信仰跳跃。例如,在这本书中,我们使用了多个外部包;当我们使用包的给定函数时,我们信任这个函数会做它应该做的事情或引发错误告诉我们出了什么问题。除此之外的一切都称为意外行为,这是应用程序可能具有的最危险的错误类型,因为它在代码中静默传递,但后来有后果。
提示
到目前为止,我们看到了一个可能具有这种意外行为的模块:GDAL/OGR。
即使文件不存在,ogr.Open() 函数也会静默传递,除非我们明确告诉 OGR 我们希望它为我们引发异常。
记录你的代码
随着应用程序规模的扩大,跟踪每段代码的功能非常重要。这可以防止程序员重复代码,并在以后节省大量时间来查找发生了什么。此外,它还允许其他人使用和改进你的代码。有两种关键的工具可以用来记录代码,我们已经在第一章中简要介绍过:
代码注释:这些是用#符号插入代码中的注释。
从此符号到下一行的所有内容都是注释,并且在程序运行时将被忽略。Python 语法直观,良好的代码需要少量注释。以下有两个简洁注释代码的技巧:在每个逻辑代码块之前放置注释,说明它在做什么;注释难以阅读或理解的代码片段
文档字符串:文档字符串是放置在文档类、函数和方法特殊位置的文本。它们有特殊的意义,因为它们可以被某些程序解释并用于向用户提供帮助和自动生成文档。文档字符串还可以用来测试你的代码,但这本书不会涉及这一点。在 PyCharm 中,文档字符串有特殊的作用,并提供自动代码检查的提示。在文档字符串中,你可以指定参数和返回类型(例如,字符串、列表和字典)。PyCharm 使用这些信息来提供自动完成建议并警告你可能的错误。
提示
在这本书中,我们将使用 reStructuredText 类型的标记来编写文档字符串;你可以在docutils.sourceforge.net/rst.html找到更多信息。
在以下示例中,你可以注意到一个使用文档字符串进行文档化的类和方法(你不需要输入此代码):
class MyClass:
"""这是一个类的文档字符串示例。”
def init(self):
"""你还可以在 init 方法中放置文档字符串。”
pass
def sum_values(self, arg1, arg2):
"""这是方法的文档字符串,你可以描述参数并指定其类型。
如果你这样做,PyCharm 将使用这些信息
用于自动完成和检查代码。
:参数 float arg1: 第一个参数。
:参数 float arg2: 第二个参数。
:返回 float: 参数的总和。
"""
return arg1 + arg2
创建应用程序入口点
应用程序的入口点是当你运行程序时首先执行的部分。在 Python 中,它是第一行代码。我们可以编写一个自上而下运行的程序,将函数和类声明与其他语句混合,但这会使代码更难开发和调试,尤其是在代码变得复杂时。如果我们明确地显示程序开始的地方,并且从这个点开始,程序的不同部分将根据需要被调用,那会更好。
让我们做一个实验来理解一些关于代码执行和模块导入的要点:
1. 在你的 Chapter2/experiments 文件夹内,创建两个新的文件,命名为 import_test.py 和 module_test.py。为此,在 experiments 文件夹内右键单击并选择新建 | Python 文件。
2. 双击 module_test.py 以打开它进行编辑。
3. 现在,输入以下代码:
coding=utf-8
print "我是 module_test.py,我的名字是: " + name
def function1():
print "Hi, I'm inside function1."
print "调用 function1…"
function1()
每个模块都包含一个 name 属性,我们将在测试代码的第一行打印其值。
接下来,我们将声明一个函数,当调用时,将"Hi, I'm inside function1."打印到输出。
最后,我们将打印出函数将被调用,然后我们将调用 function1.
4. 运行代码,按Alt + Shift + F10并从列表中选择 module_test。查看输出:
我是 module_test.py,我的名字是: main
调用 function1…
Hi, I'm inside function1.
进程以退出代码 0 结束
注意
注意这里,name 属性等于 main;这是 Python 中的一个特殊条件。运行的模块(文件)总是被调用为 main。
5. 要了解更多关于这个机制的信息,在 experiments 文件夹内创建一个新的 Python 文件,命名为 import_test.py,并打开它进行编辑。现在,输入以下代码:
coding=utf-8
print "我是 import_test.py,我的名字是: " + name
print "导入 module_test"
import module_test
print "从 import_test 中调用 function1"
module_test.function1()
6. 现在,运行 import_test.py(按Alt + Shift + F10并从列表中选择它)并查看以下输出:
我是 import_test.py,我的名字是: main
导入 module_test
我是 module_test.py,我的名字是: module_test
调用 function1…
Hi, I'm inside function1.
从 import_test 中调用 function1
Hi, I'm inside function1.
进程以退出代码 0 结束
这次是 import_test 被调用为 main,因为它是被执行的文件。接下来,当我们导入 module_test 时,其中的代码将被执行。注意,module_test 不再被调用为 main;它被调用为 module_test。
这个特殊的 name 属性的行为允许我们在 Python 中实现一种技术,这反过来又允许我们在文件直接运行时执行一些代码,并避免在导入此文件时执行此代码。让我们看看它是如何工作的:7. 编辑 module_test.py 并更改其代码,如下所示:
coding=utf-8
print "我是 module_test.py,我的名字是: " + name
def function1():
print "Hi, I'm inside function1."
if name == 'main':
print "调用 function1 - 只有如果我是 main…"
function1()
所以,如果 name 等于'main',则这个块内的代码将被执行,
我们知道,只有当文件直接执行时,name 才等于 main。
因此,这个块内的代码只有在文件被运行时才会执行,而不是在导入时执行。
8. 接下来,再次运行 import_test.py(要重新运行最后一个文件,请按Shift + F10),看看会发生什么:
我是 import_test.py,我的名字是:main
导入 module_test
我是 module_test.py,我的名字是:module_test
在 import_test 中调用 function1
Hi, I'm inside function1.
进程已结束,退出代码为 0
9. 现在,运行 module_test.py(要选择要运行的文件,请按Alt + Shift + F10)并查看输出:
我是 module_test.py,我的名字是:main
只有当我是 main 时调用 function1 - ...
Hi, I'm inside function1.
进程已结束,退出代码为 0
如预期,当直接运行 module_test.py 时,if name == 'main': 块内的代码才会运行,而不是当它被导入时。
现在我们知道了如何在 Python 中显式创建入口点,让我们为应用程序创建第一个文件并创建一个入口点。
10. 在你的 Chapter2 文件夹内创建一个新文件,并将其命名为 geocaching_app.py。
11. 然后,打开文件进行编辑并插入以下代码片段:
coding=utf-8
def main():
print("Hello geocaching APP!")
if name == "main":
main()
main()函数的目的是接收初始参数,然后采取行动,以便程序执行并产生预期的结果。main 函数的内容应尽可能简单,并尝试表达一系列清晰的动作。这使得应用程序的逻辑非常容易调试。
提示
对于 Windows 用户,if name == 'main'技术也是必需的,以便并行处理能够工作;我们将在第十章中讨论这一点,并行处理。
下载 Geocaching 数据
我们现在有了基本的应用程序结构,具有一个入口点;接下来,我们将开始编写执行应用程序需要产生预期结果的任务的模块。
我们首先需要从互联网上获取一些 Geocaching 数据,我们希望我们的应用程序为我们完成这项工作。有两种常见的方法可以做到这一点,并且它们不仅限于 Geocaching 数据。许多地理数据存储库可以通过这些方法访问:
直接下载:这是一种类似于在浏览器中进行的下载。有一个链接,向该链接发出请求,然后开始下载。
REST API:许多服务提供这种数据访问方式。REST(表征状态转移)是一种服务数据的方式,其中客户端通过一系列约束发出请求,服务器响应结果。它特别有用,因为它允许用户自定义感兴趣的数据。
Geocaching 数据源
互联网上有许多 Geocaching 数据来源;有些是商业的,有些是社区驱动的。在下表中,你可以注意一些可用来源的摘要:
站点
REST
区域
Y (OKAPI) 开放
美国
Y (OKAPI) Open
波兰
Y (OKAPI) Open
丹麦
Y (OKAPI) Open
荷兰
Y (OKAPI) Open
罗马尼亚
N
Open
意大利
Open
西班牙
N
Open
英国
N
Open
捷克共和国
商业全球
注意
OKAPI 是一个面向国家 Opencaching 网站(也称为Opencaching 节点)的公共 API 项目。
它为 OC 网站提供了一套有用且文档齐全的 API 方法,允许外部开发者轻松读取公共 Opencaching 数据,并允许我们使用 OAuth 3-legged 认证读取和写入私有(即与用户相关的)数据。该项目旨在成为所有国家 Opencaching.xx 网站的标准化 API。
(http://opencaching.pl/okapi/introduction.html)
从 REST API 获取信息
我们将进行一个简单的测试,从地理藏宝的 REST API 获取数据。这次我们不会深入探讨与 REST API 的通信,因为所有地理藏宝网站都需要一个用户密钥,以便用户可以访问数据;这是为了避免滥用和误用。目前,我们将简要了解其工作原理,并请求一个不需要密钥的方法。
如果你对访问下载功能感兴趣,你可以联系网站并请求一个密钥。以下是你可以这样做的步骤:
-
在你的 Chapter2/utils 目录内创建一个新文件,命名为 data_transfer.py。
-
在文件中输入此代码:
coding=utf-8
from pprint import pprint
import requests
def request_api_methods():
result = requests.get(
"http://www.opencaching.us/okapi/services/apiref/method_index") pprint(result.json())
if name == "main":
request_api_methods()
- 运行此文件,按Alt + Shift + F10,并在列表中选择 rest_api。现在,看看结果:
[{u'brief_description': u'检索有关给定问题的信息', u'name': u'services/apiref/issue'},
{u'brief_description': u'获取有关给定 OKAPI 服务方法的信息',
u'name': u'services/apiref/method'},
{u'brief_description': u'获取带有简要描述的 OKAPI 方法列表',
u'name': u'services/apiref/method_index'},
{u'brief_description': u'获取此 OKAPI 安装的信息', u'name': u'services/apisrv/installation'},
...
{u'brief_description': u'检索有关单个用户的信息', u'name': u'services/users/user'},
{u'brief_description': u'检索有关多个用户的信息', u'name': u'services/users/users'}]
进程以退出代码 0 结束
您看到的 URL 旨在检索包含 API 公开的所有方法描述的列表。requests 模块使我们的一切变得容易,并且
result.json() 方法将我们的请求结果转换为 Python 对象(字典列表)并 pprint(即,美化打印)逐行打印列表。请注意,我们使用了 if name == 'main':,这样我们就可以测试我们的函数;稍后,当这个函数被其他模块导入时,所有在 if name == 'main':之后的代码将不会运行,因此我们可以安全地将所有测试放在那里。
'main': 不会运行,因此我们可以安全地将所有测试放在那里。
从 URL 下载数据
为了避免在地理藏宝网站上的 API 密钥限制,并使我们的示例保持连贯,我们准备了一些可以直接从链接下载的样本数据。您编写的函数将从给定的 URL 下载文件并将其保存到磁盘。这个函数将被通用化,并可能在未来的其他应用程序中使用。我们希望以下内容作为参数(即,参数)传递:
文件的 URL 或链接
目标文件夹的路径
重写文件名。
执行以下步骤:
- 在 data_transfer.py 文件中,添加 download_data 函数并编辑 if name == 'main'块。您的代码应类似于以下内容:
coding=utf-8
from pprint import pprint
import requests
from os import path
def request_api_methods():
result = requests.get(
"http://www.opencaching.us/okapi/services/apiref/method_index") pprint(result.json())
def download_data(base_url, data_path, data_filename):
save_file_path = path.join(data_path, data_filename)
request = requests.get(base_url, stream=True)
将下载保存到磁盘。
with open(save_file_path, 'wb') as save_file:
for chunk in request.iter_content(1024):
save_file.write(chunk)
if name == "main":
download_data('https://s3.amazonaws.com/geopy/geocaching.gpx',
'../../data',
'geocaching_test.gpx')
- 现在,运行代码并检查您的数据目录;那里应该有一个名为 geocaching_test.gpx 的新文件。
函数中发生的事情是,首先,我们使用 os.path 函数准备 save_file_path 变量;这个函数负责连接路径并确保结果对每个操作系统都是正确的。每次我们在应用程序中处理路径时,
我们更喜欢使用 os.path。
使用 requests 库,我们可以向所需的 URL 发出请求。可选的 stream=True 参数告诉它我们希望下载以块的形式发生,正如我们请求的那样,而不是一次性将整个文件下载到内存中。这很重要,因为某些文件可能很大,会占用大量内存。
最后,打开一个文件,并将数据块读取和写入磁盘。with 语句也被称为上下文管理器,因为它只使给定的资源(在这种情况下是文件)在块内可用。然后,以每个 1024 字节的数据块读取和写入文件。当程序退出 with 块时,文件会自动关闭,save_file 变量将被删除。
我们不希望每次运行应用程序时都下载文件;这将是一种时间浪费。因此,在下一部分,我们需要实现一个验证,以便在已存在具有选定名称的文件时跳过下载。
修改download_data函数,如下所示:
def download_data(base_url, data_path, data_filename):
save_file_path = path.join(data_path, data_filename)
request = requests.get(base_url, stream=True)
检查文件是否存在。
如果path.isfile(save_file_path):
打印('文件已可用。')
将下载保存到磁盘。
with open(save_file_path, 'wb') as save_file:
for chunk in request.iter_content(1024):
save_file.write(chunk)
现在,再次运行你的应用程序,你应该会看到以下输出警告你文件已被下载:
文件已可用。
进程以退出代码 0 结束

手动下载数据
现在,你可能想选择特定于你所在地区的特定数据;为此,你需要访问一个地理藏宝网站,过滤数据,并手动下载文件。
作为示例,我们将通过从网站下载数据的过程进行说明
www.opencaching.us/ 你不需要账户;只需遵循以下步骤:1. 打开网站。在左侧菜单中,点击寻找藏宝:2. 这将打开一个包含各种字段的页面。首先,使用以下图片中显示的字段选择你的搜索限制因素:


- 接下来,你需要指定一个区域或标准来搜索地理藏宝。有许多选择可供选择,所以滚动页面并查看。你可以使用邮政编码、坐标、州等。让我们按州进行搜索;选择纽约并点击搜索。

- 将出现一个包含结果的列表。滚动到页面底部,你会注意到下载数据的链接。在GPX中选择下载所有页面的缓存。
格式:
打开文件并获取其内容
现在,我们将打开下载的文件并为其处理做准备。这是我们已经在第一章, 准备工作环境中做过的事情,所以我们将复制我们的函数并改进它,以便我们可以在本应用程序以及未来的应用程序中重用它。
这里是我们将要执行的步骤:
-
在
utils目录内创建一个名为geo_functions.py的新文件。 -
打开第一章中的 Preparing the Work Environment 的 world_areas.py 文件,并复制 open_shapefile 函数。然后,将其粘贴到创建的文件中。
-
现在,将函数名改为 open_vector_file,这样它就更有意义了,因为我们将会使用这个函数来打开多种类型的文件。地理藏宝文件不是 shapefile,而是一个 GPX 文件,打开它我们不需要做任何改变。OGR
将为我们处理这个问题。
-
现在,为了使代码有良好的文档记录,将 docstring 改为反映函数的功能。改为类似“打开与 OGR 兼容的矢量文件,获取第一层,并返回 OGR 数据源”的内容。
-
最后,别忘了导入所需的包。你的代码应该看起来像这样:
coding=utf-8
import ogr
import osr
def open_vector_file(file_path):
"""打开一个与 OGR 兼容的矢量文件,获取第一层并返回 OGR 数据源。
:param str file_path: 文件的完整路径。
:return: 返回 OGR 数据源。
"""
datasource = ogr.Open(file_path)
layer = datasource.GetLayerByIndex(0)
print("打开 {}".format(file_path))
print("特征数量: {}".format(
layer.GetFeatureCount()))
return datasource
if name == "main":
open_vector_file("../../data/geocaching.gpx")
- 再次运行代码,你应该会看到以下输出(不用担心警告信息):
打开 ../data/geocaching.gpx
警告 1:无法解析 {2010-10-01T00:00:00Z} 为有效的 dateTime 警告 1:无法解析 {2011-04-10T00:00:00Z} 为有效的 dateTime
警告 1:无法解析 {2010-11-21T00:00:00Z} 为有效的 dateTime 特征数量: 130
警告 1:无法解析 {2010-11-22T00:00:00Z} 为有效的 dateTime
准备分析内容
这个应用程序使用米或英里等距离,所以我们不希望我们的测量结果以度为单位。大多数地理藏宝坐标和点数据都是以度为单位,因此我们需要将坐标系转换为公制系统。
为了做到这一点,我们将首先使用第一章中的函数 Preparing the Work Environment:transform_geometries。执行以下操作:1. 复制此函数并将其粘贴到 geo_functions.py 文件中。这个函数将遍历数据中的特征以获取其几何形状,然后转换坐标系,返回包含所有转换几何形状的列表。函数看起来应该像这样:
def transform_geometries(datasource, src_epsg, dst_epsg):
"""转换第一层中所有几何形状的坐标。
"""
第一部分
src_srs = osr.SpatialReference()
src_srs.ImportFromEPSG(src_epsg)
dst_srs = osr.SpatialReference()
dst_srs.ImportFromEPSG(dst_epsg)
transformation = osr.CoordinateTransformation(src_srs, dst_srs) layer = datasource.GetLayerByIndex(0)
第二部分
geoms = []
layer.ResetReading()
for feature in layer:
geom = feature.GetGeometryRef().Clone()
geom.Transform(transformation)
geoms.append(geom)
return geoms
将函数组合成应用程序
到目前为止,我们看到了一些非常实用的工具函数,它们执行特定的任务;然而,为了形成一个应用程序,我们需要通过按顺序调用这些函数来组合它们,以实现我们的目标。我们需要协调调用和结果的代码——
一个将使应用程序运行。
为了做到这一点,我们将深入 Python 编程中最美丽和最有力的部分之一:类和方法。
Python 是一种面向对象的编程语言(但它不是严格的)。如果你不熟悉面向对象编程的概念,请不要担心;理解这个概念最好的方式是通过例子,所以我现在不会深入理论,而是通过例子来教学。现在执行以下步骤:
- 记得应用程序的入口点吗?它在 Chapter2 文件夹中的 geochaching_app.py 文件里。打开它进行编辑,你应该有这个:
coding=utf-8
def main():print "Hello geocaching APP!"
if name == "main":
main()
- 现在,让我们导入我们迄今为止编写的模块,以便我们可以在应用程序中使用它们。同时,让我们导入我们还需要的其他模块。在编码声明(# coding=utf-8)之后插入导入语句。现在你的代码应该类似于这个:
coding=utf-8
from utils.geo_functions import open_vector_file
from utils.geo_functions import transform_geometries
import numpy as np
import math
def main():
print "Hello geocaching APP!"
if name == "main":
main()
- 现在,删除 main() 函数,并在导入之后立即添加将代表我们应用程序的类:
class GeocachingApp(object):
def init(self, data_file=None):
"""应用程序类。
:param data_file: 一个与 OGR 兼容的文件
with geocaching points.
"""
第一部分。
self._datasource = None
self._transformed_geoms = None
第二部分。
if data_file:
self.open_file(data_file)
def open_file(self, file_path):
"""打开包含地理藏宝数据的文件并准备使用。
:param file_path:
"""
self._datasource = open_vector_file(file_path)
self._transformed_geoms = transform_geometries(
self._datasource, 4326, 3395)
在这里,我们创建了一个代表我们应用程序的类。在类内部,有一个特殊的方法叫做 init。这个方法在类实例化时被调用,这意味着当创建类的新实例时。在这里,我们可以看到一个名为 self 的参数;这个参数由类传递给所有实例方法,self 是类的实例本身。再次提醒,如果你对这些术语感到陌生,请不要担心,我们很快会对此进行更多讨论。
在第一部分,我们定义了两个任何类的实例都可能拥有的属性;注意,名称前的下划线表示该属性仅用于内部使用,不应从类外部调用。这种表示法仅是一种约定,实际上并不能真正阻止属性在方法外部被使用。在第二部分,如果用户传递可选文件,应用程序将调用 open_file 方法,该方法反过来打开文件并使用我们已开发的函数准备数据。
代码的编写方式允许我们随时更改我们正在工作的文件。
注意,当我们达到这一点时,我们已经达到了更高的抽象层次。首先,你有 OGR 库及其基本功能,需要编写许多代码行来完成特定的任务。然后,你有 utils 包,它将 ogr 函数封装成执行简单任务的实用函数,只需一行代码。现在,你有应用程序类,它将实用函数组合成方法,通过按正确的顺序和参数调用每个函数来自动化流程。
在这一点上,你需要执行以下步骤:
- 使用以下代码编辑 if name == 'main': 块:if name == "main":
my_app = GeocachingApp()
my_app.open_file('../data/geocaching.gpx') 2. 运行应用程序,查看结果。
设置当前位置
到目前为止,应用程序可以打开文件。下一步是定义你的位置,以便我们可以找到最近的藏宝地。为此,我们将修改 GeocachingApp 类,使其能够通过属性跟踪当前位置。我们还将创建更改位置(类似于几何形状)的方法、转换其坐标,并为其处理做准备。
需要执行以下步骤:
- 使用以下代码编辑 GeocachingApp 类的 init 方法:
..
def init(self, data_file=None, my_location=None):
"""应用程序类。
:param data_file: 一个兼容 OGR 的文件
with geocaching points.
"""
self._datasource = None
self._transformed_geoms = None
self._my_location = None
self.distances = None
if data_file:
self.open_file(data_file)
if my_location:
self.my_location = my_location
- 现在,将这些两个方法添加到类中:
@property
def my_location(self):
return self._my_location
@my_location.setter
def my_location(self, coordinates):
self._my_location = transform_points([coordinates])[0]
这里的逻辑是,类实例应该有一个 my_location 属性,我们希望程序能够自动转换其坐标系,就像处理 geocaching 数据那样。
实现这种行为有许多方法。如果你有其他编程语言的经验,你可能已经遇到过 getters 和 setters 的概念。
Getters 和 setters 是设计用来获取和设置类给定属性的方法。
使用方法而不是直接访问属性,允许程序员在检索或更改属性时修改值或执行复杂的过程。
我们可以为这个属性提供一个获取器和设置器方法——get_my_location() 和
set_my_location(),例如——但是 Python 提供了一种优雅的方式来干预设置和获取给定属性的流程,使用 @property 装饰器。
如前述代码所示,my_location 的实际值存储在 _my_location 属性中,并在 init 方法中定义(名称前的下划线表示该属性不应在类外部访问)。
然后,有两个具有相同名称的方法,这是我们要公开的属性的名称。这些函数被装饰,以便第一个成为获取器,第二个成为设置器。在设置器中,我们将调用在存储之前转换点坐标的函数(我们将在下一步中了解这个函数)。
就像我们对数据进行处理一样,位置也可以作为初始参数传递给类,并且可以在任何时间进行更改。以下是我们可以如何做到这一点:
- 现在,你的完整类应该类似于这个:
class GeocachingApp(object):
def init(self, data_file=None, my_location=None):
"""应用程序类。
:param data_file: 一个与 OGR 兼容的文件
with geocaching points.
:param my_location: 您的位置坐标。
"""
self._datasource = None
self._transformed_geoms = None
self._my_location = None
self.distances = None
if data_file:
self.open_file(data_file)
if my_location:
self.my_location = my_location
def open_file(self, file_path):
"""打开包含地理藏宝数据
并为其使用做准备。
:param file_path:
"""
self._datasource = open_vector_file(file_path)
self._transformed_geoms = transform_geometries(
self._datasource, 4326, 3395)
@property
def my_location(self):
return self._my_location
@my_location.setter
def my_location(self, coordinates):
self._my_location = transform_points([coordinates])[0]
- 由于我们没有 transform_points 函数,你应该注意到 PyCharm
在红色下划线处标记了 transform_points。因此,让我们在 geo_functions.py 文件中创建一个。此外,我们将通过创建另一个创建 OSR 转换的函数来避免样板代码:
注意
样板代码
样板代码,或称为样板,是许多地方重复出现且几乎或根本不进行更改的代码片段。
def create_transform(src_epsg, dst_epsg):
"""创建一个 OSR 转换。
:param src_epsg: 源几何的 EPSG 代码。
:param dst_epsg: 目标几何的 EPSG 代码。
:return: osr.CoordinateTransformation
"""
src_srs = osr.SpatialReference()
src_srs.ImportFromEPSG(src_epsg)
dst_srs = osr.SpatialReference()
dst_srs.ImportFromEPSG(dst_epsg)
return osr.CoordinateTransformation(src_srs, dst_srs)
def transform_points(points, src_epsg=4326, dst_epsg=3395):
"""转换坐标参考系中的一系列坐标(一系列点)
:param src_epsg: 源几何的 EPSG 代码。
:param dst_epsg: 目标几何的 EPSG 代码。
"""
transform = create_transform(src_epsg, dst_srs)
points = transform.TransformPoints(points)
return points
transform_points 函数使用具有相同名称的 OSR 函数,该函数在数组上执行转换。这个函数非常高效,可以在普通家用电脑上以每秒数百万次的速率转换坐标对。我们将它封装在我们的函数中的原因是我们想避免代码重复并添加默认参数。
注意,在 my_location 设置器中,我们将坐标放入一个列表中,然后获取返回值的第一个元素(self.mylocation =
transform_points([coordinates])[0]).


寻找最近点
要找到最近点,我们首先需要计算当前位置(我的位置)和所有点之间的距离。然后,我们需要找到离我的位置最近的点。
因此,对于每个点,我们必须应用一个返回到我位置的距离的方程,并将这些结果按以下表格中点的顺序存储:点索引 x
y
到我位置的距离
0
35 44 ?
1
20 92 ?
2
11 77 ?
两点之间的距离由以下方程给出:将此方程转换为 Python,我们有以下代码:distance = math.sqrt((xb-xa)2 + (yb-ya)2)
以下表格展示了基本的 Python 数学运算符 语法
数学表达式运算名称
a + b
a + b
加法

a - b
a - b
减法
a * b
a x b
乘法
a / b
a ÷ b
除法
a ** b
a b
指数
math.sqrt(a)
平方根
现在,通过执行以下代码将前面的方法插入到 GeocachingApp 类中:
...
def calculate_distances(self):
"""计算两点之间的距离
一组点和给定位置。
:return: 按相同顺序返回距离列表
这些点。
"""
xa = self.my_location[0]
ya = self.my_location[1]
points = self._transformed_geoms
distances = []
for geom in points:
point_distance = math.sqrt(
(geom.GetX() - xa)**2 + (geom.GetY() - ya))
distances.append(point_distance)
return distances
提示
代码的逐步优化
一些方程或操作可能非常复杂,有时它们变得难以编写,或者你可能需要查看中间步骤的结果来调试。这些情况下的提示是不要一开始就担心编写优化和快速的代码。
首先编写可读性和清晰的代码,将每个中间步骤分离到变量中。
例如,考虑以下距离方程:
distance = math.sqr((xb-xa)2 + (yb-ya)2)
这可以分解为中间步骤:
vertical_distance = yb - ya
水平距离 = xb – xa
distance = math.sqrt(horizontal_distance2 + vertical_distance2) 现在,调试并检查结果;当你确定逻辑正确且结果符合预期时,你可以逐步优化代码,通过替换部分和尝试不同的路径来提高性能,检查结果是否匹配。
最后的部分是找到距离列表中的最近点,这意味着在点集中找到一个给定位置。
index = the item that has the minimum value. Add this method to the class:
...
def find_closest_point(self):
"""找到给定位置最近的点并返回该点上的藏宝。
:return: 包含点的 OGR 特征。
"""
第一部分。
distances = self.calculate_distances()
index = np.argmin(distances)
第二部分。
layer = self._datasource.GetLayerByIndex(0)
feature = layer.GetFeature(index)
print "Closest point at: {}m".format(distances[index]) return feature
有可能数据中包含重复值,这会导致相同的距离,或者有极小的可能性两个点具有相同的距离。
所以,在第一部分,np.argmin 函数返回所有点中具有最小值的索引或索引。在第二部分,程序获取此索引处的特征。
执行以下步骤:
- 现在,让我们测试我们的应用程序并编辑 if name == 'main' 块,如下所示:
if name == "main":
my_app = GeocachingApp('../data/geocaching.gpx', [-73.0, 43.0]) my_app.find_closest_point()
- 现在,你的 geocaching_app.py 应该看起来像这样:
coding=utf-8
from utils.geo_functions import open_vector_file
from utils.geo_functions import transform_geometries
from utils.geo_functions import transform_points
import numpy as np
import math
class GeocachingApp(object):
def init(self, data_file=None, my_location=None):
"""应用程序类。
:param data_file: 一个与 OGR 兼容的文件
with geocaching points.
:param my_location: 你的位置的坐标。
"""
self._datasource = None
self._transformed_geoms = None
self._my_location = None
self.distances = None
if data_file:
self.open_file(data_file)
if my_location:
self.my_location = my_location
def open_file(self, file_path):
"""打开包含地理藏宝数据的文件并准备使用。
:param file_path:
"""
self._datasource = open_vector_file(file_path)
self._transformed_geoms = transform_geometries(
self._datasource, 4326, 3395)
@property
def my_location(self):
return self._my_location
@my_location.setter
def my_location(self, coordinates):
self._my_location = transform_points([coordinates])[0]
def calculate_distances(self):
"""计算两点之间的距离。
set of points and a given location.
:return: 与点顺序相同的距离列表。
the points.
"""
xa = self.my_location[0]
ya = self.my_location[1]
points = self._transformed_geoms
distances = []
for geom in points:
point_distance = math.sqrt(
(geom.GetX() - xa)**2 + (geom.GetY() - ya))
distances.append(point_distance)
return distances
def find_closest_point(self):
"""找到给定位置最近的点并返回该点上的缓存。
:返回: 包含点的 OGR 特征。
"""
第一部分。
distances = self.calculate_distances()
index = np.argmin(distances)
第二部分。
layer = self._datasource.GetLayerByIndex(0)
feature = layer.GetFeature(index)
print "最近的点在:{}m".format(distances[index]) return feature
if name == "main":
my_app = GeocachingApp('../data/geocaching.gpx', [-73.0, 43.0]) my_app.find_closest_point()
- 运行代码,按 Alt + Shift + F10,并选择 geocaching_app。查看输出结果:
最近的点在:49653.3244095m
处理完成,退出代码 0
摘要
在本章中,我们讨论了与代码组织和数据处理相关的重要概念。这是通过编写具有递增抽象级别的代码来实现的,直到我们拥有一个具有高级功能类的代码。
首先,我们编写了实用函数,以自动化任务并准备要处理的数据。其中一些函数是 OGR 库的简单抽象,旨在避免不必要的代码重复。
然后,我们在代表应用程序的类中编写了方法。这些方法负责执行一系列操作,使应用程序能够运行。
最后,我们介绍了如何在数据元素上执行数学运算的基础。我们编写了一个非常高效的方法,用于计算元素列表的距离。
在下一章中,我们将改进我们的数据抽象,使应用程序能够组合多个数据来源。
第三章. 组合多个数据
来源
地理数据往往具有异质性。仅举几个导致这种异质性的因素,它可能来自不同的来源,在不同的时间产生,甚至有不同的语言。鉴于这一事实,编写能够组合多个数据来源的程序是地理处理中的基本主题。
数据源可能以不同的格式存在,例如 shapefiles、文本文件、Google KML
文件,来自 GPS 的 GPX 文件等。它们的内容也可能不同;例如,它们可能具有不同的几何类型、坐标系和属性。
在本章中,我们将通过添加从不同站点和不同文件格式的多个数据来源的组合能力来增强我们的应用程序。为了实现这一点,我们将编写能够识别数据类型的代码,并根据这一点进行转换,以获得一组同质数据。
通过扩展 OGR 功能并包含我们自己的函数,我们可以在 Python 类中表示数据,并为其添加一些智能功能,使将多个来源的数据组合起来对应用程序和其他应用程序来说非常容易。
为了实现这些目标,我们将在本章中介绍以下主题:地理数据文件的结构
几何形状的表示
如何将数据转换为 Python 对象
如何组合多个数据源
使用 Python 中的类继承来编写更好的代码
地理数据的表示
大多数包含地理数据的文件格式都由一个常见的简单结构组成,该结构由多个特征组成,每个特征包含一个几何形状和无数个命名属性。
在这里,您可以查看 GeoJSON 文件内容的样本。这种地理文件的优势在于其可读性,使我们能够确切地看到所描述的结构。您不需要输入此示例;只需仔细查看即可。
其结构非常类似于 Python 字典。在最顶层,有 FeatureCollection,其中包含特征列表。每个特征都有一个几何形状,其类型可能不同,以及一个可能包含用户定义的任何任意属性的字典。简而言之,它严格遵循以下代码所示的数据表示方案:
{"type": "FeatureCollection",
"features": [
{"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [102.0, 0.5]},
"properties": {"prop0": "value0"}
},
{"type": "Feature",
"geometry": {
"type": "LineString",
"coordinates": [[102.0, 0.0], [103.0, 1.0], [104.0, 0.0]]
},
"properties": {
"prop0": "value0",
"prop1": 0.0
}
},
{"type": "Feature",
"geometry": {
"type": "Polygon",
"coordinates": [
[ [100.0, 0.0], [101.0, 0.0], [101.0, 1.0],
[100.0, 1.0], [100.0, 0.0] ]
]
},
"properties": {
"prop0": "value0",
"prop1": {"this": "that"}
}
}
]
}
JSON代表JavaScript 对象表示法,是一种易于在多种编程语言中读取和写入的格式。特别是在 Python 中,一个 JSON 对象可以
可以转换为字典,反之亦然。
有许多其他格式实现了相同结构;其中一些增加了额外的功能,而另一些具有针对特定目的非常具体的特征。
例如,ESRI的 shapefile 具有索引功能,GPX格式是为了与 GPS 设备配合使用以存储航点和轨迹而设计的,而SpatiLite是 SQLite 之上的单文件空间数据库,允许对象之间相互关联。
在以下表中,有一些常见的文件格式及其简要描述:格式
描述
笛卡尔
这是一个简单的点云。
坐标系统
数字线
这是一个用于矢量数据的 USGS 格式。
图形(DLG)
地理学
标记
这是一个基于 XML 的开放标准(由 OpenGIS 制定)用于 GIS 数据交换。
语言
GeoJSON
这是一种基于 JSON 的轻量级格式,并被许多开源 GIS 软件包所使用。
这是 SQLite 的空间扩展,提供了矢量地理数据库功能。它与 Spatialite 类似
PostGIS、Oracle Spatial 和具有空间扩展的 SQL Server。
Shapefile
这是一个由 Esri 开发的流行的矢量数据 GIS 格式。

表示几何形状
正如我们之前看到的,在地理数据中,每个特征都包含一个几何形状。几何形状是给定对象的空間表示。例如,一个点可以代表一棵树、一个兴趣点,或者在我们的案例中,一个藏宝地。一条线可以是一条道路、一条河流等等。
国家、城市、州或任何其他类型的区域都可以用多边形表示。
在这本书中,我们将遵循由 ISO 19125 标准化的简单特征规范描述的几何形状表示。它由点、线、多边形及其聚合或集合组成,如下面的图像所示:
在这种格式中,任何几何形状都是由点和它们之间的线性插值表示的。一个例子就是形成一条线的两个点。
这种类型的几何形状简单、非常常见且易于使用。尽管如此,它有一些明显的缺陷,其中最重要的是缺乏拓扑表示。
例如,我们可能有两个特征代表两个相邻的国家——例如,加拿大和美国。对于每个特征(即每个国家),都有一个多边形代表该国家的整个边界。因此,两个国家共享的边界将重叠。

现在,考虑美国的州和加拿大的省;每个都将是一个多边形,它们的边界也将重叠,反过来,它们将重叠国家。
边界。因此,我们将得到以下结果:
州/省
国家边界
其他国家边界
其他州/省
这使得有四条重叠的线;如果我们想表示城市、地区等,重叠几何形状的数量将会增加。这样,我们会有更高的错误概率,并且需要更多的存储空间。
这也是为什么这种几何形状表示也被称为意大利面数据;它以大量线条的并置结束(类似于意大利面)。
这种缺陷可以通过几何形状的拓扑表示来解决。最大的不同是,在这个例子中,它不会存储多边形;它会存储对象之间的关系。你有一组相互关联的边界,代表一个区域,两个区域可以拥有相同的边界。OpenStreetMap是地理特征拓扑表示的一个很好的例子。
虽然拓扑表示更先进,但处理起来更困难,而且绝大多数地理分析都可以使用简单表示来完成。
使数据同质化
将数据表示与实际对象联系起来的简单方法是将几何形状与特征属性相结合。
例如,一行可以是一条道路、河流、栅栏等。唯一的区别可能是类型属性,它告诉我们它是什么。或者,我们可能有一个名为 roads 的文件,它让我们知道它包含道路。
然而,计算机并不知道这一点,因为它不知道其他属性代表什么,也不知道文件是什么。因此,我们需要对数据进行转换,以便有一个可以分析的共同格式。
这种常见的格式是本主题的主题;这是数据如何在 Python 中以最佳方式表示,以及如何操作和分析这些对象以产生预期结果。
目标是将特征、几何形状和属性的基本数据表示转换为现实生活对象的表示,在这个过程中隐藏底层功能的细节。在计算机科学中,这被称为抽象。
而不是仅仅编写一些准备好的代码并神奇地执行转换,我们将逐步通过如何进行转换的演绎过程。这非常重要,因为这是开发代码以对任何类型的地理数据进行任何类型转换的基础,这些数据可以在未来使用。
抽象概念
现在我们已经清楚地了解了数据是如何表示的,让我们回到我们的地理缓存应用。
抽象是一种编程技术,旨在降低程序员编写代码的复杂性。它是通过将复杂代码封装在更符合人类友好的解决方案的渐进层中实现的。抽象级别越低,越接近机器语言,维护起来越困难。抽象级别越高,代码越试图模仿真实事物的行为,或者越接近自然语言,因此越直观,也越容易维护和扩展。
回到我们迄今为止看到的例子,我们可能会注意到许多抽象级别
——例如,当我们使用 OGR 库在打开 shapefiles 的函数中使用时。
看看下面的代码:
def open_vector_file(file_path):
"""打开与 OGR 兼容的矢量文件,获取第一层,并返回 OGR 数据源。
:param str file_path: 文件的完整路径。
:return: OGR 数据源。
"""
datasource = ogr.Open(file_path)
layer = datasource.GetLayerByIndex(0)
print("正在打开 {}".format(file_path))
print("特征数量: {}".format(layer.GetFeatureCount())) return datasource
在抽象的最上层,我们有自己的函数,它隐藏了 OGR 的功能。然后,我们有 OGR 的 Python 绑定,它抽象了 OGR C
API,它反过来处理内存分配、所有数学运算等。

抽象地理缓存点
因此,我们需要以智能的方式处理多个数据源,这样我们就不需要为每种数据类型更改代码
有可能结合来自多个源的数据
如果我们为程序添加额外的功能,我们不需要担心文件格式和数据类型
我们将如何做到这一点?答案是简单的:我们将抽象我们的数据,并将格式和类型处理的流程隐藏在内部功能中。
目标是,在应用程序的这个点之后,我们不再需要处理 OGR、图层、要素等。我们将只有一个类型的数据对象,我们将使用这个对象来表示我们的数据,所有的交互都将通过这个对象来完成。地理藏宝对象将表示一个单独的地理藏宝点,具有可以用来操作此对象的属性和方法。
现在执行以下步骤:
1. 首先,让我们组织项目结构。在 PyCharm 中打开您的 geopy 项目,并创建一个名为 Chapter3 的目录。
2. 将 Chapter2 中的所有文件和目录复制到 Chapter3 中。您应该得到以下类似的结构:
+---Chapter3
| | geocaching_app.py
| | init.py
| |
| +---experiments
| | import_test.py
| | module_test.py
| |
| ---utils
| data_transfer.py
| geo_functions.py
| init.py
3. 在 Chapter3 中,创建一个名为 models.py 的新文件(从现在开始,我们将
在 Chapter3 目录下工作)。
4. 现在将此代码添加到文件中:
class Geocache(object):
"""这是一个表示单个地理藏宝点的类。"""
def init(self, x, y):
self.x = x
self.y = y
@property
def coordinates(self):
返回 self.x, self.y
5. 现在,我们有一个带有其第一个属性的 geocache 类:地理藏宝点的坐标。为了测试我们的类,我们可以编写以下代码:if name == 'main':
one_geocaching_point = Geocache(20, 40)
打印(one_geocaching_point.coordinates)
6. 运行您的代码,按 Alt + Shift + F10,并选择模型文件。您应该在控制台看到以下输出:
(20, 40)
进程已退出,退出代码为 0
抽象地理藏宝数据
由于我们有一个单独的点,我们还需要有一个点的集合。我们将称这个为 PointCollection。继续抽象的过程,目标是隐藏导入和转换数据的操作。我们将通过创建一个新的类并在其中封装一些我们的实用函数来实现这一点。转到您的 models.py 文件,并添加以下类:
class PointCollection(object):
def init(self):
"""这是一个表示一组矢量数据的类。"""
Self.data = []
这是一个简单的类定义,在 init 方法中,我们将定义这个类的每个实例都将有一个数据属性。现在我们已经创建了简单的抽象,让我们给它添加一些功能。
导入地理藏宝数据
在上一章中,我们通过添加导入更多由 OGR 支持的数据类型的功能来泛化我们的导入函数。
现在,我们将再次改进它,使其能够处理一些错误,使其与我们的对象兼容,并添加两个新功能。我们还将转换数据以生成统一的对象。
为了实现我们的目标,我们将分析我们想要打开的文件中存储了哪些信息。我们将使用 OGR 来检查文件,并返回一些可能帮助我们进行数据转换的信息。
首先,让我们修改我们的 open_vector_file 函数,使其能够处理不正确的路径和文件名,这是一个非常常见的错误。执行以下步骤:1. 前往 utils 文件夹并打开 geo_functions.py 文件。
- 在文件开头添加以下导入语句:
coding=utf-8
import ogr
import osr
import gdal
import os
from pprint import pprint
- 现在,通过以下代码编辑 open_vector_file 函数:def open_vector_file(file_path):
"""打开与 OGR 兼容的矢量文件,获取第一层并返回 OGR 数据源。
:param str file_path: 文件的完整路径。
:return: OGR 数据源。
"""
datasource = ogr.Open(file_path)
检查文件是否已打开。
if not datasource:
if not os.path.isfile(file_path):
message = "路径错误。"
else:
message = "文件格式无效。"
raise IOError(
'Error opening the file {}\n{}'.format(
file_path, message))
layer = datasource.GetLayerByIndex(0)
print("正在打开 {}".format(file_path))
print("要素数量: {}".format(
layer.GetFeatureCount()))
return datasource
在这一步中,我们添加了一个验证来检查文件是否正确打开。如果文件不存在或存在任何其他问题,OGR 将保持沉默,并且
datasource 将会为空。所以,如果数据源为空(None),我们将知道出了些问题,并执行另一个验证来查看是否文件路径有误或其他情况发生。在任何情况下,程序将抛出异常,防止它继续使用错误的数据。
- 现在,我们将添加另一个函数来打印有关数据源的一些信息。在 open_vector_file 函数之后,添加 get_datasource_information 函数,代码如下:
def get_datasource_information(datasource, print_results=False):
"""获取数据源中第一层的信息。
:param datasource: 一个 OGR 数据源。
:param bool print_results: 如果为 True,则打印结果到屏幕。
the screen.
"""
info = {}
layer = datasource.GetLayerByIndex(0)
bbox = layer.GetExtent()
info['bbox'] = dict(xmin=bbox[0], xmax=bbox[1],
ymin=bbox[2], ymax=bbox[3])
srs = layer.GetSpatialRef()
if srs:
info['epsg'] = srs.GetAttrValue('authority', 1)
else:
info['epsg'] = 'not available'
info['type'] = ogr.GeometryTypeToName(layer.GetGeomType())
获取属性名称。
info['attributes'] = []
layer_definition = layer.GetLayerDefn()
for index in range(layer_definition.GetFieldCount()):
info['attributes'].append(
layer_definition.GetFieldDefn(index).GetName())
打印结果。
if print_results:
pprint(info)
return info
在这里,我们将使用 OGR 的许多方法和函数从数据源和层中获取信息。这些信息被放入一个字典中,该字典由函数返回。如果 print_results = True,则使用 pprint 函数(美化打印)打印字典。此函数尝试以更人性化的方式打印 Python 对象。
- 现在,为了测试我们的代码,编辑文件末尾的 if name == 'main': 块,如下所示:
if name == "main":
gdal.PushErrorHandler('CPLQuietErrorHandler')
datasource = open_vector_file("../../data/geocaching.gpx") info = get_datasource_information(
datasource, print_results=True)
这里有一个新元素:gdal.PushErrorHandler('CPLQuietErrorHandler').
藏宝文件通常包含具有空日期字段的特征。当 OGR 遇到这种情况时,它会打印警告消息。当我们有很多特征时,这可能会相当烦人。此命令告诉 OGR/GDAL 抑制这些消息,以便我们可以得到一个干净的输出,只显示我们想要看到的内容。
- 运行代码,按Alt + Shift + F10,选择geo_functions。你应该得到以下输出,显示收集到的信息:打开 ../../data/geocaching.gpx
特征数量:130
{'attributes': ['ele',
'time',
'magvar',
'geoidheight',
'name',
'cmt',
'desc',
'src',
'url',
'urlname',
'sym',
'type',
'fix',
'sat',
'hdop',
'vdop',
'pdop',
'ageofdgpsdata',
'dgpsid'],
'bbox': {'xmax': -73.44602,
'xmin': -79.3536,
'ymax': 44.7475,
'ymin': 40.70558},
'epsg': '4326',
'type': 'Point'}
Process finished with exit code 0
注意
字典的属性键包含可以从数据中读取的字段名。我们 GPX 文件上的每个要素(即每个点)都包含这个属性集。bbox 代码是数据的边界框,是包含数据地理范围的矩形的左上角和右下角的坐标。epsg 代码包含数据的坐标系统代码。最后,type 是 OGR 识别的几何类型。
读取 GPX 属性
在前一个示例中,查看 OGR 找到的属性(字段名);我们有一个名称、一个描述和日期。我们有一些关于 GPS 的技术数据
solution (pdop, hdop, sat, fix, and many more) 和一些其他字段,但它们都不包含关于藏宝地点的深入信息。
为了查看 GPX 文件包含的信息,OGR 没有显示,让我们在 PyCharm 中打开它:
-
在你的 geopy 项目中,进入数据文件夹。
-
定位 geocaching.gpx。要打开它,可以将它拖放到编辑区域或双击文件名。
PyCharm 将打开它以进行编辑,但不会识别文件格式,并以单色显示;因此,让我们通知它这是一个 XML 文件。
- 右键单击 geocaching.gpx 文件。在菜单中,选择关联文件类型,并弹出一个包含列表的窗口。选择XML 文件,然后点击确定按钮。
现在,GPX 文件的内容应该以不同颜色区分扩展标记语言的各个元素显示。PyCharm 也能够识别文件结构,就像它识别 Python 一样。让我们通过以下步骤查看:1. 按下Alt + 7或导航到视图 | 工具窗口 | 结构菜单。

- 这是 GPX 文件结构。请注意,在初始标签之后,它包含所有航点。点击任何航点左侧的箭头以展开它。然后,找到航点的地理藏宝标签并展开它。

-
如您所注意到的,地理藏宝点包含的信息比 OGR 能够读取的要多得多,包括地理藏宝标签的状态属性。
-
在我们继续之前,先探索一下文件,熟悉其符号表示。点击一些标签,查看代码编辑器以查看内容。
由于我们无法直接使用 OGR 访问这些属性,我们将编写一个替代方案。
目标是读取这些信息,并将其展平为字典中的单个键/值对级别。GPX 文件是 XML 文件,因此我们可以使用 XML 解析器来读取它们。这里的选择是 xmltodict 包;它将简单地将 XML 文件转换为 Python 字典,这使得我们更容易操作,因为我们非常熟悉字典。现在,执行以下步骤:
- 在 geo_functions.py 文件的开始处添加对 xmltodict 的导入,执行以下代码:
coding=utf-8
导入 xmltodict
导入 ogr
导入 osr
导入 gdal
导入 os
从 pprint 导入 pprint
- 在 open_vector_file 之前创建一个新函数,并添加以下代码:def read_gpx_file(file_path):
"""读取包含地理藏宝点的 GPX 文件。
:param str file_path: 文件的完整路径。
"""
with open(file_path) as gpx_file:
gpx_dict = xmltodict.parse(gpx_file.read())
print("Waypoint:")
print(gpx_dict['gpx']['wpt'][0].keys())
print("Geocache:")
print(gpx_dict['gpx']['wpt'][0]['geocache'].keys())
- 现在,编辑 if name == 'main':块以测试代码:if name == "main":
gdal.PushErrorHandler('CPLQuietErrorHandler')
read_gpx_file("../../data/geocaching.gpx")
- 再次使用Shift + F10运行代码并查看结果:Waypoint:
[u'@lat', u'@lon', u'time', u'name', u'desc', u'src', u'url', u'urlname', u'sym', u'type', u'geocache']
Geocache:
[u'@status', u'@xmlns', u'name', u'owner', u'locale', u'state', u'country', u'type', u'container', u'difficulty', u'terrain', u'summary', u'description', u'hints', u'licence', u'logs', u'geokrety']
处理完成,退出代码为 0
通过 print(gpx_dict['gpx']['wpt'][0].keys())语句,我们获得了 gpx 的值,然后是 wpt,它是一个列表。然后,我们获得了此列表的第一个元素的键并打印了它。
接下来,通过 print(gpx_dict['gpx']['wpt'][0]['geocache'].keys()),我们获得了 geocache 的值并打印了其键。
查看输出并注意,这与我们在 PyCharm 中探索 GPX 文件结构时所做的是同一件事。现在结构作为字典可用,包括标签的属性,这些属性在字典中以@符号表示。
现在我们有了处理 GPX 文件字典的简单且方便的方法,让我们提取和展平相关信息,并使函数返回它。编辑 read_gpx_file 函数,如下所示:
def read_gpx_file(file_path):
"""读取包含寻宝点的 GPX 文件。
:param str file_path: 文件的完整路径。
"""
with open(file_path) as gpx_file:
gpx_dict = xmltodict.parse(gpx_file.read())
output = []
for wpt in gpx_dict['gpx']['wpt']:
geometry = [wpt.pop('@lat'), wpt.pop('@lon')]
第二章:如果 geocache 不在字典中,则跳过此 wpt。
try:
geocache = wpt.pop('geocache')
except KeyError:
continue
attributes = {'status': geocache.pop('@status')}
合并字典。
attributes.update(wpt)
attributes.update(geocache)
构建一个 GeoJSON 特征并将其追加到列表中。
feature = {
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": geometry},
"properties": attributes}
output.append(feature)
return output
注意,在这里,我们使用了字典的 pop 方法;此方法返回给定键的值并从字典中删除该键。目标是只保留具有属性(properties)的字典,这些属性可以合并成一个包含属性的单一字典;合并是通过 update 方法完成的。
当某些航点没有 geocache 键时,我们会捕获异常并跳过此点。
最后,将信息组合成一个具有 GeoJSON 结构的字典。你可以这样做:
- 使用以下代码编辑 if name == 'main':块:if name == "main":
gdal.PushErrorHandler('CPLQuietErrorHandler')
points = read_gpx_file("../../data/geocaching.gpx") print points[0]['properties'].keys()
- 运行代码,你将看到以下输出:
['status', u'logs', u'locale', u'terrain', u'sym', u'geokrety', u'difficulty', u'licence', u'owner', u'urlname', u'desc', u'@xmlns', u'src', u'container', u'name', u'url', u'country', u'description', u'summary', u'state', u'time', u'hints', u'type']
Process finished with exit code 0
非常好!现在,所有地理藏点的属性都包含在特征的 properties 中。
返回同质数据
我们有一个 read_gpx_file 函数,它返回字典中的特征列表,以及一个 open_vector_file 函数,它返回 OGR 数据源。我们还有一个 get_datasource_information 函数,它返回关于文件所需的信息。
现在,是时候将这些函数结合起来,以便能够读取多种类型的数据(GPX、Shapefiles 等)。为此,我们将更改 open_vector_file 函数,使其能够根据文件格式做出决策,并转换数据,以确保始终返回相同的结构。执行以下步骤:1. 首先,确保 geo_function.py 中的函数顺序正确;如果不正确,请重新排列以符合以下顺序:
def read_gpx_file(file_path):
def get_datasource_information(datasource, print_results=False): def open_vector_file(file_path):
def create_transform(src_epsg, dst_epsg):
def transform_geometries(datasource, src_epsg, dst_epsg):
def transform_points(points, src_epsg=4326, dst_epsg=3395): 2. 现在,添加一个新函数,将 OGR 特征转换为字典,就像我们对 GPX 文件所做的那样。此函数可以插入在 open_vector_file 之前,如下所示:
def read_ogr_features(layer):
"""将图层中的 OGR 特征转换为字典。
:param layer: OGR 图层。
"""
features = []
layer_defn = layer.GetLayerDefn()
layer.ResetReading()
type = ogr.GeometryTypeToName(layer.GetGeomType())
for item in layer:
attributes = {}
for index in range(layer_defn.GetFieldCount()):
field_defn = layer_defn.GetFieldDefn(index)
key = field_defn.GetName()
value = item.GetFieldAsString(index)
attributes[key] = value
feature = {
"type": "Feature",
"geometry": {
"type": type,
"coordinates": item.GetGeometryRef().ExportToWkt()},
"properties": attributes}
features.append(feature)
return features
- 现在,通过以下代码编辑 open_vector_file 函数:def open_vector_file(file_path):
"""打开与 OGR 兼容的矢量文件或 GPX 文件。
返回特征列表和关于文件的信息。
:param str file_path: 文件的完整路径。
"""
datasource = ogr.Open(file_path)
检查文件是否已打开。
if not datasource:
if not os.path.isfile(file_path):
message = "路径错误。"
else:
message = "文件格式无效。"
raise IOError('Error opening the file {}\n{}'.format(
file_path, message))
metadata = get_datasource_information(datasource)
file_name, file_extension = os.path.splitext(file_path)
检查是否为 GPX 文件,如果是则读取。
if file_extension in ['.gpx', '.GPX']:
features = read_gpx_file(file_path)
如果没有,则使用 OGR 获取特征。
else:
features = read_ogr_features(
datasource.GetLayerByIndex(0))
return features, metadata
- 为了确保一切正常,让我们通过打开两种不同的文件类型来测试代码。编辑 if name == 'main': 块,如下所示:if name == "main":
gdal.PushErrorHandler('CPLQuietErrorHandler')
points, metadata = open_vector_file(
"../../data/geocaching.shp")
打印 points[0]['properties'].keys()
points, metadata = open_vector_file(
"../../data/geocaching.gpx")
打印 points[0]['properties'].keys()
- 运行代码并查看以下输出:
['src', 'dgpsid', 'vdop', 'sat', 'name', 'hdop', 'url', 'fix', 'pdop',
'sym', 'ele', 'ageofdgpsd', 'time', 'urlname', 'magvar', 'cmt', 'type',
'geoidheigh', 'desc']
['status', u'logs', u'locale', u'terrain', u'sym', u'geokrety', u'difficulty', u'licence', u'owner', u'urlname', u'desc', u'@xmlns', u'src', u'container', u'name', u'url', u'country', u'description', u'summary', u'state', u'time', u'hints', u'type']
进程以退出代码 0 完成
将数据转换为 Geocache 对象 到目前为止,我们已经定义了 Geocache 类;它具有纬度和经度属性以及一个返回这对坐标的方法。PointCollection 是一个包含地标的集合。
我们还有一个 open_vector_file 函数,它返回表示特征的字典列表。
现在,我们将通过利用 open_vector_file 函数实现将数据导入 PointCollection 类的过程来达到更高的抽象层次。
执行以下步骤:
- 打开 models.py 文件,并在文件开头执行以下代码以编辑导入:
coding=utf-8
导入 gdal
导入 os 模块
from pprint import pprint
from utils.geo_functions import open_vector_file
- 现在,让我们让 PointCollection 在实例化时自动导入文件。
前往 models.py 文件,更改类 init 方法,并添加 import_data 和 _parse_data 方法。运行此脚本:
class PointCollection(object):
def init(self, file_path=None):
"""此类表示一组矢量数据。”
self.data = []
self.epsg = None
if file_path:
self.import_data(file_path)
def import_data(self, file_path):
"""打开与 OGR 兼容的矢量文件并解析数据。”
:param str file_path: 文件的完整路径。
"""
features, metadata = open_vector_file(file_path)
self._parse_data(features)
self.epsg = metadata['epsg']
打印 "导入的文件: {}".format(file_path)
def _parse_data(self, features):
"""将数据转换为 Geocache 对象。”
:param features: 特征列表。
"""
for feature in features:
geom = feature['geometry']['coordinates']
attributes = feature['properties']
cache_point = Geocache(geom[0], geom[1],
attributes = attributes) self.data.append(cache_point)
- 现在,我们只需要将 Geocache 类调整为接收和存储属性。
用以下代码替换它:
class Geocache(object):
"""此类表示单个地理藏宝点。”
def init(self, lat, lon, attributes=None):
self.lat = lat
self.lon = lon
self.attributes = attributes
@property
def coordinates(self):
return self.lat, self.lon
属性参数被称为关键字参数。关键字参数是可选的,默认值是等号后面的值。
由于目前没有对地理藏宝数据格式的标准化,我们将保留从源文件中读取的所有属性不变。
在 Python 中,你不必提前定义类实例将具有哪些属性;属性可以在代码执行期间添加。然而,在 init 方法中定义它们是一个好习惯,因为它可以避免错误,例如尝试访问未定义的属性。PyCharm 可以跟踪这些属性并警告你关于拼写错误。它还充当文档。
- 在测试代码之前,编辑 PointCollection 类并添加一个显示一些信息的方法,如下所示:
...
def describe(self):
print("SRS EPSG 代码:{}".format(self.epsg))
print("特征数量:{}".format(len(self.data))) 2. 为了测试你的代码,通过以下代码行编辑 if name == 'main'块:
if name == 'main':
gdal.PushErrorHandler('CPLQuietErrorHandler')
vector_data = PointCollection("../data/geocaching.gpx") vector_data.print_information()
- 现在,运行代码。你应该看到以下输出:导入文件:../data/geocaching.gpx
SRS EPSG 代码:4326
特征数量:112
进程以退出代码 0 结束
合并多个数据源
现在数据以 PointCollection 形式包含 Geocache 对象,从多个文件或多个 PointCollection 数据中合并数据应该很容易。
执行以下步骤:
- 进行另一次测试。首先,我们将查看是否可以导入多个文件并编辑 models.py 文件的 if name == 'main'块。执行以下代码:
if name == 'main':
gdal.PushErrorHandler('CPLQuietErrorHandler')
vector_data = PointCollection("../data/geocaching.gpx") vector_data.describe()
vector_data.import_data("../data/geocaching.shp") vector_data.describe()
- 再次运行代码。现在,在导入另一个文件后,你应该看到特征数量翻倍,如下所示:
导入文件:../data/geocaching.gpx
SRS EPSG 代码:4326
特征数量:112
导入文件:../data/geocaching.shp
SRS EPSG 代码:None
特征数量:242
进程以退出代码 0 结束
-
让我们实现一些非常优雅的功能。我们将向 PointCollection 类添加一个魔法方法,以便我们可以合并两个实例的内容。
-
编辑 PointCollection 类,并在 init 方法之后添加 add 方法
通过以下代码实现方法:
class PointCollection(object):
def init(self, file_path=None):
"""此类表示一组矢量数据。”
self.data = []
self.epsg = None
if file_path:
self.import_data(file_path)
def add(self, other):
self.data += other.data
return self
与 init 方法类似,add 方法是 Python 的魔法方法之一。
这些方法不是直接调用的;它们在特定事件发生时自动调用。init 方法在类实例化时调用,add 方法在使用加号(+)运算符时调用。因此,为了合并两个 PointCollection 实例的数据,我们只需要将它们相加。以下是我们需要做的:
- 编辑 if name == 'main':块,如下所示:if name == 'main':
gdal.PushErrorHandler('CPLQuietErrorHandler')
my_data = PointCollection("../data/geocaching.gpx") my_other_data = PointCollection("../data/geocaching.shp") merged_data = my_data + my_other_data
merged_data.describe()
- 然后,运行代码并查看结果:
导入的文件:../data/geocaching.gpx
导入的文件:../data/geocaching.shp
SRS EPSG 代码:4326
特征数量:242
处理完成,退出代码为 0
将新功能集成到
应用程序
在第二章,《Geocaching App》中,我们开发了应用程序,使其能够找到靠近你位置的点。然而,在应用程序内部,数据组织方式不同;虽然这是一种处理数据非常有效的方法,但它使我们很难理解如何对此数据进行操作。
通过抽象,我们实现了一种新的数据表示形式——这是一种非常直观且易于使用的形式。
现在,我们将修改应用程序,使其能够使用这种新的数据类型来执行其功能,并且还可以聚合结合多个数据源的新功能。
查看 GeocachingApp 和 PointCollection 类;你可能注意到它们有一些部分看起来彼此相似。这两个类都存储数据,并且有打开数据的方法。
在这一点上,经过少量修改,如果我们将一个类的方法转移到另一个类中,我们最终可以得到一个功能性的应用程序,这正是我们将要做的。然而,我们不会复制和粘贴,而是使用 Python 的类继承。我们将使用 GeocachingApp 类,使其继承 PointCollection 类的所有功能。
为了完全理解,我们将逐个方法地通过这些过程。
打开你的 geocaching_app.py 文件,现在,让我们专注于类声明和 init 方法。在导入部分进行以下更改,在类中,你可以保留其他方法不变;不要删除它们:
coding=utf-8
from pprint import pprint
导入 gdal
导入 numpy as np
导入 math
从utils.geo_functions导入transform_geometries
从utils.geo_functions导入transform_points
从models导入Geocache,PointCollection
class GeocachingApp(PointCollection):
def init(self, data_file=None, my_location=None):
"""应用程序类。
:param data_file: 一个与 OGR 兼容的文件
与地理藏点一起。
:param my_location: 您的位置坐标。
"""
super(GeocachingApp, self).init(file_path=data_file)
self._datasource = None
self._transformed_geoms = None
self._my_location = None
self.distances = None
删除包含 "if data_file…" 的代码
if my_location:
self.my_location = my_location
在类声明(class GeocachingApp(PointCollection))中,我们添加了GeocachingClass,这告诉 PythonGeocachingApp类应该从PointCollection继承方法和属性。然而,由于两个类都有一个__init__方法,除非我们做些什么,否则地理藏应用(Geocaching app)的__init__方法将完全覆盖PointCollection方法。
我们希望调用两个__init__方法,因此我们将使用super()函数。这告诉 Python 调用继承类的__init__方法。此外,由于PointCollection类现在处理文件导入,我们将data_file参数传递给它。执行以下步骤:
- 让我们测试它并检查继承是否工作。转到
if __name__ == "__main__":
'main': 文件末尾的代码块,并按以下方式编辑:if name == "main":
gdal.PushErrorHandler('CPLQuietErrorHandler')
创建应用程序:
my_app = GeocachingApp()
现在,我们将调用PointCollection类的一个方法:my_app.import_data("../data/geocaching.gpx")
-
实际上,当你编写代码时,你可能注意到 PyCharm 的自动完成功能现在包括继承类的方法和属性。
-
运行代码,你应该看到以下输出:
导入的文件:../data/geocaching.gpx
进程以退出代码 0 结束
恭喜!你刚刚成功使用了类继承。这是 Python 的一个非常强大且实用的特性。
摘要
在本章中,挑战在于找到一种方法来组合来自多个来源的数据。
解决这个问题的方法是编写可以接受不同类型的数据并将其转换为通用类型对象的代码。
为了实现这一点,我们首先创建了两个新的 Python 类。第一个是Geocache类,它代表一个单独的地理藏点位置,包含其坐标、名称和描述。第二个是PointCollection类,它代表一组Geocache对象。这个类具有从所需文件中导入和转换信息的 ability。
我们使用的这种技术被称为抽象;其基础在于隐藏复杂的过程,使其可以通过人类易于理解的对象来实现。
最后,我们通过类继承将这一新的抽象层集成到应用程序中。GeocachingApp 类继承了 PointCollection,最终它可以同时表现出任何一种或两种行为。
在下一章中,虽然我们将提高应用程序搜索点的功能,但你还将学习其他组合类的方法。
第四章. 提高应用程序搜索功能
能力
到目前为止,我们的应用程序能够简单地搜索接近定义位置的点。在本章中,我们将进行一次巨大的飞跃,使我们的应用程序能够通过地理边界和数据的任何字段来过滤数据。
到本章结束时,你将能够搜索位于给定城市、州、国家或任何你定义的边界内的地理藏点。此外,你还可以通过其属性(如难度级别、名称、用户等)进行搜索。还可以组合多个过滤器。
在这个过程中,我们将看到如何处理多边形以及如何在地理处理应用程序中分析几何形状之间的关系。
为了实现这些目标,我们将探讨以下主题:如何使用众所周知的文本描述多边形
使用 Shapely 包处理几何形状
导入多边形数据
导入线数据
基类和继承的使用
几何关系类型
通过多个属性进行过滤和链式方法调用

处理多边形
假设我们想要通过一个给定的区域来过滤我们的数据,那么这个区域可能是由一个多边形表示的。
例如,以下图像表示世界各国的边界,它是由一个 Shapefile 渲染的,其中每个特征是一个国家,其几何形状是一个多边形。
与仅有一对坐标的地理藏点不同,多边形是一系列至少有三个点的坐标,起点和终点在同一个点上。
到现在为止,你可以假设我们无法使用与地理藏点相同的结构来存储多边形的坐标。我们需要存储整个 OGR 几何形状或存储可以从中或转换到它的东西。
这些多边形是如何表示的,这是一个重要的主题,因为掌握它可以使你以任何你需要的方式操纵它们。它还允许你从点坐标(例如从 GPS)或形成矩形等形状中构建多边形。

了解众所周知的文本
众所周知的文本(WKT)是一种人类可读的标记语言,用于在空间应用程序中表示几何形状。它最初由开放地理空间联盟(OGC)定义,并被许多软件作为数据交换的形式所接受。WKT 有一个二进制等效物,称为众所周知的二进制(WKB)。它用于数据存储和传输,在这些情况下不需要人类可读性。
让我们通过一些示例来了解 WKT 是如何工作的。首先,我们将创建一个 OGR
下图所示的多边形几何体:
1. 在 geopy 项目中复制您的 Chapter3 文件夹,并将其重命名为 Chapter4\。
2. 定位 Chapter4\experiments 目录并删除其中的文件。如果您没有这个目录,请创建它。
3. 在 Chapter4\experiments 文件夹内,创建一个新的 Python 文件。在 PyCharm 中,右键点击文件夹并选择新建 | Python 文件。将此文件命名为 wkt_experiments.py。
4. 输入以下代码:
coding=utf-8
import ogr
wkt_rectangle = "POLYGON ((1 1, 1 9, 8 9, 8 1, 1 1))"
geometry = ogr.CreateGeometryFromWkt(wkt_rectangle)
print(geometry.class)
print(geometry.Area())
print(8*7)
5. 现在运行它( Alt + Shift + F10 并选择 wkt_experiments)。你应该看到以下输出:
<class 'osgeo.ogr.Geometry'>
56.0
56

进程已结束,退出代码为 0
我们在这里所做的就是在 Python 字符串中定义了多边形的 WKT 表示。请注意,它从坐标 1.1 开始,按顺时针方向列出所有坐标,最后又回到 1.1(方向不重要;也可以是逆时针)。
在下一行,我们调用了 OGR 的 CreateGeometryFromWkt 函数,该函数将字符串作为参数传递。内部,它将字符串转换为 OGR 几何对象。
为了确保接下来的三行一切顺利,我们打印了对象的类名、OGR 计算的区域以及手动计算的区域。
现在,一个更复杂的多边形,中间有一个洞或一个岛屿。
6. 编辑你的代码:
coding=utf-8
import ogr
wkt_rectangle = "POLYGON ((1 1, 1 9, 8 9, 8 1, 1 1))"
geometry = ogr.CreateGeometryFromWkt(wkt_rectangle)
print(geometry.class)
print(geometry.Area())
print(8*7)
wkt_rectangle2 = "POLYGON ((1 1, 8 1, 8 9, 1 9, 1 1)," \
"(4 2, 4 5, 7 5, 7 2, 4 2))"
geometry2 = ogr.CreateGeometryFromWkt(wkt_rectangle2)
print(geometry.class)
print(geometry2.Area())
print((87) - (33))
7. 现在再次运行它( Shift + F10)。你应该看到以下输出:
<class 'osgeo.ogr.Geometry'>
56.0
56
<class 'osgeo.ogr.Geometry'>
47.0
47
每个多边形环都位于括号内,由逗号分隔。外部环应该首先描述,然后是所有内部环。
当复杂性和坐标数量增加时,使用 WKT 管理几何体变得复杂。为了解决这个问题和其他问题,我们将使用另一个包,这将使事情变得容易得多。
使用 Shapely 处理几何体
Shapely 是一个用于平面特征分析的 Python 包。它使用 GEOS 库中的函数以及 Java 拓扑套件(JTS)的移植版本。
它在处理几何体时主要具有与 OGR 相同的类和函数。
虽然它不能替代 OGR,但它有一个更Pythonic和非常直观的接口,它优化得更好,并且拥有完善的文档。
为了使事情更清晰,Shapely 旨在分析几何体,仅限于几何体。它不处理要素的属性,也不具备读取和写入地理空间文件的能力。
为了直接比较 Shapely 和 OGR,我们将重写之前的示例:
- 将以下行添加到 wkt_experiments.py 文件中(你可以保留或删除之前的代码,由你决定):
from shapely.geometry import Polygon
print('使用 Shapely 的示例')
polygon1 = Polygon([(1, 1), (1, 9), (8, 9), (8, 1), (1, 1)]) print(polygon1.class)
打印多边形 1 的面积
polygon2 = Polygon([(1, 1), (1, 9), (8, 9), (8, 1), (1, 1)], [[(4, 2), (4, 5),(7, 5), (7, 2), (4, 2)]])
print(polygon2.class)
print(polygon2.area)
- 现在再次运行代码并查看输出:
使用 Shapely 的示例
<class 'shapely.geometry.polygon.Polygon'>
56.0
<class 'shapely.geometry.polygon.Polygon'>
47.0
Process finished with exit code 0
一切都按预期工作,但你可能注意到一些差异。首先,为了创建多边形,我们传递了一个元组的列表(它可以是列表的列表),其中每个元组是一个点坐标。这个小小的变化带来了很大的不同;列表比字符串更容易操作。
其次,当我们打印由 Shapely 创建的对象的类名时,我们看到它是一个多边形类,而不是像 OGR 那样是一个几何体。这代表了一个更高的抽象级别,如第三章中解释的结合多个数据源。随着它而来的是抽象的所有好处和减少对内部功能的担忧。
当你输入代码时,特别是 print(polygon1.area),PyCharm 显示给你一个列表
Polygon 类的多种方法。这是 Shapely 的另一个特性,它是一个编写良好且 IDE 友好的 Python 包。结果是,它允许你使用自动完成、代码检查、重构以及现代 IDE 带来的许多其他功能。
导入多边形
现在我们已经了解了如何处理多边形以及如何表示和存储它们,我们将回到我们的应用程序中,添加导入包含多边形的地形文件的功能。就像我们处理点一样,我们将抽象要素到 Python 对象中,并且我们还将使用类继承。
首先,让我们看看我们已编写的代码。在 models.py 文件中,我们有 PointCollection 类:
class PointCollection(object):
def init(self, file_path=None):
"""这个类表示一组矢量数据。”
self.data = []
self.epsg = None
if file_path:
self.import_data(file_path)
def add(self, other):
self.data += other.data
return self
def import_data(self, file_path):
"""打开与 OGR 兼容的矢量文件并解析数据。”
:param str file_path: 文件的完整路径。
"""
features, metadata = open_vector_file(file_path)
self._parse_data(features)
self.epsg = metadata['epsg']
print("File imported: {}".format(file_path))
def _parse_data(self, features):
"""将数据转换为 Geocache 对象。
:param features: 特征列表。
"""
for feature in features:
geom = feature['geometry']['coordinates']
attributes = feature['properties']
cache_point = Geocache(geom[0], geom[1],
attributes = attributes)
self.data.append(cache_point)
def describe(self):
print("SRS EPSG code: {}".format(self.epsg))
print("Number of features: {}".format(len(self.data))) 这个类代表了一组地理藏点,并负责导入这些点以及转换和存储它们。这些正是我们想要实现以导入多边形的功能。
在上一章中,你看到了如何通过继承,使一个类从其他类继承功能。我们将使用同样的技术来使用我们已有的内容来导入多边形。
由于地理藏点和多边形的处理可能有其特殊性,因此需要针对每个对象具体指定一些内容。一个具体的例子是 _parse_data 方法,目前它将特征转换为地理藏点。
因此,直接从 PointCollection 类继承来表示多边形类不是一个好主意。相反,想法是拥有两个基类,一个表示单个对象,另一个表示该对象的集合。这些基类将包含点和多边形共有的方法,然后子类将包含针对每种情况的特定方法。
我们将要导入的多边形可能是国家、边界、州或省、城市、区域等。由于目前还不清楚,让我们称它为boundaries。
这将在以下步骤中解释:
-
我们将开始创建 BaseGeoObject 对象,并从 Geocache 类进行适配。打开 Chapter4 文件夹中的 models.py 文件。
-
创建 Geocache 类的副本,包含所有方法(复制粘贴)。
-
将第一个副本重命名为 BaseGeoObject,并更改文档字符串为类似
"Base class for single geo objects.". 你应该有这个:class BaseGeoObject(object):
"""Base class for a single geo object."""
def init(self, lat, lon, attributes=None):
self.lat = lat
self.lon = lon
self.attributes = attributes
@property
def coordinates(self):
return self.lat, self.lon
class Geocache(object):
"""这个类代表了一个单个的地理藏点。”
def init(self, lat, lon, attributes=None):
self.lat = lat
self.lon = lon
self.attributes = attributes
@property
def coordinates(self):
return self.lat, self.lon
现在尝试思考,看着这两个类,Geocache 有什么是特定的,什么不属于通用 GeoObject 或属于它,以及每种类型的地理空间对象可能有哪些属性和方法。
这种分离可能会引起一些争议,有时,根据项目的复杂性和你处理的事物的性质,可能很难达成最终
在代码的第一遍迭代中。在你的项目中,你可能需要多次回来更改类的组织方式。
现在,我将提出以下逻辑:
纬度,经度: 这些属性仅适用于 Geocache。正如我们所见,我们可能有其他类型的几何形状,我们希望泛化几何形状的存储方式。
属性: 所有对象都应该具有这个属性。
一个 repr 方法: 这是另一个像 init 和 add 一样的“魔法方法”,我们在上一章中提到过。当你在对象上使用 print()函数时,会调用 repr。我们将添加它,并将其设置为在基类中不实现,因为每种类型的对象都应该有自己的表示。
坐标属性: 所有地理对象都应该有坐标,但在这里的实现方式是针对 Geocache 特定的。我们将将其改为通用形式:一个包含对象几何形状的 geom 属性。
让我们对这些类进行第一次修改。编辑你的代码如下:class BaseGeoObject(object):
"""单个地理对象的基类。”
def init(self, geometry, attributes=None):
self.geom = geometry
self.attributes = attributes
@property
def coordinates(self):
raise NotImplementedError
def repr(self):
raise NotImplementedError
class Geocache(BaseGeoObject):
"""这个类表示单个地理藏点。”
def init(self, geometry, attributes=None):
super(Geocache, self).init(geometry, attributes)
def repr(self):
name = self.attributes.get('name', 'Unnamed')
return "{} {} - {}".format(self.geom.x,
self.geom.y, name)
在实例化时,向类中添加了一个 geom 属性作为必需参数。在这个属性中,我们将存储 Shapely 对象。lat 和 lon 属性已被删除;它们可以直接从 Shapely 对象(geom)访问,我们将调整 PointCollection 以实现这一点。
Geocache 类的 repr 方法返回一个包含点的坐标和名称属性(如果可用)或“Unnamed”的字符串。
现在添加 Boundary 类:
class Boundary(BaseGeoObject):
"""表示单个政治边界。”
def repr(self):
return self.name
目前,Boundary 类几乎与 BaseGeoObject 类相同,所以我们只更改 repr 方法,使其只返回边界的名称。
下一步是编辑集合类。我们的 PointCollection 类几乎与新组织兼容。我们只需要对 _parse_data 方法进行一些修改,将这个类转换为基类,并创建从它继承的类:
1. 首先,就像我们之前做的那样,复制 PointCollection 类。
2. 现在,重命名这个类的第一次出现并更改其文档字符串:class BaseGeoCollection(object):
"""这个类代表了一个空间数据的集合。”
...
- 前往 _parse_data 方法并修改它如下:
...
def _parse_data(self, features):
raise NotImplementedError
我们在这里明确指出,这个方法在基类中未实现。这样做有两个原因:首先,它向程序员提供了一个提示,即当这个类被继承时需要实现这个方法;它还声明了方法的 签名(它应该接收的参数)。其次,如果没有实现,Python 将引发 NotImplementedError 而不是 AttributeError,这会导致更好的调试体验。
- 在我们继续之前,编辑文件开头的导入模块以匹配以下代码:
coding=utf-8
从 future 导入 print_function
导入 gdal
从 shapely.geometry 导入 Point
从 shapely 导入 wkb 和 wkt
从 utils.geo_functions 导入 open_vector_file
- 基类已经准备好了,现在我们将编辑 PointCollection 类。
首先,您可以从这个类中删除所有方法。只留下文档字符串和 _parse_data 方法。
-
编辑类声明并使其继承自 BaseGeoCollection。
-
最后,编辑 _parse_data 方法以符合 Shapely 对象表示的几何形状。您的代码应如下所示:
class PointCollection(BaseGeoCollection):
"""这个类代表了一个空间数据的集合。
地理藏宝点。
"""
def _parse_data(self, features):
"""将数据转换为 Geocache 对象。
:param features: 特征列表。
"""
for feature in features:
coords = feature['geometry']['coordinates']
point = Point(float(coords[1]), float(coords[0]))
attributes = feature['properties']
cache_point = Geocache(point, attributes = attributes)
self.data.append(cache_point)
注意,区别在于在实例化 Geocache 时,我们不再传递坐标,而是现在传递一个 Point 对象,它是 Shapely 提供的 Point 类的实例。
- 接下来,我们将创建 BoundaryCollection 类。在任何基类之后插入此代码:
class BoundaryCollection(BaseGeoCollection):
"""这个类代表了一个空间数据的集合。
地理边界。
"""
def _parse_data(self, features):
for feature in features:
geom = feature['geometry']['coordinates']
attributes = feature['properties']
polygon = wkt.loads(geom)
boundary = Boundary(geometry=polygon,
attributes=attributes)
self.data.append(boundary)
与 PointCollection 的区别在于我们现在正在创建多边形和 Boundary 类的实例。注意多边形是如何通过语句 wkt.loads(geom) 创建的。
- 我们几乎完成了。检查是否一切正确。完整的 models.py 文件应包含以下代码:
coding=utf-8
从 future 导入 print_function
导入 gdal
从 shapely.geometry 导入 Point
从 shapely 导入 wkb 和 wkt
从 utils.geo_functions 导入 open_vector_file
class BaseGeoObject(object):
"""Base class for a single geo object."""
def init(self, geometry, attributes=None):
self.geom = geometry
self.attributes = attributes
@property
def coordinates(self):
raise NotImplementedError
def repr(self):
raise NotImplementedError
class Geocache(BaseGeoObject):
"""This class represents a single geocaching point."""
def init(self, geometry, attributes=None):
super(Geocache, self).init(geometry, attributes)
def repr(self):
name = self.attributes.get('name', 'Unnamed')
return "{} {} - {}".format(self.geom.x,
self.geom.y, name)
class Boundary(BaseGeoObject):
"""Represents a single geographic boundary."""
def repr(self):
return self.attributes.get('name', 'Unnamed')
class BaseGeoCollection(object):
"""This class represents a collection of spatial data."""
def init(self, file_path=None):
self.data = []
self.epsg = None
if file_path:
self.import_data(file_path)
def add(self, other):
self.data += other.data
return self
def import_data(self, file_path):
"""Opens an vector file compatible with OGR and parses the data.
:param str file_path: The full path to the file.
"""
features, metadata = open_vector_file(file_path)
self._parse_data(features)
self.epsg = metadata['epsg']
print("File imported: {}".format(file_path))
def _parse_data(self, features):
raise NotImplementedError
def describe(self):
print("SRS EPSG code: {}".format(self.epsg))
print("Number of features: {}".format(len(self.data))) class PointCollection(BaseGeoCollection):
"""This class represents a collection of
geocaching points.
"""
def _parse_data(self, features):
"""Transforms the data into Geocache objects.
:param features: A list of features.
"""
for feature in features:
coords = feature['geometry']['coordinates']
point = Point(coords)
attributes = feature['properties']
cache_point = Geocache(point, attributes=attributes)
self.data.append(cache_point)
class BoundaryCollection(BaseGeoCollection):
"""This class represents a collection of
geographic boundaries.
"""
def _parse_data(self, features):
for feature in features:
geom = feature['geometry']['coordinates']
attributes = feature['properties']
polygon = wkt.loads(geom)
boundary = Boundary(geometry=polygon,
attributes=attributes)
self.data.append(boundary)
7. Now, in order to test it, go to the end of the file and edit the if name ==
'main': block:
if name == 'main':
world = BoundaryCollection("../data/world_borders_simple.shp") for item in world.data:
print(item)
8. Now run it, press Alt + Shift + F10, and select models. If everything is OK, you should see a long list of the unnamed countries:
File imported: ../data/world_borders_simple.shp
未命名
未命名
未命名
未命名…
Process finished with exit code 0
This is disappointing. We expected to see the names of the countries, but for some reason, the program failed to get it from the attributes. We will solve this problem in the next topic.
获取属性值
让我们探索世界边界的属性,以了解为什么我们无法获取名称。
- 编辑 if name == 'main'块:
if name == 'main':
world = BoundaryCollection("../data/world_borders_simple.shp") print(world.data[0].attributes.keys())
- 运行代码并查看输出:
File imported: ../data/world_borders_simple.shp
['SUBREGION', 'POP2005', 'REGION', 'ISO3', 'ISO2', 'FIPS', 'UN',
'NAME']
Process finished with exit code 0
我们所做的是获取 world.data 中的第一个项目,然后打印其属性键。输出中显示的列表有一个 NAME 键,但它全部是大写的。这对于包含在 DBF 文件中的数据 Shapefiles 来说非常常见。
由于我们不希望担心属性名称是大写还是小写,我们有两个可能的解决方案:在导入时转换名称,或者在请求属性值时即时转换名称。
根据您的应用程序,您可能可以使用其中一种方法获得更好的性能。在这里,为了教学目的,我们将选择即时转换并给它添加一点趣味。
- 而不是直接访问属性,让我们创建一个为我们完成这项工作的方法。
编辑 BaseGeoObject 类的 init 方法,并添加一个 get_attribute 方法:
class BaseGeoObject(object):
"""Base class for a single geo object."""
def init(self, geometry, attributes=None):
self.geom = geometry
self.attributes = attributes
# 创建不区分大小写的属性查找表。
self._attributes_lowercase = {}
for key in self.attributes.keys():
self._attributes_lowercase[key.lower()] = key
@property
def coordinates(self):
raise NotImplementedError
def get_attribute(self, attr_name, case_sensitive=False):
"""通过名称获取属性。
:param attr_name: 属性的名称。
:param case_sensitive: True 或 False。
"""
if not case_sensitive:
attr_name = attr_name.lower()
attr_name = self._attributes_lowercase[attr_name]
return self.attributes[attr_name]
def repr(self):
raise NotImplementedError
在 init 方法中,我们创建了一个包含小写属性名称与原始名称之间等价的字典。如果您在互联网上搜索,会发现有许多技术可以实现不区分大小写的字典。但我们在这里实现的方法允许我们保留原始名称,使用户可以选择是否希望搜索区分大小写。
- 现在,编辑 Boundary 类以使用新方法:
class Boundary(BaseGeoObject):
"""Represents a single geographic boundary."""
def repr(self):
return self.get_attribute('name')
- 编辑 if name == 'main'块:
if name == 'main':
world = BoundaryCollection("../data/world_borders_simple.shp") for item in world.data:
print(item)
- 再次运行代码。现在,你应该有一个漂亮的国籍名称列表:导入的文件:../data/world_borders_simple.shp
安提瓜和巴布达
阿尔及利亚
阿塞拜疆
阿尔巴尼亚
亚美尼亚…
进程以退出代码 0 完成
导入线
正如我们在地理藏宝点和政治边界中所做的那样,我们将实现程序导入线(即线字符串)的能力。这些线可以代表道路、河流、电力线等。有了这类特性,我们将能够搜索靠近给定道路的点等。
线和线集合也将是 BaseGeoObject 和 BaseGeoCollection 的子类。让我们首先创建一个 LineString 和 LineStringCollection 类,如下所示:
- 将这个新类插入到 models.py 文件中。它可以在基类定义之后任何位置:
class LineString(BaseGeoObject):
"""表示单个线字符串。”
def repr(self):
return self.get_attribute('name')
再次,我们只实现了 repr 方法。其他功能是从 BaseGeoObject 类继承的。
- 现在,添加一个表示线字符串集合的类及其 _parse_data 方法:
class LineStringCollection(BaseGeoCollection):
"""表示线字符串的集合。”
def _parse_data(self, features):
for feature in features:
geom = feature['geometry']['coordinates']
attributes = feature['properties']
line = wkt.loads(geom)
linestring = LineString(geometry=line,
attributes=attributes)
self.data.append(linestring)
为了测试我们新的类,我们将使用包含美国主要道路的 shapefile。

- 编辑文件末尾的 if name == 'main' 块。如果你愿意,可以注释掉之前的代码而不是删除它:
if name == 'main':
usa_roads = LineStringCollection('../data/roads.shp')
for item in usa_roads.data:
print(item)
- 运行代码。你应该会在输出控制台中看到一个包含道路名称的大列表:导入文件:../data/roads.shp
州际公路 131
州际公路 3
州际公路 3
州际公路 3
州际公路 411
州际公路 3
州际公路 3
州际公路 5,州际公路 786…
进程以退出代码 0 完成
为了使我们的输出更有意义,我们可以更改每个 LineString 类的打印方式。记住,当你在对象上使用 print() 函数时,会调用名为 repr 的特殊方法,并且它应该返回一个要打印的字符串。
让我们在打印 LineString 时返回更多信息。
- 编辑你的 LineString 类并更改 repr 方法,使其返回道路名称和长度:
class LineString(BaseGeoObject):
"""表示单个线字符串。”
def repr(self):
length = self.geom.length
return "{} - {}".format(self.get_attribute('name'), length)
在这里,我们使用了 Python 的字符串格式化来组成一个可以由此方法返回的字符串。
- 运行代码并查看新的输出:
导入文件:../data/roads.shp
美国公路 395-0.16619770512
美国公路 30-0.0432070790491
州际公路 84-0.0256320861143
美国公路 6-0.336460513878
美国公路 40-0.107844768871
州际公路 272-0.0264889614357…
进程结束,退出代码为 0
尽管它比以前好得多,但它仍然有一个问题。长度是以度为单位,对我们来说意义不大,因为我们习惯于以米、英里或其他线性单位来衡量。因此,在打印长度之前,我们需要转换单位。
转换空间参考系统
以及单位
幸运的是,我们之前已经执行过此类操作,现在我们将将其适应到我们的数据模型中。
我们将在需要时才转换几何形状的坐标。为了执行转换,我们将创建一个新的实用函数,如下所示:1. 在我们的 utils 文件夹中打开 geo_functions.py 并创建一个新的函数:def transform_geometry(geom, src_epsg=4326, dst_epsg=3395):
"""将单个 wkb 几何形状进行转换。"""
:param geom: WKB 几何形状.
:param src_epsg: 源几何形状的 EPSG 代码。
:param dst_epsg: 目标几何形状的 EPSG 代码.
"""
ogr_geom = ogr.CreateGeometryFromWkb(geom)
ogr_transformation = create_transform(src_epsg, dst_epsg)
ogr_geom.Transform(ogr_transformation)
return ogr_geom.ExportToWkb()
它接受 WKB 格式的几何形状、其 EPSG 代码和 EPSG
返回具有所需输出坐标系代码的 WKB 几何形状。它执行转换并返回一个 WKB 几何形状。
现在回到模型;让我们导入这个函数并使用它。
- 在 models.py 文件的开始处编辑导入:
coding=utf-8
from future import print_function
import gdal
from shapely.geometry import Point
from shapely import wkb, wkt
from utils.geo_functions import open_vector_file
从 utils.geo_functions 导入 transform_geometry
- 现在,编辑 BaseGeoObject,以便我们的类可以继承这个新功能:class BaseGeoObject(object):
"""单个地理对象的基类。"""
def init(self, geometry, attributes=None):
self.geom = geometry
self.attributes = attributes
self.wm_geom = None
创建一个不区分大小写的属性查找表。
self._attributes_lowercase = {}
for key in self.attributes.keys():
self._attributes_lowercase[key.lower()] = key
def transformed_geom(self):
"""返回转换为 WorldMercator 坐标系统的几何形状。
"""
如果 self.wm_geom 不存在:
geom = transform_geometry(self.geom.wkb)
self.wm_geom = wkb.loads(geom)
return self.wm_geom
def get_attribute(self, attr_name, case_sensitive=False):
"""通过名称获取属性。"""
:param attr_name: 属性的名称。
:param case_sensitive: True 或 False.
"""
if not case_sensitive:
attr_name = attr_name.lower()
attr_name = self._attributes_lowercase[attr_name]
return self.attributes[attr_name]
def repr(self):
raise NotImplementedError
注意,我们选择保留几何形状在两个坐标系中。在第一次转换时,WorldMercator 中的几何形状存储在 wm_geom 属性中。下一次调用 transformed_geom 时,它将只获取属性值。这被称为 记忆化,我们将在本书的后面部分看到更多关于这种技术的例子。
根据您的应用程序,这可能是一个好习惯,因为您可能希望为特定目的使用不同的坐标系。例如,为了绘制地图,您可能希望使用经纬度,而为了进行计算,您将需要以米为单位的坐标。缺点是内存消耗更高,因为您将存储两组几何形状。
- 最后,我们回到 LineString 类,并更改其 repr 方法以使用 transformed_geom 来计算长度:
class LineString(BaseGeoObject):
"""表示单个线字符串。”
def repr(self):
return "{}-{}".format(self.get_attribute('name'), self.transformed_geom().length) 5. 运行代码并查看新的输出:
导入文件:../data/roads.shp
州际公路 3-100928.690515
州际公路 411-3262.29448315
州际公路 3-331878.76971
州际公路 3-56013.8246795.73…
处理完成,退出代码为 0
现在好多了,因为我们现在可以看到以米为单位的道路长度。但仍然不完美
because, normally, we would want the lengths in kilometres or miles. So, we need to convert the unit.
在 第一章, 准备工作环境 中,我们创建了一个美丽的函数,能够执行这些转换;我们用它来转换面积单位。以它为模板,我们将实现它以转换长度单位。
由于这是一个可以在任何应用程序的任何部分使用的函数,我们将将其放入 utils 包(即目录)中的 geo_functions.py 模块中。
- 编辑 geo_functions.py 文件,并复制粘贴我们使用的函数
第一章, 准备工作环境,用于计算和转换面积单位。
我们将保留它以供以后使用:
def calculate_areas(geometries, unity='km2'):
"""计算一系列 ogr 几何形状的面积。”
conversion_factor = {
'sqmi': 2589988.11,
'km2': 1000000,
'm': 1}
if unity not in conversion_factor:
raise ValueError(
"此单位未定义:{}".format(unity))
areas = []
for geom in geometries:
area = geom.Area()
areas.append(area / conversion_factor[unity])
return areas
- 复制此函数(复制并粘贴)并编辑它,使其类似于以下代码:def convert_length_unit(value, unit='km', decimal_places=2):
"""将给定值的长度单位进行转换。
输入是以米为单位,输出由单位设定
参数.
:param value: 以米为单位的输入值。
:param unit: 所需的输出单位。
:param decimal_places: 输出的小数位数。
"""
conversion_factor = {
'mi': 0.000621371192,
'km': 0.001,
'm': 1.0}
if unit not in conversion_factor:
raise ValueError(
"此单位未定义:{}".format(unit))
return round(value * conversion_factor[unit], decimal_places) 再次,这是一个非常通用的函数,因为你可以轻松地更改其代码以向其中添加更多的转换因子。在这里,我们还引入了 round()函数,因此我们可以看到更易读的结果。默认情况下,它将结果四舍五入到两位小数,这在大多数情况下,足以很好地表示长度。
- 返回模型并在此其他导入之后导入此新函数:
coding=utf-8
from future import print_function
import gdal
from shapely.geometry import Point
from shapely import wkb, wkt
from utils.geo_functions import open_vector_file
from utils.geo_functions import transform_geometry
from utils.geo_functions import convert_length_unit
- 现在编辑 LineString 类。我们将添加一个方便方法(我们将在本章后面了解更多关于此的内容),该方法将返回转换后的单位长度,更改 repr 值以使用它,并改进字符串格式化以显示单位并获得更好的输出:
class LineString(BaseGeoObject):
"""表示单个线字符串。”
def repr(self):
unit = 'km'
return "{} ({}{})".format(self.get_attribute('name'), self.length(unit), unit)
def length(self, unit='km'):
**"""这是一个方便的方法,它返回给定单位中线的长度。"""
:param unit: 所需的输出单位。
"""
return convert_length_unit(self.transformed_geom().length, unit)
- 再次运行代码,看看我们取得了什么成果:
导入文件:../data/roads.shp
州际公路 146(10.77 公里)
美国公路 7,美国公路 20(5.81 公里)
州际公路 295(13.67 公里)
州际公路 90(3.55 公里)
州际公路 152(18.22 公里)
州际公路 73(65.19 公里)
州际公路 20(53.89 公里)
州际公路 95(10.38 公里)
...
进程以退出代码 0 结束
几何关系
我们希望过滤掉落在给定边界(一个国家、州、城市等)内的地理缓存点。为了执行此类过滤,我们需要验证每个点,看看它是否在表示边界的多边形内部。
在地理处理中,两个几何体之间的关系由一组已知的谓词描述。这些关系非常重要,因为它们允许建立条件,从而可以进行操作和计算。
Shapely 附带了一套完整的谓词,用于分析两个几何体之间的关系。在我们进一步开发应用程序之前,让我们看看可能的关系检查。

接触
如果几何体有一个或多个共同点,但它们的内部不相交,则这是正确的。

交叉
如果两个对象相交但没有一个包含另一个,则这是正确的。

包含
这表示一个对象是否完全包含另一个对象;所有边界、线条或点都必须在第一个对象内部。
包含
如果一个几何形状包含在另一个几何形状中,则这是真的。它与包含相同,但如果您交换这两个几何形状。

等于或几乎相等
如果两个对象具有相同的边界和内部,则这是真的。几乎相等允许在测试精度的精度中配置容差。

相交
这表示一个几何形状以任何方式与另一个几何形状相交。如果以下任何关系为真,则这是真的:包含、交叉、相等、接触和包含。

不相交
这返回 true,如果两个几何形状之间没有关系。
按属性和关系过滤
现在我们知道几何形状是如何相互关联的,我们可以使用这些关系来搜索点。我们已经有导入点和表示任何可能对我们感兴趣的各种边界的多边形的方法。
随书文件附带的数据包含世界国家边界的示例,但您可以自由地在互联网上搜索对您有意义的任何数据。请记住,数据坐标应以纬度和经度表示,并且它们需要有一个名称字段。
对于我们的测试,我准备了一套特殊的全球地理藏宝点,我们将通过一个国家来过滤这些点。
建议的工作流程如下:
导入点和边界
找到我们想要使用的边界
通过该边界过滤点
将点返回给用户
为了找到我们想要的点,我们将遍历数据直到找到匹配项。迭代可能会根据数据量和每个循环上执行的操作而变得昂贵。让我们记住这一点。
工作流程的第一步已经完成,所以让我们编写代码来找到我们感兴趣的边界。如果您使用提供的数据,我们可以如下找到您国家的边界:
- 前往 BoundaryCollection 类并添加一个新的方法 get_by_name: class BoundaryCollection(BaseGeoCollection):
"""此类表示一组
地理边界。
"""
def _parse_data(self, features):
for feature in features:
geom = feature['geometry']['coordinates']
attributes = feature['properties']
polygon = wkt.loads(geom)
boundary = Boundary(geometry=polygon,
attributes=attributes)
self.data.append(boundary)
def get_by_name(self, name):
"""通过其名称属性查找对象并返回它。”
for item in self.data:
if item.get_attribute('name') == name:
return item
raise LookupError(
"未找到名为:{}".format(name)) 这种非常简单的方法遍历数据。当它找到第一个边界
其名称属性与传递给参数的名称匹配的对象,函数执行停止并返回对象。如果没有找到任何内容,将引发 LookupError。
2. 让我们玩玩它。转到文件末尾的 if name == 'main' 块并编辑它:
if name == 'main':
world = BoundaryCollection("../data/world_borders_simple.shp") print(world.get_by_name('Brazil'))
3. 尝试不同的国家名称并查看结果。如果找到了,你应该得到类似以下输出:
文件已导入: ../data/world_borders_simple.shp
巴西
进程结束,退出代码为 0
4. 如果没有找到,你应该得到一个很好的异常:
Traceback (most recent call last):
文件 "Chapter 4/code/models.py",第 153 行,在
文件 "Chapter 4/code/models.py",第 148 行,在 get_by_name 中
'Object not found with the name: {}'.format(name))
LookupError: Object not found with the name: Foo
进程结束,退出代码为 1
非常好,我们的方法工作得很好,并且还有一个额外的(几乎)意外的特性:它不仅对边界有效;它可以用来查找任何类型的 GeoObject。看看它如何只使用我们基类中可用的属性。
5. 将 get_by_name 方法移动到 BaseGeoCollection 类中,并再次测试你的代码。记住,类内部方法的顺序对类的行为是无关紧要的,但最佳实践建议你首先放置魔法方法,然后是私有方法,最后是其他方法。你的完整 BaseGeoCollection 类应该如下所示:
class BaseGeoCollection(object):
"""此类表示空间数据集合。”
def init(self, file_path=None):
self.data = []
self.epsg = None
if file_path:
self.import_data(file_path)
def add(self, other):
self.data += other.data
return self
def _parse_data(self, features):
raise NotImplementedError
def import_data(self, file_path):
"""打开与 OGR 兼容的矢量文件并解析数据。”
:param str file_path: 文件的完整路径。
"""
features, metadata = open_vector_file(file_path)
self._parse_data(features)
self.epsg = metadata['epsg']
print("File imported: {}".format(file_path))
def describe(self):
print("SRS EPSG code: {}".format(self.epsg))
print("Number of features: {}".format(len(self.data))) def get_by_name(self, name):
"""通过名称属性查找对象并返回它。”
for item in self.data:
if item.get_attribute('name') == name:
return item
raise LookupError(
"Object not found with the name: {}".format(name)) 现在,在下一步中,我们将搜索位于我们找到的边界内的点。这次,我们将在 BaseGeoCollection 类中直接创建一个方法,这样它就通过继承对 PointCollection 和 BoundaryCollection 类可用。通过这样做,我们将获得一个额外功能——我们能够通过另一个边界来过滤边界。
- 前往 BaseGeoCollection 类并添加 filter_by_boundary 方法:
...
def filter_by_boundary(self, boundary):
"""通过给定边界过滤数据"""
result = []
for item in self.data:
if item.geom.within(boundary.geom):
result.append(item)
return result
在这里,我们创建了一个名为 result 的变量,它包含一个列表来存储通过测试的对象。使用 within 谓词来测试每个项目是否在作为参数传递的边界内。在这种情况下,如果没有找到任何内容,则不会引发异常,并返回一个空列表。
- 编辑 if name == 'main':块中的测试代码:if name == 'main':
gdal.PushErrorHandler('CPLQuietErrorHandler')
world = BoundaryCollection("../data/world_borders_simple.shp") geocaching_points = PointCollection("../data/geocaching.gpx") usa_boundary = world.get_by_name('United States')
result = geocaching_points.filter_by_boundary(usa_boundary) for item in result:
print(item)
在测试过程中,创建了两个实例,一个来自 BoundaryCollection 类,另一个来自 PointCollection 类。数据文件作为参数传递。然后找到感兴趣的国家的信息,并存储在 usa_boundary 变量中。然后,将此变量传递给 filter_by_boundary 方法。
- 运行代码。你应该会看到一个长长的 geocache 列表,如下所示:
-78.90175 42.89648 - LaSalle Park No 1
-78.89818 42.89293 - LaSalle Park No 2
-78.47808 43.02617 - A Unique Walk in Akron
-78.93865 42.95982 - A view of Strawberry Island
-78.90007 42.7484 - A View to a Windmill
-79.07533 43.08133 - A Virtual Made in the Mist
-74.43207 43.86942 - Adirondack Museum Guestbook…
Process finished with exit code 0
如预期的那样,它打印了一个 Geocache 对象的列表,其表示由 repr 方法给出,即它们的坐标和名称。
通过多个属性过滤
下一步是通过它们的属性搜索 geocaching 点。例如,我们可能想要通过 geocache 的作者、找到 geocache 的难度等级等来过滤点。
我们将借鉴那些允许我们通过 GeoObject 的名称属性获取 GeoObject 和通过多边形过滤的方法所使用的技巧。这里的区别是我们必须允许我们想要过滤的属性作为参数传递,并且我们希望有组合多个字段的能力。
- 让我们从在 BaseGeoCollection 类中添加一个简单的过滤方法开始:
...
def filter(self, attribute, value):
"""通过属性过滤集合。
:param attribute: 要过滤的属性名称。
:param value: 过滤值。
"""
result = []
for item in self.data:
if item.get_attribute(attribute) == value:
result.append(item)
return result
此方法接受两个参数:我们想要过滤的属性名称以及该属性需要具有的值以通过过滤。与 get_by_name 不同,此过滤函数将找到的每个对象累积到一个列表中,并返回此列表。
- 要测试过滤方法,编辑 if name == 'main':块。我们将过滤难度级别为 1 的地理藏点:
if name == 'main':
gdal.PushErrorHandler('CPLQuietErrorHandler')
points = PointCollection("../data/geocaching.gpx") result = points.filter('difficulty', '1')
points.describe()
print("找到 {} 个点".format(len(result)))
- 运行代码。你应该得到以下输出:
导入的文件:../data/geocaching.gpx
SRS EPSG 代码:4326
特征数量:112
找到 38 个点
处理完成,退出代码为 0
在总共 112 个点中,有 38 个符合我们的标准。
链式过滤
这一部分值得单独说明,因为我们将要使用一个非常实用的 Python 技术,你很可能不止一次需要它来解决你的地理处理挑战。
到目前为止,我们可以应用单个过滤器,它将返回一个对象列表。如果我们想应用多个过滤器,我们可以简单地让过滤器函数返回另一个包含结果的集合对象,而不是返回一个列表。这样,我们可以使从一次过滤的结果中再次过滤成为可能,从而缩小结果范围。
除了出奇地简单外,这个解决方案在处理效率方面也非常高,因为在每次过滤过程中,结果都会更小,迭代次数也会减少。
Python 允许函数调用链式操作。这意味着我们不需要将每个步骤存储到变量中。我们可以简单地以非常优雅和直观的模式将每个调用依次放置,如下所示:
my_points = points.filter('difficulty', '1').filter('status', 'Available') 注意这是一个“与”条件。它将返回同时满足两个过滤条件的点。但由于我们在 BaseGeoCollection 类中实现了 add 方法,我们可以轻松地实现“或”类型的过滤:
my_points = points.filter('difficulty', '1') + points.filter('difficulty',
'2')
- 让我们的方法返回一个新的实例以使这成为可能。编辑 BaseGeoCollection 类的 filter 方法:
...
def filter(self, attribute, value):
"""通过属性过滤集合。
:param attribute: 要过滤的属性名称。
:param value: 过滤值。
"""
result = self.class()
for item in self.data:
if getattr(item, attribute) == value:
result.data.append(item)
return result
现在,结果是一个与调用该方法的原实例相同的类的实例,因为 class 是一个包含创建实例的类的属性。由于我们正在使用继承,这确保了结果与数据类型相同。尽管这是一个非常简单的解决方案,但它非常有效。让我们试试:
- 编辑 if name == 'main':块,以便我们可以过滤出符合两个条件(且条件)的点:
if name == 'main':
gdal.PushErrorHandler('CPLQuietErrorHandler') points = PointCollection("../data/geocaching.gpx") result = points.filter('difficulty', '1').filter('container',
'Virtual')
points.describe()
result.describe()
- 运行以下代码:
导入的文件:../data/geocaching.gpx
SRS EPSG 代码:4326
特征数量:112
SRS EPSG 代码:None
特征数量:34
Process finished with exit code 0
从上一个测试中,我们知道有 38 个难度为 1 的点,现在我们有 34 个
points 因为那些 38 个点,其中四个没有容器 = 虚拟。
- 这次尝试使用或条件再进行另一个测试:
if name == 'main':
gdal.PushErrorHandler('CPLQuietErrorHandler')
points = PointCollection("../data/geocaching.gpx") result = points.filter('difficulty', '1') + points.filter(
'difficulty', '2')
points.describe()
result.describe()
- 运行以下代码:
导入的文件:../data/geocaching.gpx
SRS EPSG 代码:4326
特征数量:112
SRS EPSG 代码:None
特征数量:50
Process finished with exit code 0
这次,这些难度为 1 的 38 个点与另外 12 个难度为 2 的点合并。
集成到应用程序中
随着我们继续使用更高层次的抽象级别工作,想想我们的应用程序组织。我们有两种类型的数据,并且有一个具有高级功能的 GeocachingApp 类。在这个阶段,我们想要的是使应用程序能够以我们在测试中做的方式过滤,但要以简单直接的方式进行。
看看应用程序现在的样子:
class GeocachingApp(PointCollection):
def init(self, data_file=None, my_location=None):
"""应用程序类。
:param data_file: 一个与 OGR 兼容的文件
with geocaching points.
:param my_location: 您的位置坐标。
"""
super(GeocachingApp, self).init(file_path=data_file)
self._datasource = None
self._transformed_geoms = None
self._my_location = None
self.distances = None
if my_location:
self.my_location = my_location
@property
def my_location(self):
return self._my_location
@my_location.setter
def my_location(self, coordinates):
self._my_location = transform_points([coordinates])[0]
def calculate_distances(self):
"""计算两点之间的距离。
一组点和给定位置。
:return: 按相同顺序返回距离列表
这些点。
"""
xa = self.my_location[0]
ya = self.my_location[1]
points = self._transformed_geoms
distances = []
for geom in points:
point_distance = math.sqrt(
(geom.GetX() - xa)**2 + (geom.GetY() - ya))
distances.append(point_distance)
return distances
def find_closest_point(self):
"""找到给定位置最近的点。
返回该点上的缓存。
:return: 包含点的 OGR 要素。
"""
第一部分。
distances = self.calculate_distances()
index = np.argmin(distances)
第二部分。
layer = self._datasource.GetLayerByIndex(0)
feature = layer.GetFeature(index)
打印 "最近点在:{}m".format(distances[index]) return feature
使用继承来给应用程序提供 PointCollection 类中包含的功能。但这个模式将不再适用,因为我们现在有两种类型的数据。我们必须移除继承并采取不同的方法。
我们将要做的就是存储集合类(PointCollection 和 BoundaryCollection)的实例,并实现将它们联系起来的方法,就像在链式过滤器主题的测试中做的那样。
让我们从导入和类的定义开始:
- 打开 geocaching_app.py 文件,并编辑文件开头的导入部分,以包含新的类:
编码格式:utf-8
导入 gdal
导入 numpy as np
导入 math
从 utils.geo_functions 导入 transform_points
从 models 导入 PointCollection, BoundaryCollection
- 现在,编辑 GeocachingApp 类定义和 init 方法,如下所示:
class GeocachingApp(object):
def init(self,
geocaching_file=None,
boundary_file=None,
my_location=None):
"""应用程序类。
:param geocaching_file: 一个 OGR 兼容的文件
与地理藏宝点。
:param boundary_file: 一个包含边界的文件。
:param my_location: 你的位置坐标。
"""
self.geocaching_data = PointCollection(geocaching_file)
self.boundaries = BoundaryCollection(boundary_file)
self._my_location = None
如果 my_location:
self.my_location = my_location
继承已被移除,现在数据存储在 geocaching_data 和
boundaries 属性。如果用户将包含地理藏宝数据或边界数据的文件传递给 GeocachingApp 类,这些相同的文件将作为参数传递给 PointCollection 和 BoundaryCollection 的创建。
使用你现在拥有的,你已经开始做任何类型的过滤了。你只需要访问 geocaching_data 和 boundaries,并做我们之前所做的那样。让我们试试。
- 前往文件末尾的 if name == "main": 行,并编辑代码:
如果 name == "main":
my_app = GeocachingApp("../data/geocaching.gpx",
"../data/world_borders_simple.shp")
usa_boundary = my_app.boundaries.get_by_name('United States') result = my_app.geocaching_data.filter_by_boundary(
usa_boundary)
打印(result)
- 现在运行它。记住,每次你想运行不同的文件时,你需要按 Alt + Shift + F10 并在弹出窗口中选择文件。你应该再次看到包含地理藏宝点的列表输出。
但让我们假设存在一种需要多次使用的过滤功能,或者,也许有一种你希望明确的过滤功能。
按照相同的示例,假设我们正在按国家名称进行过滤。
我们可以使用 GeocachingApp 类,这是我们代码中抽象层次最高的一层,来实现这个或任何其他高级过滤方法。
- 将此方法添加到 GeocachingApp 类:
...
def filter_by_country(self, name):
"""通过给定的名称筛选国家。
:param name: 边界的名称(例如,县名)
:return: PointCollection
"""
boundary = self.boundaries.get_by_name(name)
return self.geocaching_data.filter_by_boundary(boundary)
在计算机编程中,这也被称为便利方法。它是为了方便解决更复杂的问题或避免样板代码(即避免代码重复)而创建的方法。
总结
在本章中,我们看到了几何形状之间不同类型的关系可以被测试,并且这些测试可以在程序中用来解决问题。
为了通过多边形进行筛选,我们首先使用与点相同的代码将这些多边形导入系统,但这次我们使用了 Shapely 来抽象多边形和点的几何形状。最后,我们使用几何关系来搜索多边形内的点。
然后,我们实现了一种通过名称属性筛选数据的方法,并使其能够通过对象的任何属性或任何属性组合进行筛选。
最后,我们将应用程序类适配到新的更改,并看到我们可以在其中添加便利方法以简化一些任务。
在下一章,我们将开始着手制作地图制作应用程序,并创建可视化我们数据的方法。
第五章:制作地图
在本章中,我们将启动一个新的应用程序,并使用它从矢量数据生成漂亮的地图。
为了生成这些地图,我们将使用 Mapnik,这是世界上使用最广泛的地图包之一。目标是了解它是如何工作的,并使其适应制作易于使用的地图应用程序。
在前面的章节中,我们创建了一些非常实用的类来抽象地理数据;我们将使这个应用程序能够消费此类数据。
我们将涵盖以下主题:
了解 Mapnik 及其工作原理
比较使用纯 Python 和 XML 定义地图时的差异
使用 Python 对象作为 Mapnik 的数据源
将 Mapnik 抽象为高级应用程序
了解 Mapnik
Mapnik 是我们用来制作地图的工具。它是一个非常强大的地图库,被许多网站使用。
在本节第一个主题中,我们将进行一些实验来了解 Mapnik 的功能。
现在,我们将通过几个实验来了解 Mapnik 的工作原理。
首先,让我们组织本章的代码:
-
在你的 geopy 项目中,复制 Chapter4 文件夹并将其重命名为 Chapter5。
-
在 Chapter5 文件夹内,创建一个名为 mapnik_experiments 的新文件夹。为此,在 Chapter5 文件夹中右键单击并选择新建 | 目录。
-
仍然在 Chapter5 中,创建一个名为 output 的新文件夹;我们将把创建的地图和图像放入该文件夹。
使用纯 Python 制作地图
Mapnik 有两种定义地图的方式;一种使用纯 Python 代码,另一种使用 XML 文件。
Mapnik 的 Python API 非常广泛,几乎封装了该包的所有功能。在接下来的步骤中,我们将仅使用 Python 代码进行地图制作实验。
1. 在 mapnik_experiments 文件夹内创建一个名为 mapnik_python.py 的 Python 文件。
2. 将以下代码输入到 mapnik_python.py 文件中:
coding=utf-8
导入 mapnik 库
创建地图
map = mapnik.Map(800, 600)
设置地图的背景颜色。
map.background = mapnik.Color('white')
创建一个样式和一个规则。
style = mapnik.Style()
rule = mapnik.Rule()
创建一个 PolygonSymbolizer 来填充多边形。
将其添加到规则中。
polygon_symbolizer = mapnik.PolygonSymbolizer(
mapnik.Color('#f2eff9'))
rule.symbols.append(polygon_symbolizer)
创建一个 LineSymbolizer 来设置多边形的边框样式。
将其添加到规则中。
line_symbolizer = mapnik.LineSymbolizer(
mapnik.Color('rgb(50%,50%,50%)'), 0.1)
rule.symbols.append(line_symbolizer)
将规则添加到样式。
style.rules.append(rule)
将样式添加到地图。
map.append_style('My Style', style)
data = mapnik.Shapefile(file='../../data/world_borders_simple.shp')
创建一个名为'world'的图层。
layer = mapnik.Layer('world')
设置图层数据源并将样式添加到图层。
layer.datasource = data
layer.styles.append('My Style')
将图层添加到地图。
map.layers.append(layer)
将地图缩放到所有图层范围。
map.zoom_all()

将地图写入图像。
mapnik.render_to_file(map,'../output/world.png', 'png')
3. 现在运行代码;按Alt + Shift + F10并选择 mapnik_python。
4. 在您的输出文件夹中应该有一个名为 world.png 的新文件。您可以在 PyCharm 中查看此图像;只需双击它。您应该看到这个:恭喜您创建了这张第一张美丽的地图;注意渲染的优越质量和 Mapnik 完成工作的速度。
使用样式表制作地图
除了使用 Python 代码外,地图样式、图层和其他定义也可以放在一个 XML 文件中。让我们试试这个:
1. 在 mapnik_experiments 文件夹内,创建一个名为 map_style.xml 的新文件。
2. 输入以下代码:
这是您地图的样式定义。尽管 PyCharm 是一个 Python IDE,但它也能识别许多文件类型,包括 XML;它应该会帮助您处理标签,并给代码应用漂亮的颜色。
现在需要 Python 代码来生成此地图:
3. 在 mapnik_experiments 文件夹内创建一个名为 mapnik_xml.py 的 Python 文件,并输入以下代码:
coding=utf-8
import mapnik
map = mapnik.Map(800, 600)
mapnik.load_map(map, 'map_style.xml')
map.zoom_all()
mapnik.render_to_file(map, '../output/world2.png')
-
运行此文件。请记住,要运行上一个文件之外的不同文件,您需要按 Alt + Shift + F10 并选择它。
-
打开输出文件夹中的生成图像(world2.png);你应该看到与之前完全相同的结果。
在 Python 和 XML 中设置地图样式几乎具有相同的功能。除了少数非常具体的情况外,你可以使用其中任何一个来获得完全相同的结果。
在这些简单的示例中,使用 Python 或 XML 时需要注意两件事:代码可读性和组织。查看 XML 代码,你应该看到地图、样式和规则具有树状结构;这里非常清晰,但在纯 Python 定义中可能会变得混乱,并可能导致错误。
这是一个非常简单的地图,但随着你添加更多的规则和符号化器,使用纯 Python 来理解事情开始变得非常混乱。
另一个重要点是,将地图创建逻辑与样式分开是一个好主意。我们将在下一个主题中看到这样做如何帮助保持代码非常干净和可重用。
创建生成实用函数
maps
现在我们将创建第一个将组成我们应用程序的函数。
-
仍然在 mapnik_experiments 文件夹中,创建一个新的文件:map_functions.py。
-
将以下代码插入到该文件中:
coding=utf-8
import mapnik
def create_map(style_file, output_image, size=(800, 600)):
"""从 XML 文件创建地图并将其写入图像。
:param style_file: Mapnik XML 文件。
:param output_image: 输出图像文件的名称。
:param size: 地图的像素大小。
"""
map = mapnik.Map(*size)
mapnik.load_map(map, style_file)
map.zoom_all()
mapnik.render_to_file(map, output_image)
if name == 'main':
create_map('map_style.xml', '../output/world3.png',
size=(400, 400))
我们在这里所做的就是将地图生成代码打包到一个函数中,我们可以在将来重用它。它接受两个必需参数:XML 样式文件和 Mapnik 将结果写入的图像文件名。
第三个可选参数是创建的地图的大小;你可以传递一个包含地图宽度和高度的像素值的列表或元组。这个元组或列表随后使用 * 符号解包到 mapnik.Map 参数中。
最后,我们再次使用了 if __name__ == '__main__': 技巧来测试代码。
请记住,这个 if 块内部的所有内容仅在文件直接调用时运行。另一方面,如果这个文件作为模块导入,则此代码将被忽略。
如果需要更多关于该技术的信息,请查看第二章中的创建应用程序入口点部分,Geocaching 应用程序。
在运行时更改数据源
这是一个有用的函数;现在我们可以用一行代码从 XML 文件创建地图。但是有一个缺陷:数据源(将要使用的 shapefile)在 XML 中是硬编码的。假设我们想要为多个 shapefile 生成地图;对于每个文件,我们都需要更改 XML,这阻碍了批处理操作的执行。
幸运的是,有两种方法可以更改 Mapnik 将使用的数据源文件,而无需手动更改 XML。我们可以编写代码来为我们编辑 XML,或者我们可以在地图定义中混合 XML 和 Python。
Mapnik 的 Map 对象有几个属性可以访问。目前,我们感兴趣的是访问层,因为层包含我们想要定义或更改的数据源。
每个 Map 实例都包含一个 layers 属性,该属性返回一个包含地图中定义的所有层的 Layers 对象。这个对象的行为类似于一个 Layer 对象的列表,其项可以通过迭代或通过索引检索。反过来,Layer 对象包含名称和数据源属性。让我们看看它是如何工作的:注意
检查 Mapnik API 文档:mapnik.org/docs/v2.2.0/api/python/
在那里你可以找到所有可用的类、方法和属性。
- 修改你的函数,以便我们可以检查地图对象的属性:def create_map(style_file, output_image, size=(800, 600)):
"""从 XML 文件创建地图并将其写入图像。
:param style_file: Mapnik XML 文件。
:param output_image: 输出图像文件名。
:param size: 地图像素大小。
"""
map = mapnik.Map(*size)
mapnik.load_map(map, style_file)
layers = map.layers
layer = layers[0]
print(layer)
print(layer.name)
print(layer.datasource)
map.zoom_all()
mapnik.render_to_file(map, output_image)
突出的代码获取 layers 对象及其中的第一个层(索引为 0),然后打印它、它的名称和数据源属性。
- 重新运行代码,你应该得到以下输出:
<mapnik._mapnik.Layer 对象在 0x01E579F0>
world
<mapnik.Datasource 对象在 0x01F3E9F0>
处理完成,退出代码为 0
正如你在输出中看到的,第一层是 XML 中定义的世界层,它有一个数据源。这个数据源就是我们希望在代码执行期间设置或修改的内容。
- 再次进行测试。打开 map_style.xml 文件,从定义中删除数据源,如下所示:
<Map 背景颜色="白色">
<Layer 名称="world">
- 再次运行代码并查看输出:
<mapnik._mapnik.Layer 对象在 0x01DD79F0>
world
None
处理完成,退出代码为 0
现在,当我们打印数据源属性时,它显示为 None,因为我们从定义中移除了它;此外,图像(world3.png)为空,因为没有数据要显示。现在我们将在 Python 代码中定义它。
- 编辑
map_functions.py文件:
coding=utf-8
import mapnik
def create_map(shapefile, style_file, output_image, size=(800, 600)):
"""从 XML 文件创建地图并将其写入图像。
:param shapefile: 包含地图数据的 Shapefile。
:param style_file: Mapnik XML 文件。
:param output_image: 输出图像文件的名称。
:param size: 地图在像素中的大小。
"""
map = mapnik.Map(*size)
mapnik.load_map(map, style_file)
data source = mapnik.Shapefile(file=shapefile)
layers = map.layers
layer = layers[0]
layer.datasource = data source
map.zoom_all()
mapnik.render_to_file(map, output_image)
if name == 'main':
create_map('../../data/world_borders_simple.shp',`
'map_style.xml', '../output/world3.png',
size=(400, 400))
函数中新增的必需参数是包含数据的 shapefile 的名称。在代码中,我们从这个文件创建一个 Mapnik 数据源,获取第一层,并将其数据源设置为创建的那个。运行代码并查看输出,你应该会看到一个渲染好的世界地图。除了设置数据源外,还可以结合 XML 和 Python 以任何方式更改地图定义。
自动预览地图
当我们开始玩转地图样式时,每次想要查看结果时手动打开图像可能会有些无聊。因此,我们将编写一个函数,在运行代码时自动为我们打开并显示图像。为此,我们将使用Open Computer Vision(OpenCV)包。
- 在
map_functions.py文件的开始处导入包:import mapnik
import cv2
- 在
create_map函数之前创建这个新函数:def display_map(image_file):
"""打开并显示地图图像文件。
:param image_file: 图片的路径。
"""
image = cv2.imread(image_file)
cv2.imshow('image', image)
cv2.waitKey(0)
cv2.destroyAllWindows()
- 现在将我们的测试改为调用该函数;为此,编辑
if __name__ == '__main__':块:
'main': block:
if name == 'main':
map_image = '../output/world3.png'
创建地图(create_map('../../data/world_borders_simple.shp',)
'map_style.xml',map_image, size=(400, 400))
display_map(map_image)
- 运行代码。现在你应该会看到一个包含地图图像的窗口弹出:

我们现在不会深入探讨 OpenCV 的功能;只需注意 cv2.waitKey(0)会暂停代码执行,直到按下任意键或窗口关闭。
地图样式
现在我们有一个生成地图的函数以及预览它们的一种简单方法,我们将尝试不同的样式选项:
- 首先,让我们生成一个更大的地图,这样我们可以更好地看到变化。编辑 map_functions.py 文件末尾的 if name == 'main':块,更改 create_map 函数调用的大小参数:
if name == 'main':
map_image = '../output/world3.png'
create_map('../../data/world_borders_simple.shp',
'map_style.xml',map_image, size=(1024, 500))
display_map(map_image)

地图样式
地图是绘图的画布;可以更改背景颜色或背景图像、坐标参考系统以及一些其他选项。
让我们尝试更改背景:
- 在文件 map_style.xml 中编辑地图标签以更改背景颜色。你可以使用十六进制值、颜色名称或 RGB 组合。以下是一个示例:
- 运行代码并查看结果。
样式遵循画家模型。这意味着事物是按照它们在文件中的顺序绘制的,因此图案是在多边形填充上绘制的。

线条样式
线条(包括多边形边界)是通过 LineSymbolizer 标签和 LinePatternSymbolizer 标签进行样式的。在接下来的示例中,我们将地图恢复到其初始样式并放大,以便更好地看到选项如何影响生成的地图,如下所示:
- 通过删除背景图像和多边形图案来编辑样式。通过更改最大范围进行放大:
<地图 背景颜色="white" 最大范围="-21,68,66,28">
<样式 name="My Style">
<规则>
<多边形符号 izer fill="#f2eff9" />
<线符号 izer stroke="rgb(50%,50%,50%)" stroke-width="0.1" />
</规则>
</样式>
<图层 name="world">
<样式名称>My Style</样式名称>
</图层>
</地图>
- 现在更改 LineSymbolizer 标签:
<线符号 izer stroke="red" stroke-width="3.0" /> 3. 运行代码并查看线条如何变粗并变为红色。
你可能会注意到一些奇怪的边缘和点,因为这是一张低分辨率的全球地图,线条太粗。我们可以通过减少厚度和使用平滑参数来改进这张地图。
- 再次编辑 LineSymbolizer 标签并运行代码。现在你应该有一个更清晰的地图:

<线符号 izer stroke="red" stroke-width="1.0" smooth="0.5" />

文本样式
现在,让我们将国家名称添加到我们的地图中。为此,我们将使用文本符号 izer 标签:
- 使用以下代码更改 map_style.xml 文件:
<地图 背景颜色="white" 最大范围="-21,68,66,28">
<样式 name="My Style">
<规则>
<多边形符号 izer fill="#f2eff9" />
<线符号 izer stroke="red" stroke-width="1.0" smooth="0.5" />
<文本符号 izer face-name="DejaVu Sans Book" size="10"
填充="black" 外围填充= "white"
外围半径="1" 位置="内部"
allow-overlap="false">[姓名]
</文本符号 izer>
</规则>
</样式>
<图层 name="world">
<样式名称>My Style</样式名称>
</图层>
</地图>
- 运行代码并查看结果:
向地图添加图层
我们看到可以使用 Python 更改地图的数据源。在 Mapnik 地图中,数据源位于图层内部或地图内部;为了简化,我们将仅使用图层来保存数据源。
如果我们想添加多个数据源(例如,点、线、多边形或图像),我们需要添加更多图层。作为一个例子,我们将添加我们在前几章中看到的地理藏宝点到地图上。
第一步是完全从 XML 文件中删除图层定义。这将完成将代码分为两个类别的分离:XML 仅包含样式,而 Python 代码处理数据和地图创建。
其次,我们将更改 create_map 函数,使其向地图添加图层。这个更改将只在我们完全在应用程序上实现此功能之前作为一个实验:
- 在 mapnik_xml.xml 文件中,从定义中删除图层,将样式名称更改为 style1,并为点添加新的样式。同时,更改地图的范围以聚焦于点。它应该像这样:
<地图 背景颜色="white" 最大范围="-81,45,-69,40">
<样式 name="style1">
<规则>
<多边形符号 izer fill="#f2eff9" />
<线符号 izer stroke="red" stroke-width="1.0" smooth="0.5" />
<文本符号 izer face-name="DejaVu Sans Book" size="10"
fill="black" halo-fill= "white"
halo-radius="1" placement="interior"
allow-overlap="false">[NAME]
2. 在 map_functions.py 文件中,更改你的 create_map 函数和 if name == 'main': 块。完整的代码应如下所示:
coding=utf-8
import mapnik
import cv2
def display_map(image_file):
"""打开并显示地图图像文件。
:param image_file: 图像的路径。
"""
image = cv2.imread(image_file)
cv2.imshow('image', image)
cv2.waitKey(0)
cv2.destroyAllWindows()
def create_map(shapefile, gpx_file, style_file, output_image, size=(800, 600)):
"""从 XML 文件创建地图并将其写入图像。
:param shapefile: 包含地图数据的形状文件。
:param style_file: Mapnik XML 文件。
:param output_image: 输出图像文件的名称。
:param size: 地图像素大小。
"""
map = mapnik.Map(*size)
mapnik.load_map(map, style_file)
layers = map.layers
添加形状文件。
world_datasource = mapnik.Shapefile(file=shapefile)
world_layer = mapnik.Layer('world')
world_layer.datasource = world_datasource
world_layer.styles.append('style1')
layers.append(world_layer)
添加形状文件。
points_datasource = mapnik.Ogr(file=gpx_file, layer='waypoints') points_layer = mapnik.Layer('geocaching_points')
points_layer.datasource = points_datasource
points_layer.styles.append('style2')
layers.append(points_layer)
map.zoom_all()
mapnik.render_to_file(map, output_image)
if name == 'main':
map_image = '../output/world3.png'
create_map('../../data/world_borders_simple.shp',
'../../data/geocaching.gpx',
'map_style.xml',map_image, size=(1024, 500))
display_map(map_image)
现在函数接受两个文件:一个包含世界边界的形状文件和一个包含航点的 GPX 文件。对于每个文件,创建一个包含它的数据源和图层,并将其添加到地图图层列表中。我们还使用在 XML 中定义的样式名称定义图层样式。
3. 运行代码;你应该会看到默认的点符号化样式下渲染在世界边界上的点:

点样式
现在我们将改进点的视觉表示:1. 编辑 map_style.xml 文件并更改点样式:
我们介绍了使用可缩放矢量图形(SVG)文件来表示点的用法;这种文件的优势在于,由于它由矢量组成而不是像素,因此可以缩放或缩放而不会失真。
由于我们使用的 SVG 文件太大,不适合我们的地图,因此使用了 transform 参数来缩放图像。
注意
你可以在以下位置了解更多关于 SVG 转换的信息
2. 运行你的代码并查看结果:

注意
如果你的项目需要符号,你可以在名词项目中找到一个很好的集合,该项目汇集了来自世界各地的社区创作,网址为
我们已经有一个漂亮的点表示,现在我们将添加更多关于它们的信息。
使用 Python 对象作为数据源
提示
Mapnik for Windows 没有附带 Python 数据源插件,将为 Windows 用户提供一个解决方案;只需按照以下步骤操作。
在 Mapnik 内部,数据通过数据源对象表示。该对象负责访问数据源(例如,包含数据的文件、数据库等)并将该数据源提供的功能转换为特征对象。反过来,特征对象包含几何形状和多个属性(属性)。这种组织方式与我们之前在第四章,“改进应用搜索功能”主题中看到的非常相似,即地理数据的表示。
如果我们能够黑入数据源并提供我们想要的功能,我们就能够使 Mapnik 使用我们提供的 Python 对象作为数据源。
使用 Python 对象作为数据源(例如,而不是文件)的优点是,我们可以在数据上执行任何类型的转换和分析,然后将它馈送到 Mapnik,而无需将其保存到磁盘。通过这样做,我们保持数据在内存中,提高应用程序的性能,并使其更加灵活和易于使用。
幸运的是,Mapnik 已经提供了一个用于此类操作的类;正如你可能已经猜到的,它被称为 PythonDatasource。
在我们准备构建应用程序的过程中,在这一步中,我们将创建一个继承自 mapnik.PythonDatasource 并实现其所需方法的类。
首先,我们将查看 Mapnik 的源代码,以便了解 PythonDatasource 类的逻辑。
1. 前往你的 Chapter5 文件夹,创建一个名为 my_datasource.py 的文件。
2. 将此代码插入该文件:
coding=utf-8
import mapnik
test_datasource = mapnik.PythonDatasource()
3. 现在点击 PythonDatasource 上的任何位置,将光标放在它上面,然后按Ctrl
- B. 或者,在 PythonDatasource 上的任何位置右键单击,然后选择转到 | 声明。
这将为您打开并显示类声明。
- 我将逐部分讲解这个类,并在每个解释前放置类的摘录。如果您不在电脑附近,请不要担心。我会在每个解释前放置类的摘录:class PythonDatasource(object):
"""这是一个 Python 数据源的基类。
可选参数:
envelope:mapnik.Box2d (minx, miny, maxx, maxy) 边界
的数据源,默认为(-180,-90,180,90)
geometry_type:DataGeometryType 枚举值之一,默认为 Point
data_type:DataType 枚举之一,默认为 Vector
"""
def init(self, envelope=None, geometry_type=None,
data_type=None):
self.envelope = envelope or Box2d(-180, -90, 180, 90)
self.geometry_type = geometry_type or DataGeometryType.Point self.data_type = data_type or DataType.Vector
这是类的声明和 init 方法;创建类的参数都是可选的,但如果我们需要,我们可以定义边界(即边界框)和两个重要参数:
geometry_type: 可以是 Collection、LineString、Point 或 Polygon 数据类型:可以是 Vector 或 Raster
def features(self, query):
"""返回一个可迭代对象,该对象生成在传递的查询内的 Feature 实例。
必要参数:
query:指定区域的 Query 实例
features 应该返回
"""
return None
这是 PythonDatasource 工作的关键方法。该方法应返回一个包含特征的可迭代对象。
可迭代对象是任何可以在 for 循环中使用的 Python 对象,或者,如 Python 词汇表所述,任何能够一次返回其成员的对象。它可以是一个列表、一个元组、一个字典等等。
尽管文档字符串中的描述,此方法为空并返回 None。它是表明它应该在子类中实现,以及如何创建此可迭代对象完全取决于程序员的指示:
注意
查看 Python 词汇表以获取有关新术语或您仍然感到困惑的术语的信息:docs.python.org/2/glossary.html.
def features_at_point(self, point):
"""很少使用。返回一个可迭代对象,该对象生成指定点的 Feature 实例。”
return None
这是一个更方便的方法,所以我们不会使用它。
@classmethod
def wkb_features(cls, keys, features):
"""这是一个包装迭代器生成对的便利函数
将 WKB 格式的几何形状和键值对字典转换为 mapnik 特征。返回此值。
PythonDatasource.features()传递给它一个键序列
以出现在输出中,以及生成特征的迭代器。
例如,一个派生类可能有一个如下的 features()方法:
def features(self, query):
... 创建 WKB 特征 feat1 和 feat2
return mapnik.PythonDatasource.wkb_features(
keys = ( 'name', 'author' ),
features = [
(feat1, { 'name': 'feat1', 'author': 'alice' }),
(feat2, { 'name': 'feat2', 'author': 'bob' }),
]
)
"""
ctx = Context()
[ctx.push(x) for x in keys]
def make_it(feat, idx):
f = Feature(ctx, idx)
geom, attrs = feat
f.add_geometries_from_wkb(geom)
for k, v in attrs.iteritems():
f[k] = v
return f
return itertools.imap(make_it, features, itertools.count(1))
@classmethod
def wkt_features(cls, keys, features):
"""一个便利函数,用于包装一个生成 WKT 格式几何形状和键值对字典对的迭代器
转换为 mapnik 特征。从
PythonDatasource.features(),传递一个键序列
以便出现在输出中,并生成特征迭代器。
例如,可能有一个 features() 方法在
类似以下的派生类:
def features(self, query):
... 创建 WKT 特征 feat1 和 feat2
return mapnik.PythonDatasource.wkt_features(
keys = ( 'name', 'author' ),
features = [
(feat1, { 'name': 'feat1', 'author': 'alice' }),
(feat2, { 'name': 'feat2', 'author': 'bob' }),
]
)
"""
ctx = Context()
[ctx.push(x) for x in keys]
def make_it(feat, idx):
f = Feature(ctx, idx)
geom, attrs = feat
f.add_geometries_from_wkt(geom)
for k, v in attrs.iteritems():
f[k] = v
return f
return itertools.imap(make_it, features, itertools.count(1)) 这些是两个便利函数(或方法)。如果你不记得便利方法是什么,可以查看 第四章,Improving the App Search Capabilities) 中的 Integrating with the app 部分。我们在那里创建了一个。
这些方法是从包含几何形状和字典中属性列表(或元组)的列表创建 Mapnik 特征的可迭代对象的简单快捷方式,一个来自 WKT 几何形状,另一个来自 WKB 几何形状(如果你需要,可以查看 第四章,Improving the App Search Capabilities) 中的 Knowing well-known text 部分)。
一个注意事项是,这些不是实例方法;它们是类方法。注意
@classmethod 在方法名之前;这是一个装饰器,它改变了方法的行为。
我不会深入讲解类方法和装饰器(那需要整整一章的内容)。我们只需要知道,我们是从类而不是从实例调用这个方法,使用 PythonDatasource.wkt_features() 或
PythonDatasource.wkb_features().
在实践中,我们需要做的是创建一个从 PythonDatasource 继承并重新实现其特征方法的类。让我们从类骨架开始,然后稍后我们将回到我们在 第二章,The Geocaching App,第三章,Combining Multiple Data Sources,和 第四章,Improving the App Search Capabilities 中构建的类,并将它们用作特征的来源:1. 编辑 my_datasource.py;删除之前的代码并添加新的类:
coding=utf-8
import mapnik
class MapDatasource(mapnik.PythonDatasource):
"""Mapinik 的 PythonDatasource 的实现。”
def features(self, query=None):
raise NotImplementedError
我们首先做的事情是将查询参数设置为可选参数;我们不会移除它,因为存在破坏兼容性的风险。然后函数仅抛出一个异常,表示尚未实现。
导出地理对象
在我们继续之前,作为为 Windows 用户解决方法的一部分,我们需要将我们的地理对象导出为文件。
我们将使用 GeoJSON 文件格式。它是导出地理数据的好选择,因为:
它是可读的
这是一个开放标准
编写导出 GeoJSON 的代码很容易
Mapnik 可以导入它
属性/属性可以有多个级别
这里你可以看到我们之前在第三章中看到的 GeoJSON 文件示例
结合多个数据源 – 地理数据是如何表示的。你不需要输入它,我们只是用它作为参考来编写我们的导出代码:
{"type": "FeatureCollection",
"features": [
{"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [102.0, 0.5]},
"properties": {"prop0": "value0"}
},
{"type": "Feature",
"geometry": {
"type": "LineString",
"coordinates": [[102.0, 0.0], [103.0, 1.0], [104.0, 0.0]]
},
"properties": {
"prop0": "value0", "prop1": 0.0
}
},
{"type": "Feature",
"geometry": {
"type": "Polygon",
"coordinates": [
[[100.0, 0.0], [101.0, 0.0], [101.0, 1.0],
[100.0, 1.0], [100.0, 0.0] ]
]
},
"properties": {"prop0": "value0",
"prop1": {"this": "that"}
}
}
]
}
查看文件,我们可以看到我们创建的地理对象具有便于将其导出为该文件格式的特性。如果我们认为 BaseGeoObject 是一个 GeoJSON "Feature",而 BaseGeoCollection 是一个
"FeatureCollection",它很容易开始:1. 打开你的 models.py 文件,转到 BaseGeoObject 类。添加 export_geojson_feature 方法:
...
def export_geojson_feature(self):
"""将此对象导出为字典格式,作为 GeoJSON 特征。
"""
feature = {
"type": "Feature",
"geometry": mapping(self.geom),
"properties": self.attributes}
return feature
映射函数调用每个 shapely 几何体都有的一个“魔法方法”;它返回几何体的 GeoJSON 表示。
- 现在,编辑 BaseGeoCollection 类。添加 export_geojson 方法:
...
def export_geojson(self, file):
"""将集合导出为 GeoJSON 文件。”
features = [i.export_geojson_feature() for i in self.data]
geojson = {"type": "FeatureCollection",
"features": features}
with open(file, 'w') as out_file:
json.dump(geojson, out_file, indent=2)
print("文件已导出: {}".format(file))
这里我们使用了一个列表推导式([i.export_geojson_feature() for i in self.data])来生成特征列表,然后使用 json 模块将字典序列化为 JSON。
- 从 shapely 导入 mapping 函数,并将 json 模块添加到文件开头的导入中:
coding=utf-8
from future import print_function
导入 json
from shapely.geometry import Point, mapping
从 shapely 导入 wkb, wkt
导入 gdal 库
从 utils.geo_functions 导入 open_vector_file
从 utils.geo_functions 导入 transform_geometry
从 utils.geo_functions 导入 convert_length_unit
- 最后,让我们测试它。编辑您的 if name == 'main':块:if name == 'main':
gdal.PushErrorHandler('CPLQuietErrorHandler')
points = PointCollection("../data/geocaching.gpx") points.export_geojson("output/data.json")
- 运行代码并打开 output/data.json 文件以检查结果:
{
"type": "FeatureCollection",
"features": [
{
"geometry": {
"type": "Point",
"coordinates": [
-78.90175,
42.89648
]
},
"type": "Feature",
"properties": {
"status": "Available",
"logs": {
"log": [
{
"@id": "1",
"time": "05/09/2015T11:04:05",
"geocacher": "SYSTEM",
"text": "属性:快速缓存 | 适合儿童 |\n
},
...
所有内容都整齐地导出,包括所有属性和日志。PyCharm 还可以检查 JSON 文件,因此您可以使用结构面板(Alt + 7)来探索文件结构,就像您处理 GPX 文件时那样。
创建地图制作应用程序
现在我们将准备一个能够使用此数据源的环境。我们将把之前的实验改编成构建块,并将它们放入应用程序类中,就像我们处理 Geocaching 应用程序时那样。
首先,让我们整理文件夹和文件。
-
在您的 Chapter5 文件夹中创建一个名为 map_maker 的新包。为此,右键单击文件夹,选择新建 | Python 包。
-
将 my_datasource.py 文件移动到 map_make 文件夹中(拖放)。
-
将 mapnik_experiments 文件夹中的 map_style.xml 和 map_functions.py 文件复制到 map_maker 文件夹中。
-
将 map_style.xml 重命名为 styles.xml。
-
在 Chapter5 根目录下创建一个名为 map_maker_app.py 的文件。完整的树结构应如下所示:
Chapter5
│ geocaching_app.py
| map_maker_app.py
│ models.py
│
├───mapnik_experiments
│
├───map_maker
│ init.py
│ my_datasource.py
| styles.xml
| map_functions.py
│
├───utils
现在我们创建一个代表应用程序的类。
- 在 map_maker_app.py 文件中,创建这个新类及其 init 方法:
coding=utf-8
导入 cv2
导入 mapnik
class MapMakerApp(object):
def init(self, output_image=None):
"""应用程序类."""
self.output_image = output_image
output_image 将是应用程序将写入地图的图像。它不是私有的,因为在应用程序执行期间我们可能想要更改它。
- 从 map_functions.py 文件中复制 display_map 函数,并将其适配为我们的新类的方法:
class MapMakerApp(object):
def init(self, output_image=None):
"""应用程序类."""
self.output_image = output_image
def display_map(self):
"""打开并显示地图图像文件。
:param image_file: 图像的路径。
"""
image = cv2.imread(self.output_image)
cv2.imshow('image', image)
cv2.waitKey(0)
cv2.destroyAllWindows()
此函数现在使用 output_image 属性来显示地图,调用时除了类实例(self)外不接受任何参数。
接下来,让我们来处理 create_map 函数。
- 从 map_functions.py 文件中复制 create_map 函数,并对类进行以下修改:
第三章:coding=utf-8
import cv2
import mapnik
class MapMakerApp(object):
def init(self, output_image="map.png",
style_file="map_maker/styles.xml",
map_size=(800, 600)):
"""应用程序类。
:param output_image: 地图图像输出的路径。
:param style_file: 仅包含地图样式的 Mapnik XML 文件。
:param map_size: 地图像素大小。
"""
self.output_image = output_image
self.style_file = style_file
self.map_size = map_size
def display_map(self):
"""打开并显示地图图像文件。”
image = cv2.imread(self.output_image)
cv2.imshow('image', image)
cv2.waitKey(0)
cv2.destroyAllWindows()
def create_map(self):
"""创建地图并将其写入文件。”
map = mapnik.Map(*self.map_size)
mapnik.load_map(map, self.style_file)
layers = map.layers
map.zoom_all()
mapnik.render_to_file(map, self.output_image)
正如我们在 display_map 中所做的那样,现在 create_map 函数不接受任何参数(除了 self),所有参数都来自实例属性,这些属性被添加到 init 方法中。我们还改进了这些参数的默认值。
从 create_map 中移除了所有层和数据源的定义,因为在接下来的步骤中我们将插入我们之前创建的 PythonDatasource。
使用 PythonDatasource
为了使用此类数据源并实现能够在地图上显示任意数量的数据源的能力,我们将使我们的应用程序类控制层和数据组织的结构,始终遵循应用程序应具有高度抽象的原则:
- 在文件开头包含此导入:
from map_maker.my_datasource import MapDatasource
- 修改类 init 方法并创建一个 add_layer 方法,如下所示:class MapMakerApp(object):
def init(self, output_image="map.png",
style_file="map_maker/styles.xml",
map_size=(800, 600)):
"""应用程序类。
:param output_image: 地图图像输出的路径。
:param style_file: 仅包含地图样式的 Mapnik XML 文件。
:param map_size: 地图像素大小。
"""
self.output_image = output_image
self.style_file = style_file
self.map_size = map_size
self._layers = {}
def display_map(self):...
def create_map(self):...
def add_layer(self, geo_data, name, style='style1'):
"""向地图添加数据,以便在具有给定样式的层中显示。
:param geo_data: 一个 BaseGeoCollection 子类实例。
"""
data source = mapnik.Python(factory='MapDatasource',
data=geo_data)
layer = {"data source": data source,
"data": geo_data,
"style": style}
self._layers[name] = layer
我们在这里所做的是使用一个私有属性 (_layers) 来跟踪我们将使用的层,通过它们的名称。add_layer 方法负责实例化 MapDatasource 类,并将数据传递给它。
我们在这里使用的数据是 BaseGeoCollection 的子类,我们在前面的章节中使用过。有了这个,我们将仅使用高级对象来操作地图,并且还可以免费获得它们的所有功能。
正如我们之前所说,Python Datasource 在 Windows 上不工作,因此我们需要
创建一个解决方案来使事情在操作系统不同的情况下也能工作。我们将要做的是将数据保存到临时文件中,然后使用 Mapnik 的 GeoJSON
plugin to create a data source.
3. 在文件开头添加以下导入:
coding=utf-8
导入 platform
导入 tempfile
从 models 导入 BoundaryCollection, PointCollection
import cv2
import mapnik
4. 现在让我们创建一个文件夹来存放我们的临时文件。在您的 Chapter5 文件夹内创建一个名为 temp 的新文件夹。
5. 修改 add_layer 方法以包含解决方案:
...
def add_layer(self, geo_data, name, style='style1'):
"""向地图添加数据,以给定样式在图层中显示。
:param geo_data: 一个 BaseGeoCollection 子类实例。
"""
if platform.system() == "Windows":
print("Windows system")
temp_file, filename = tempfile.mkstemp(dir="temp") print temp_file, filename
geo_data.export_geojson(filename)
data source = mapnik.GeoJSON(file=filename)
else:
data source = mapnik.Python(factory='MapDatasource',
data=geo_data)
layer = {"data source": data source,
"data": geo_data,
"style": style}
self._layers[name] = layer
在这里,我们使用 platform.system() 来检测操作系统是否为 Windows。如果是,则不是创建 Python DataSource,而是创建一个临时文件,并将 geo_data 导出至其中。然后我们使用 GeoJSON 插件打开该文件,创建 DataSource。
现在解决方案完成,我们需要回到 MapDatasource 的定义,并使其能够接受我们传递给它的数据。
6. 在 my_datasource.py 文件中,在 MapDatasource 类中包含以下 init 方法:
class MapDatasource(mapnik.PythonDatasource):
"""Mapnik 的 PythonDatasource 实现。”
def init(self, data):
super(MapDatasource, self).init(envelope, geometry_type, data_type)
self.data = data
def features(self, query=None):
raise NotImplementedError
我们对 PythonDatasource 的子类现在接受一个必选的数据参数。由于我们正在提高抽象级别,我们将使 MapDatasource 类通过检查它接收到的数据来自动定义所有其他参数;通过这个更改,我们不需要担心几何类型或数据类型。
7. 对 init 方法进行另一个修改:
class MapDatasource(mapnik.PythonDatasource):
"""Mapinik 的 PythonDatasource 的实现。”
def init(self, data):
data_type = mapnik.DataType.vector
if isinstance(data, PointCollection):
geometry_type = mapnik.GeometryType.Point
elif isinstance(data, BoundaryCollection):
geometry_type = mapnik.GeometryType.Polygon
else:
raise TypeError
super(MapDatasource, self).init(
envelope=None, geometry_type=geometry_type,
data_type=data_type)
self.data = data
def features(self, query=None):
raise NotImplementedError
在这里,isinstance() 检查数据类型,并为每种可能的类型定义了相应的 geometry_type,以便传递给父 init
method.
目前,我们只有一个数据类型:矢量。无论如何,我们将明确此定义(data_type = mapnik.DataType.vector),因为在下一章中,将介绍栅格类型。
在我们继续之前,让我们测试一下当前的应用程序。
- 现在编辑文件末尾的 if name == 'main' 块:if name == 'main':
world_borders = BoundaryCollection(
"../data/world_borders_simple.shp")
map_app = MapMakerApp()
map_app.add_layer(world_borders, 'world')
map_app.create_map()
map_app.display_map()
注意
注意 Mapnik 是如何完全抽象的;我们现在只处理由我们的模型和应用程序提供的高级功能。
功能性,以及我们的模型和应用程序。
- 运行代码;您应该看到一个空地图和在控制台中的输出:File imported: ../data/world_borders_simple.shp
Windows 系统
File exported: \geopy\Chapter5\temp\tmpfqv9ch
地图是空的,因为还缺少两个点:features 方法,它是我们地理数据和 Mapnik 数据源之间的粘合剂,以及使 create_map 函数使用我们已定义的图层。
- 让我们从 create_map 方法开始。修改其代码,使其能够遍历我们的图层并将它们添加到地图中:
...
def create_map(self):
"""创建地图并将其写入文件。”
map = mapnik.Map(*self.map_size)
mapnik.load_map(map, self.style_file)
layers = map.layers
for name, layer in self._layers.iteritems():
new_layer = mapnik.Layer(name)
new_layer.datasource = layer["data source"]
new_layer.stylers.append(layer['style'])
layers.append(new_layer)
map.zoom_all()
mapnik.render_to_file(map, self.output_image)
- 现在编辑 styles.xml,以从中移除范围限制:
- 现在再次运行代码并查看输出。如果您使用的是 Windows,您应该看到一个渲染的地图。如果您使用的是 Linux,您应该得到一个异常:Traceback (most recent call last):
文件 … 在
raise NotImplementedError
NotImplementedError
进程以退出代码 1 结束
如果您遇到这个异常(在 Linux 上),那么是因为一切正常,Mapnik 调用了我们的未实现的功能方法。
因此,现在让我们实现这个方法。
- 打开 my_datasource.py 文件并编辑我们的类:
class MapDatasource(mapnik.PythonDatasource):
"""Mapinik 的 PythonDatasource 实现。”
def init(self, data):
data_type = mapnik.DataType.Vector
if isinstance(data, PointCollection):
geometry_type = mapnik.GeometryType.Point
elif isinstance(data, BoundaryCollection):
geometry_type = mapnik.GeometryType.Polygon
else:
raise TypeError
super(MapDatasource, self).init(
envelope=None, geometry_type=geometry_type,
data_type=data_type)
self.data = data
def features(self, query=None):
keys = ['name',]
features = []
for item in self.data.data:
features.append([item.geom.wkb, {'name': item.name}])
return mapnik.PythonDatasource.wkb_features(keys, features) 14. 再次运行代码;现在您应该会在输出中看到渲染的地图:


使用带有过滤器的应用程序
由于 BaseGeoCollection 类具有之前实现的过滤功能,因此可以在将其传递给地图之前过滤数据。
让我们尝试一些示例:
- 在 map_maker_app.py 文件中,编辑 if name == 'main'块:if name == 'main':
world_borders = BoundaryCollection(
"../data/world_borders_simple.shp")
my_country = world_borders.filter('name', 'Brazil')
map_app = MapMakerApp()
map_app.add_layer(my_country, 'countries')
map_app.create_map()
map_app.display_map()
在这里,我们使用 BaseGeoCollection 类的过滤器功能按名称过滤国家;您可以自由尝试按您的国家进行过滤。
-
运行代码,您应该会看到一个只包含一个国家的地图(应该激活缩放),如下面的截图所示:
-
现在尝试组合过滤器以显示多个国家:

if name == 'main':
world_borders = BoundaryCollection(
"../data/world_borders_simple.shp")
countries = world_borders.filter('name', 'China') +\
world_borders.filter('name', 'India') +\
world_borders.filter('name', 'Japan')
map_app = MapMakerApp()
map_app.add_layer(countries, 'countries')
map_app.create_map()
map_app.display_map()
- 再次运行代码并查看结果。
总结
在本章中,我们了解了 Mapnik 是如何工作的,以及如何使用 Python 和 XML 定义和样式化地图。使用 Mapnik 的 Python API,我们可以在 XML 中定义地图,然后在 Python 中修改它,这显示了对于各种需求的高度灵活性。
由于应用程序的结构,Mapnik 隐藏在高级功能之后,这些功能使我们能够使用之前创建的地理数据对象,允许应用程序过滤地图中要显示的数据。
在下一章中,我们将首次遇到栅格数据;我们将看到它是如何工作的,并在我们的地图中显示它。
第六章. 使用遥感
图像
在本章中,我们将开始处理图像——这些图像可能来自卫星、无人机、飞机等携带的各种传感器。这些类型的图像,即从遥感设备收集的图像,是包含像素的图像,这些像素代表了一个给定地理区域的光谱响应。
除了将图像添加到地图上之外,准备图像在地图上展示也很重要。你可能需要合并、裁剪、改变分辨率、改变值以及执行许多其他转换,以便制作出视觉上吸引人的地图或有价值的信息。
为了对图像执行这些转换,我们将通过一个演绎过程,最终得到一个灵活且强大的软件结构。
这里涉及的主题包括:
理解图像是如何表示的
图像与真实世界的关系
合并、裁剪和调整图像的值 从高程数据创建阴影高程图
如何执行一系列处理步骤

理解图像是如何表示的
表示
为了理解图像在计算机表示及其包含的数据方面的含义,我们将从一些示例开始。首先要做的事情是组织你的项目,按照本章的代码如下:
1. 如前所述,在你的 geopy 项目中,复制你的 Chapter5 文件夹并将其重命名为 Chapter6。
2. 在第六章中,导航到实验文件夹,并在其中创建一个名为 image_experiments.py 的新文件。打开它进行编辑。
我们将首先检查一个小样本图像,其结构类似于大型卫星图像。
没有什么花哨的,你会看到四个不同颜色的正方形。但如果我们再进一步,给它加上网格,我们就能看到更多一点的信息。

该图像被分成 16 个大小相等的正方形。这些正方形中的每一个都是一个所谓的像素。像素是图像(即,栅格数据)包含的最小信息部分。在谈论地理处理时,整个图像在真实世界中代表一个空间,而每个像素是那个空间的一部分。
在本章开头将样本图像添加到地图时,我们手动定义了该图像的范围(即其边界框)。这些信息告诉 Mapnik 图像中的坐标如何与真实世界的坐标相关联。
到目前为止,我们已经看到我们的样本图像有 16 个像素,形状为 4 x 4。但是,这幅图像或任何其他栅格数据如何与真实世界空间相关联,取决于数据中可能或可能不存储的信息。
第一个表示关系的信息是图像在世界中的位置。图像和栅格数据通常以左上角为原点。如果我们给原点分配一个坐标,我们就能将图像放置在世界中。
其次,我们需要有关此图像覆盖区域的信息。这种信息可以通过以下三种方式出现:
图像像素的大小
图像的大小
图像边界框的坐标

此信息与以下方程相关:
x_pixel_size = width / columns
y_pixel_size = height / lines
width = xmax – xmin
height = ymax – ymin
使用 OpenCV 打开图像
为了更好地理解,我们将使用 OpenCV 打开示例图像并检查其内容如下:
1. 在你的 image_expriments.py 文件中,输入以下代码:def open_raster_file(image):
"""打开栅格文件。
:param image: 栅格文件的路径或 np 数组。
"""
image = cv2.imread(image)
return image
if name == 'main':
image = open_raster_file('../../data/sample_image.tiff')
print(image)
print(type(image))
print(image.shape)
2. 运行代码。由于这是你第一次运行此文件,请按 Alt + Shift + F10
并从列表中选择 image_experiments。你应该看到以下输出:
[[[ 0 0 255]
[ 0 0 255]
[ 0 255 0]
[ 0 255 0]]
[[ 0 0 255]
[ 0 0 255]
[ 0 255 0]
[ 0 255 0]]
[[255 0 0]
[255 0 0]
[100 100 100]
[100 100 100]]
[[255 0 0]
[255 0 0]
[100 100 100]
[100 100 100]]]
<type 'numpy.ndarray'>
(4, 4, 3)
Process finished with exit code 0
表达式 print(type(image)) 打印存储在 image 变量中的对象的类型。如你所见,它是一个形状为 4 x 4 x 3 的 NumPy 数组。OpenCV
打开图像并将其数据放入数组中,尽管现在还很难可视化数据的组织方式。该数组包含每个
图像上的像素。
为了更好的可视化,我将为你重新组织打印输出:
[[[ 0 0 255] [ 0 0 255] [ 0 255 0] [ 0 255 0]]
[[ 0 0 255] [ 0 0 255] [ 0 255 0] [ 0 255 0]]
[[255 0 0] [255 0 0] [100 100 100] [100 100 100]]
[[255 0 0] [255 0 0] [100 100 100] [100 100 100]]]
现在数组的形状更有意义了。注意,我们有四条行,每行正好有四个列,正如我们在图像中看到的那样。每个项目都有一组三个数字,代表蓝色、绿色和红色通道的值。
提示
记住,当你使用 OpenCV 导入彩色图像时,通道的顺序将是 BGR(蓝色、绿色和红色)。
例如,以左上角的第一像素为例。它全部是红色,正如我们在图像中看到的那样:蓝 绿 红
[ 0 0 255]
因此,将图像作为 NumPy 数组导入的第一个且最重要的含义是,它们的行为类似于数组,并具有任何 NumPy 数组都有的所有函数和方法,这为在处理栅格数据时使用 NumPy 的全部功能打开了可能性。

了解数值类型
在前一个主题中,每个像素都有三个通道:蓝色、绿色和红色。每个通道的值范围从 0 到 255(256 个可能的值)。这些通道的组合产生一个可见的颜色。这个值范围不是随机的;256 是使用单个字节可以实现的组合数量。
字节是计算机可以存储和从内存中检索的最小数据部分。它由 8 位零或一组成。
这对我们来说很重要,因为计算机使用其内存来存储图像,并且将为每个像素的每个通道保留一定空间来存储值。我们必须确保保留的空间足以存储我们想要存储的数据。
让我们进行一个抽象。假设你有 1 升(1000 毫升)的水,你想存储它。如果你选择一个 250 毫升的杯子来存储这水,多余的部分会溢出来。如果你选择一个容量为 10000 升的水罐车,你可以存储水,但会浪费很多空间。所以,你可能选择一个 3 升的桶,这样就可以足够存储水了。它不像卡车那么大,如果你想存储一点更多的水,你还会有些多余的空间。
在计算机中,事情的工作方式类似。在将东西放入容器之前,你需要选择容器的大小。在之前的例子中,OpenCV 为我们做出了这个选择。你将在未来的许多实例中看到,我们使用的程序将帮助我们做出这些选择。但是,对如何工作的清晰理解非常重要,因为如果水溢出来(即溢出),你的程序将出现意外的行为。或者,如果你选择一个太大的容器,你可能会耗尽计算机内存。
值存储的需求可能在以下方面有所不同:
只能是正数或正负数
整数或分数
小数或大数
复数
可用的选项及其大小可能因计算机架构和软件而异。对于常见的 64 位桌面,NumPy 将为你提供这些可能的数值类型:
bool:布尔值(True 或 False),以字节形式存储
int8:字节(-128 到 127)
int16:整数(-32768 到 32767)
int32:整数(-2147483648 到 2147483647)int64:整数(-9223372036854775808 到 9223372036854775807)uint8:无符号整数(0 到 255)
uint16:无符号整数(0 到 65535)
uint32:无符号整数(0 到 4294967295)
uint64:无符号整数(0 到 18446744073709551615)
float16: 半精度浮点数:符号位,5 位指数,10 位尾数
complex128: 由两个 64 位浮点数(实部和虚部)表示的复数
因此,我们可能预期我们的样本图像的类型是 uint8。让我们检查它是否正确:
- 编辑 if name == 'main':块:
if name == 'main':
image = open_raster_file('../../data/sample_image.tiff')
print(type(image))
print(image.dtype)
- 再次运行代码。你应该看到符合我们预期的输出:
<type 'numpy.ndarray'>
uint8
处理完成,退出代码为 0
处理遥感图像和
数据
卫星图像以不同的格式出现,服务于不同的目的。这些图像可以用来使用真实颜色可视化地球上的特征,或者用来通过人眼不可见的频谱部分来识别各种特征。
正如我们所见,我们的样本图像有三个通道(蓝色、绿色和红色),它们组合在一个文件中,以组成一个真实色彩图像。与样本图像不同,大多数卫星数据将每个通道分别存储在一个文件中。
这些通道被称为 波段,并包含可见或不可见于人眼的电磁频谱范围。
在以下示例中,我们将使用由 Advanced Spaceborne Thermal Emission and Reflection Radiometer (ASTER) 获取的数据生成的 数字高程模型 (DEM)。
这些 DEM 的分辨率大约为 90 米,值存储在 16 位
表示以米为单位的高的有符号整数。
我们将要使用的数据集包含在数据文件夹中,并来自一个名为 Poços de Caldas 的巴西城市。这座城市位于一个巨大的已灭绝火山口内,这是我们希望在数据处理过程中看到的一个特征。出于教学目的和为了覆盖一个大区域,我们将使用四张图片:
注意
你可以在 earthexplorer.usgs.gov/ 获取更多数字高程模型。
- 如果想下载并使用自己的 DEM,需要提取下载的 ZIP 文件
文件。注意,每个 ZIP 存档包含两张图片。以 _dem 结尾的是实际的高程数据。以 _num 结尾的包含质量评估信息。查看包含的 README.pdf 文件获取更多信息。
- 将所有图片移动或复制到第六章代码的数据文件夹中。
每张图片代表一个 1 度的瓦片。图片覆盖的瓦片信息编码在文件名中,如下面的图片所示:

图像镶嵌
Mapnik 具有从磁盘读取瓦片数据的栅格数据源的能力。但我们不会使用它,因为将图像拼接在一起的过程非常重要,值得学习。
下面的代码将打开图像,将它们组合,并将单个组合图像保存到磁盘。这个过程(具有不同复杂程度)称为拼贴:1. 仍然在 image_experiments.py 文件中,在 open_raster_file 函数之后添加一个新函数:
def combine_images(input_images):
"""将图像组合成拼贴。
:param input_images: 输入图像的路径。
"""
images = []
for item in input_images:
images.append(open_raster_file(item))
print images
- 现在,编辑 if name == 'main':块,以便我们可以测试代码:if name == 'main':
elevation_data = [
'../../data/ASTGTM2_S22W048_dem.tif',
'../../data/ASTGTM2_S22W047_dem.tif',
'../../data/ASTGTM2_S23W048_dem.tif',
'../../data/ASTGTM2_S23W047_dem.tif']
combine_images(elevation_data)
- 运行代码并查看输出:
[array([[[1, 1, 1],
[1, 1, 1],
[2, 2, 2],
...,
[4, 4, 4],
[4, 4, 4],
[4, 4, 4]],
. . .
Process finished with exit code 0
你应该看到四个数组的列表。PyCharm 将隐藏一些值,以便它可以在控制台中适应。
我们首先应该注意到,输入图像参数中图像的顺序与输出列表中数组的顺序相同。这将在以后非常重要。
其次,尽管高程数据是 16 位有符号整数(int16),但表示图像的数组仍然有三个 8 位无符号整数的波段。这是一个错误。

OpenCV 正在将灰度图像转换为彩色图像。我们将按照以下方式修复它:
- 将 open_raster_file 函数修改为接受一个新参数。这将允许我们打开图像而不更改它们:
def open_raster_file(image, unchanged=True):
"""打开栅格文件。
:param image: 栅格文件的路径或 np 数组。
:param unchanged: 设置为 true 以保留原始格式。
"""
flags = cv2.CV_LOAD_IMAGE_UNCHANGED if unchanged else -1
image = cv2.imread(image, flags=flags)
return image
cv2.imread 中的 flags 参数允许我们调整图像的打开和转换为数组的方式。如果 flags 设置为 cv2.CV_LOAD_IMAGE_UNCHANGED,则图像将以原始形式打开,不进行任何转换。
- 由于我们设置了默认的不变为 true,我们只需再次运行代码并查看结果:
[array([[ 508, 511, 514, ..., 1144, 1148, 1152],
[ 507, 510, 510, ..., 1141, 1144, 1150],
[ 510, 508, 506, ..., 1141, 1145, 1154],
...,
[ 805, 805, 803, ..., 599, 596, 593],
[ 802, 797, 803, ..., 598, 594, 590],
[ 797, 797, 800, ..., 603, 596, 593]], dtype=uint16)
. . .
Process finished with exit code 0
现在的值是正确的,并且它们是每个像素的测量高程(米)。
到目前为止,我们有一个按输入文件顺序排列的数组列表。为了确定下一步,我们可以想象这个列表就像图像被作为条带马赛克一样:现在,我们必须重新组织这个列表,以便图像放置在正确的位置。
记住,NumPy 数组有一个形状属性。在二维数组中,它是一个包含
the shape in columns and rows. NumPy 数组还有一个 reshape() 方法,它执行形状转换。
注意
查看 NumPy 文档中关于 reshape 方法和函数的说明。改变数组的形状是一个非常强大的工具,
docs.scipy.org/doc/numpy/reference/generated/numpy.reshape.html.
reshape 通过按顺序填充行中的输入值来工作。当行填满时,该方法跳到下一行并继续,直到结束。因此,如果我们将马赛克的预期形状传递给 combine_images 函数,我们就可以使用这些信息根据正确的位置组合图像。
但我们需要其他东西。我们需要通过像素数量知道输出图像的形状,这将是通过每个图像的形状与马赛克形状的乘积得到的。让我们在代码中尝试一些更改,如下所示:1. 编辑 combine_images 函数:
def combine_images(input_images, shape, output_image):
"""将图像组合成马赛克。
:param input_images: 输入图像的路径。
:param shape: 图像马赛克在列和行中的形状。
:param output_image: 输出图像马赛克的路径。
"""
if len(input_images) != shape[0] * shape[1]:
raise ValueError(
"图像数量与马赛克形状不匹配。") images = []
for item in input_images:
images.append(open_raster_file(item))
rows = []
for row in range(shape[0]):
start = (row * shape[1])
end = start + shape[1]
rows.append(np.concatenate(images[start:end], axis=1))
mosaic = np.concatenate(rows, axis=0)
print(mosaic)
print(mosaic.shape)
现在函数接受两个额外的参数,马赛克的形状(行和列中的图像数量,而不是像素数量)以及输出图像的路径,供以后使用。
使用此代码,图像列表被分离成行。然后,这些行被组合成完整的马赛克。
- 在运行代码之前,别忘了在文件开头导入 NumPy:
coding=utf-8
import cv2
import numpy as np
并编辑 if name == 'main': 块:if name == 'main':
elevation_data = [
'../../data/ASTGTM2_S22W048_dem.tif',
'../../data/ASTGTM2_S22W047_dem.tif',
'../../data/ASTGTM2_S23W048_dem.tif',
'../../data/ASTGTM2_S23W047_dem.tif']
combine_images(elevation_data, shape=(2, 2))
- 现在运行代码并查看结果:
[[508 511 514…, 761 761 761]
[507 510 510…, 761 761 761]
[510 508 506…, 761 761 761]
...,
[514 520 517…, 751 745 739]
[517 524 517…, 758 756 753]
[509 509 510…, 757 759 760]]
(7202, 7202)
进程以退出代码 0 完成
现在它是一个包含 7202 x 7202 像素的单个数组。剩余的任务是将此数组保存到磁盘上的图像。
4. 只需在函数中添加两行并编辑 if name == 'main':块:def combine_images(input_images, shape, output_image):
"""将图像组合成拼贴图。
:param input_images: 输入图像的路径。
:param shape: 拼贴图的列数和行数形状。
:param output_image: 输出图像拼贴图的路径。
"""
if len(input_images) != shape[0] * shape[1]:
raise ValueError(
"图像数量与拼贴图形状不匹配。") images = []
for item in input_images:
images.append(open_raster_file(item))
rows = []
for row in range(shape[0]):
start = (row * shape[1])
end = start + shape[1]
rows.append(np.concatenate(images[start:end], axis=1))
mosaic = np.concatenate(rows, axis=0)
print(mosaic)
print(mosaic.shape)
cv2.imwrite(output_image, mosaic)
if name == 'main':
elevation_data = [
'../../data/ASTGTM2_S22W048_dem.tif',
'../../data/ASTGTM2_S22W047_dem.tif',
'../../data/ASTGTM2_S23W048_dem.tif',
'../../data/ASTGTM2_S23W047_dem.tif']
combine_images(elevation_data, shape=(2, 2),
output_image="../output/mosaic.png")
调整图像的值
如果你运行前面的代码,你会看到一个黑色图像作为输出。这是因为表示该区域实际数据的值范围与 16 位整数图像的可能范围相比非常窄,我们无法区分灰度色调。为了更好地理解,让我们进行以下简单测试:1. 仍然在 image_experiments.py 文件中,注释掉 if name == 'main':块并添加以下新块:
if name == 'main':
image = open_raster_file("../output/mosaic.png")
print(image.min(), image.max())
2. 运行代码并查看控制台输出。
(423, 2026)
Process finished with exit code 0
精确地说,图像的范围从-32768 到 32767,该区域的高度从 423 到 2026。因此,我们需要将高度范围缩放到数据类型的范围。
由于我们正在制作旨在供人类可视化的数据表示,我们不需要使用大范围的灰度值。研究各不相同,但有些人说我们只能检测到 30 种色调,因此 256 个可能值的 8 位无符号整数应该足够用于数据可视化。
3. 添加此新函数:
def adjust_values(input_image, output_image, img_range=None):
"""通过将一系列值投影到灰度图像中来在 input_image 中创建数据的可视化。
:param input_image: 包含数据的数组
或图像的路径。
:param output_image: 写入输出的图像路径。
:param img_range: 指定的值范围或 None 以使用图像的范围(最小值和最大值)。
"""
image = open_raster_file(input_image)
if img_range:
min = img_range[0]
max = img_range[1]
else:
min = image.min()
max = image.max()
interval = max - min
factor = 256.0 / interval
output = image * factor
cv2.imwrite(output_image, output)
此函数接受数组或图像文件的路径。利用此功能,

我们可以稍后使用此函数作为其他处理步骤的子步骤。您想要使用的值范围也是可选的。它可以手动设置,也可以从图像的最小值和最大值中提取。
- 要测试代码,编辑 if name == 'main': 块:if name == 'main':
调整。
adjust_values('../output/mosaic.png',
'../output/mosaic_grey.png')
注意,输出图像现在是一个 png 文件。由于我们正在为可视化准备图像,我们可以承受在数据压缩中丢失信息,以换取更小的文件。
- 运行代码并打开 mosaic_grey.png 文件以查看结果。现在你应该看到以下美丽的灰度图像:
裁剪图像
我们制作了一个大型的图像拼贴,以覆盖感兴趣的区域,在这个过程中,我们得到了一个比所需的图像大得多的图像。现在,是时候裁剪图像了,这样我们就能得到一个更小的图像,只包含我们想要看到的部分,从而节省磁盘空间和处理时间。
在我们的例子中,我们感兴趣的是火山口。它是图像右侧的圆形物体。为了只获取该感兴趣区域,我们将编写一个函数,该函数可以使用一组坐标的边界框来裁剪图像,如下所示:1. 将新函数添加到 image_experiments.py 文件中:
def crop_image(input_image, image_extent, bbox, output_image):
"""通过边界框裁剪图像。
bbox 和 image_extent 格式:(xmin, ymin, xmax, ymax)。
:param input_image: 包含数据的数组
或图像的路径。
:param image_extent: 图像的地理范围。
:param output_image: 输出图像的路径。
:param bbox: 感兴趣区域的边界框。
"""
input_image = open_raster_file(input_image)
img_shape = input_image.shape
img_geo_width = abs(image_extent[2] - image_extent[0])
img_geo_height = abs(image_extent[3] - image_extent[1])
一个地理单位包含多少像素。
pixel_width = img_shape[1] / img_geo_width
pixel_height = img_shape[0] / img_geo_height
要切割的像素索引。
x_min = abs(bbox[0] - image_extent[0]) * pixel_width
x_max = abs(bbox[2] - image_extent[0]) * pixel_width
y_min = abs(bbox[1] - image_extent[1]) * pixel_height
y_max = abs(bbox[3] - image_extent[1]) * pixel_height
output = input_image[y_min:y_max, x_min:x_max]
cv2.imwrite(output_image, output)
由于我们处理的是 NumPy 数组,裁剪本身就是一个简单的数组切片。
数组的切片与 Python 列表的切片非常相似,但具有额外的维度。语句 input_image[y_min:y_max, x_min:x_max] 表示我们只想获取数组中指定单元格(即像素)的部分。
因此,所有涉及的数学运算都是将地理单位转换为数组索引。
2. 修改 if name == 'main': 块以测试代码:if name == 'main':
Crop.
roi = (-46.8, -21.7, -46.3, -22.1) # 利益区域。
crop_image('../output/mosaic_grey.png',
(-48, -21, -46, -23), roi, "../output/cropped.png")

3. 运行代码并打开输出图像以查看结果。
4. 如果您错过了任何步骤,可以一次性运行整个流程。只需编辑 if name == 'main' 块:
if name == 'main':
Combine.
elevation_data = [
'../../data/ASTGTM2_S22W048_dem.tif',
'../../data/ASTGTM2_S22W047_dem.tif',
'../../data/ASTGTM2_S23W048_dem.tif',
'../../data/ASTGTM2_S23W047_dem.tif']
combine_images(elevation_data, shape=(2, 2),
output_image="../output/mosaic.png")
Adjust.
adjust_values('../output/mosaic.png',
'../output/mosaic_grey.png')
Crop.
roi = (-46.8, -21.7, -46.3, -22.1) # 利益区域。
crop_image('../output/mosaic_grey.png',
(-48, -21, -46, -23), roi, "../output/cropped.png")

创建阴影立体图像
在我们处理之后,我们的数字高程模型图像有了很大的改进,但它仍然不适合地图。未经训练的眼睛可能很难仅通过观察不同灰度的阴影来理解地形。
幸运的是,有一种技术,称为阴影渲染或立体渲染,可以将高程数据转换为地形上的模拟阳光阴影。看看下面图片中的美丽地图,并注意当它以阴影立体图的形式呈现时,理解地形要容易得多:
该过程很简单,涉及将我们的图像通过以下知名算法:
1. 将 create_hillshade 函数添加到您的 image_experiments.py 文件中:def create_hillshade(input_image, output_image,
azimuth=90, angle_altitude=60):
"""从数字高程模型创建阴影立体图像。
:param input_image: 包含数据的数组
或图像的路径。
:param azimuth: 模拟太阳方位角。
:param angle_altitude: 太阳高度角。
"""
input_image = open_raster_file(input_image)
x, y = np.gradient(input_image)
slope = np.pi / 2 - np.arctan(np.sqrt(x * x + y * y))
aspect = np.arctan2(-x, y)
az_rad = azimuth * np.pi / 180
alt_rad = angle_altitude * np.pi / 180

a = np.sin(alt_rad) * np.sin(slope)
b = np.cos(alt_rad) * np.cos(slope) * np.cos(az_rad - aspect) output = 255 * (a + b + 1) / 2
cv2.imwrite(output_image, output)
2. 现在,修改 if name == 'main': 块以测试代码:if name == 'main':
create_hillshade("../output/cropped.png",
"../output/hillshade.png")
3. 运行代码并打开输出图像以查看结果。如果一切顺利,您应该看到您数据的阴影立体图表示。

构建图像处理管道
图像处理,无论是用于地理应用还是其他应用,通常需要执行一系列变换(即步骤)以获得所需的最终结果。在这些序列中,一个步骤的输出是下一个步骤的输入。在计算机科学中,这被称为处理管道。
这种类型的数据操作非常灵活,因为您有一系列可以排列成多种组合以产生广泛结果的函数或步骤。
到目前为止,在本章的示例中,我们所做的是从磁盘打开一个图像,执行一个给定的操作,并将结果保存到磁盘上的另一个图像。然后,在下一步中,我们打开前一步的结果,依此类推。
尽管步骤尚未连接,我们可以想象以下图像处理管道:
将中间步骤保存到磁盘在需要使用这些图像或在其他情况下,当管道使用并行处理或其他更复杂的方法时是有用的。
对于某些情况,仅仅在管道中传递数据而不接触硬盘,只使用计算机内存可能更有趣。这样,我们应期望速度有显著提升,并且残余文件的产生会减少。
为了调和这两种情况,我们可以使用函数的类型检查来处理参数,使它们可以接受数组或文件路径。
参数,使它们可以接受数组或文件路径。导航到您的 open_raster_file 函数并修改其代码:
def open_raster_file(file_path, unchanged=True):
"""打开栅格文件。
:param file_path: 栅格文件的路径或 np 数组。
:param unchanged: 设置为 true 以保持原始格式。
"""
if isinstance(file_path, np.ndarray):
return file_path
flags = cv2.CV_LOAD_IMAGE_UNCHANGED if unchanged else -1
image = cv2.imread(file_path, flags=flags)
return image
此函数现在将检查 file_path 的类型。如果它已经是一个 NumPy 数组,它将被返回。这改变了我们所有函数的行为,因为它们现在可以接收数组作为输入。
如果我们在所有函数中添加返回语句并使它们返回输出数组,
我们将能够如下组合函数:create_hillshade(
crop_image(
adjust_values('mosaic.png'),
(-48, -21, -46, -23), roi), 'shaded.png')
您不需要输入此代码。这种表示法难以理解。函数调用的顺序不直观,难以确定每个参数属于哪个函数。
如果我们能够像在第四章的“链式过滤器”部分中做的那样通过链式调用函数来执行管道,那会更好。第四章,改进应用搜索能力。
实际上,如果能使用如下表示法那就太好了:adjust_values().crop_image().create_hillshade()
如同 第四章,改进应用搜索功能,我们需要的只是一个类和返回相同类型类的那些方法。有了这两样东西,步骤如何组合就没有限制了。所以,让我们开始吧。
创建 RasterData 类
我们的 RasterData 类将遵循之前与我们的向量所使用的模式。实例化时,该类将接收一个文件路径或一个数组。正如之前所说,为了使用方法链来执行处理管道,每个处理方法必须返回 RasterData 类的另一个实例。
我们将从类声明开始,然后填充它的方法。为了使剪切和粘贴我们已完成的工作更容易,我们将在 image_experiments.py 文件中进行以下步骤:
- 在 image_experiments.py 文件顶部,导入之后,创建一个类:
coding=utf-8
import cv2
import numpy as np
class RasterData(object):
def init(self, input_data, unchanged=True, shape=None):
"""表示以数组形式存在的栅格数据。
:param input_data: 栅格文件或 Numpy 数组。
:param unchanged: 如果为 True,则保持原始格式。
:param shape: 当使用多个输入数据时,此参数
确定组合的形状。
"""
self.data = None
if isinstance(input_data, list) \
or isinstance(input_data, tuple):
self.combine_images(input_data, shape)
else:
self.import_data(input_data, unchanged)
该数组将被存储在 data 属性中,因此我们最初将其设置为 None。
为了使这个类与其他类保持一致并避免名称上的冗余,我们需要进行一些其他更改。第一个更改是使用与之前相同的 import_data 方法。
- 将 open_raster_file 函数剪切并粘贴到类中,将其重命名为 import_data,并修改其行为以像方法一样:
class RasterData(object):
def init(self, input_data, unchanged=True, shape=None):
...
def import_data(self, image, unchanged=True):
"""打开栅格文件。
:param image: 栅格文件路径或 np 数组。
:param unchanged: 如果为 True,则保持原始格式。
"""
if isinstance(image, np.ndarray):
self.data = image
return image
flags = cv2.CV_LOAD_IMAGE_UNCHANGED if unchanged else -1
self.data = cv2.imread(image, flags=flags)
而不是返回一个数组,现在它将数组放入 data 属性中。
接下来,由于我们将从步骤中移除将图像写入磁盘的义务,我们需要一个方法来执行此操作。
- 添加 write_image 方法:
class RasterData(object):
def init(self, input_data, unchanged=True, shape=None):
...
def import_data(self, input_image, unchanged=True):
...
def write_image(self, output_image):
"""将数据写入磁盘作为图像。
:param output_image: 输出图像的路径和名称。
"""
cv2.imwrite(output_image, self.data)
return self
- 按照示例的顺序,将 combine_images 函数剪切并粘贴到类中作为方法:
class RasterData(object):
def init(self, input_data, unchanged=True, shape=None):
...
def import_data(self, input_image, unchanged=True):
...
def write_image(self, output_image):
...
def combine_images(self, input_images, shape):
"""组合图像形成镶嵌。
:param input_images: 输入图像的路径。
:param shape: 镶嵌的列数和行数形状。
"""
if len(input_images) != shape[0] * shape[1]:
raise ValueError("图像数量与形状不匹配"
"镶嵌形状。")
images = []
for item in input_images:
if isinstance(item, RasterData):
images.append(item.data)
else:
images.append(RasterData(item).data)
rows = []
for row in range(shape[0]):
start = (row * shape[1])
end = start + shape[1]
rows.append(np.concatenate(images[start:end], axis=1))
mosaic = np.concatenate(rows, axis=0)
self.data = mosaic
return self
现在,你可以创建一个空的 RasterData 实例,然后使用此方法用镶嵌填充它。或者,你可以使用包含任何组合的图像路径、数组或甚至其他 RasterData 实例作为参数创建实例。它将自动将它们组合起来,将结果放入数据属性中,并返回自身。
现在你已经掌握了这个技巧,让我们用最后三个函数进行相同的转换。
- 将 adjust_values、crop_image 和 create_hillshade 函数作为方法添加到类中。你的完整类应该如下所示:class RasterData(object):
def init(self, input_data, unchanged=True, shape=None):
"""表示以数组形式存在的栅格数据。
:param input_data: 栅格文件或 Numpy 数组。
:param unchanged: True 以保持原始格式。
:param shape: 当使用多个输入数据时,此参数
确定组合的形状。
"""
self.data = None
if isinstance(input_data, list) \
or isinstance(input_data, tuple):
self.combine_images(input_data, shape)
else:
self.import_data(input_data, unchanged)
def import_data(self, image, unchanged=True):
"""打开栅格文件。
:param image: 栅格文件的路径或 np 数组。
:param unchanged: True 以保持原始格式。
"""
if isinstance(image, np.ndarray):
self.data = image
return image
flags = cv2.CV_LOAD_IMAGE_UNCHANGED if unchanged else -1
self.data = cv2.imread(image, flags=flags)
def write_image(self, output_image):
"""将数据写入磁盘作为图像。
:param output_image: 输出图像的路径和名称。
"""
cv2.imwrite(output_image, self.data)
return self
def combine_images(self, input_images, shape):
"""组合图像形成镶嵌。
:param input_images: 输入图像的路径。
:param shape: 镶嵌的列数和行数形状。
"""
if len(input_images) != shape[0] * shape[1]:
raise ValueError("图像数量与形状不匹配"
"镶嵌形状。")
images = []
for item in input_images:
if isinstance(item, RasterData):
images.append(item.data)
else:
images.append(RasterData(item).data)
rows = []
for row in range(shape[0]):
start = (row * shape[1])
end = start + shape[1]
rows.append(np.concatenate(images[start:end], axis=1))
mosaic = np.concatenate(rows, axis=0)
self.data = mosaic
return self
def adjust_values(self, img_range=None):
"""通过将一系列值投影到灰度图像中来在输入图像中创建数据的可视化。
:param img_range: 指定的值范围
或 None 以使用图像的范围
(最小值和最大值)。
"""
image = self.data
if img_range:
min = img_range[0]
max = img_range[1]
else:
min = image.min()
max = image.max()
interval = max - min
factor = 256.0 / interval
output = image * factor
self.data = output
return self
def crop_image(self, image_extent, bbox):
"""通过边界框裁剪图像。
bbox 和 image_extent 格式: (xmin, ymin, xmax, ymax)。
:param input_image: 包含数据的数组
或图像的路径。
:param image_extent: 图像的地理范围。
:param output_image: 写入输出的图像路径。
:param bbox: 兴趣区域的边界框。
"""
input_image = self.data
img_shape = input_image.shape
img_geo_width = abs(image_extent[2] - image_extent[0])
img_geo_height = abs(image_extent[3] - image_extent[1])
一个地理单位包含多少像素。
pixel_width = img_shape[1] / img_geo_width
pixel_height = img_shape[0] / img_geo_height
要切割的像素索引。
x_min = abs(bbox[0] - image_extent[0]) * pixel_width
x_max = abs(bbox[2] - image_extent[0]) * pixel_width
y_min = abs(bbox[1] - image_extent[1]) * pixel_height
y_max = abs(bbox[3] - image_extent[1]) * pixel_height
output = input_image[y_min:y_max, x_min:x_max]
self.data = output
return self
def create_hillshade(self, azimuth=90, angle_altitude=60):
"""从数字高程模型创建阴影图。
:param input_image: 包含数据的数组
或图像的路径。
:param azimuth: 模拟的太阳方位角。
:param angle_altitude: 太阳高度角。
"""
input_image = self.data
x, y = np.gradient(input_image)
slope = np.pi / 2 - np.arctan(np.sqrt(x * x + y * y))
aspect = np.arctan2(-x, y)
az_rad = azimuth * np.pi / 180
alt_rad = angle_altitude * np.pi / 180
a = np.sin(alt_rad) * np.sin(slope)
b = np.cos(alt_rad) * np.cos(slope)\
- np.cos(az_rad - aspect)
output = 255 * (a + b + 1) / 2
self.data = output
return self
类已完整,我们可以创建一个流程来测试它。
6. 编辑并组织 if name == 'main':块以测试图像处理流程:
if name == 'main':
高程数据 = [
'../../data/ASTGTM2_S22W048_dem.tif',
'../../data/ASTGTM2_S22W047_dem.tif',
'../../data/ASTGTM2_S23W048_dem.tif',
'../../data/ASTGTM2_S23W047_dem.tif']
roi = (-46.8, -21.7, -46.3, -22.1) # 兴趣区域。
iex = (-48, -21, -46, -23) # 图像范围。
RasterData(elevation_data, shape=(2, 2)).adjust_values().\
crop_image(iex, roi).create_hillshade().\
write_image('../output/pipeline_output.png')

由于本书的宽度限制,管道被拆分为三行,但如果你愿意,可以在 PyCharm 中将其输入为单行。
- 运行代码并欣赏结果。
到目前为止,你已经取得了了不起的成就。我说的不是阴影高程图,我说的是能够持续开发处理步骤并将它们组合成一个处理管道以实现最终结果的能力。我们在这里开发的结构可以用于几乎任何地理处理。
此外,请注意,由管道生成的图像的质量远优于之前的图像。这是因为数据一直存储在内存中。
这避免了在将数据多次保存到文件时由于压缩导致的数据丢失。
关于我们实现的结构功能的一些评论如下:所有的处理方法最终都做两件事:改变实例数据并返回实例本身。这意味着类实例将在管道中发生变异,随着过程的进行,旧数据将被新数据替换。因此,Python 的垃圾回收器将消除内存中的旧结果以节省空间。
如果在任何步骤中你想保存当前的处理状态,只需插入对 write_image 方法的调用(它也会返回 self,并且可以被管道化)。这是一个强大的功能。
调试工具,同时也可以在稍后需要重复长管道中的步骤时节省时间。
你可以分叉管道。你可以在不同的路径上创建一个分支,从而可以产生多个结果。为此,你可以使用 copy()函数,或者你可以在分叉之前将结果写入磁盘。在本书的后面部分,我们将看到,在进行并行处理时,有时我们也会需要这些技术。
总结
在本章中,我们了解了遥感图像如何在计算机内部表示为数组,以及我们如何利用这一特性来处理它们。我们看到了,为了在地图上使用图像,通常需要将它们转换以获得更好的结果。然后,我们编写了处理函数来处理数字高程模型图像,最终得到一幅美丽的阴影高程图。最后,我们创建了一个 RasterData 类,并将我们的函数转换成了这个类的成员方法。通过一些额外的修改,我们使这些方法能够链入处理管道。
在下一章中,我们将探索图像中的数据并获取有价值的信息。
第七章:提取信息
栅格数据
栅格数据不仅仅是视觉信息的资源,它们是给定空间属性的样本,其值可以进行分析以产生有价值的信息。
在本章中,我们将从栅格数据中提取信息,特别强调统计信息。遵循之前的示例,我们将使用数字高程模型来获取给定区域的最高和最低海拔值,将海拔范围分为类别,并生成直方图和其他统计信息。超越简单的数值,我们将在漂亮的彩色地图上显示所有信息。
本章涵盖的主题包括:
如何从栅格数据中获取统计信息
使用编程技术,如延迟评估和记忆化,以避免不必要的计算
如何格式化表格数据输出
如何着色地图并选择合适的颜色
如何混合颜色图以生成彩色和阴影图
获取基本统计信息
正如我们之前看到的,图像或栅格数据是包含表示给定现实世界空间的数值的数组。因此,它们是统计样本,并且可以用于统计分析。
当我们导入数据时,它被转换为 NumPy 数组。这些数组包含基本统计计算的方法。在本主题中,我们将从这些计算中获得结果并将它们保存到文件中。
在上一章的末尾,我们通过组合可以保存到磁盘上的步骤创建了一个图像处理流程。在这里,我们将遵循相同的模式。统计计算将被添加为另一个步骤。保持相同的组织结构允许用户在任何处理流程点上生成统计信息。如果需要,可以保存所有子步骤的统计信息。
让我们先组织我们的代码:
- 正如我们在每一章的开始所做的那样,我们将从上一章复制代码。在你的 geopy 项目文件夹中,复制 Chapter 6 文件夹(Ctrl + C)并将其粘贴(Ctrl + V)。将复制的文件夹命名为 Chapter7\。
在最后一章中,我们在 image_experiments.py 文件中完成了 RasterData 类。由于我们的实验已经结束,让我们将这个类移动到一个永久且有意义的位置。
-
复制文件 Chapter7/experiments/image_experiments.py(Ctrl + C)。
-
选择 Chapter7 文件夹并将文件粘贴到那里(Ctrl + V)。
-
将文件重命名为 raster_data.py。为此,右键单击文件并选择 重构 | 重命名… 或者选择文件并按 Ctrl + F6。将出现重构对话框。在对话框中更改名称,然后单击 重构 按钮。对话框中有两个复选框询问您是否想要搜索此文件的引用。如果它们被勾选(开启),PyCharm 将搜索并自动更改这些引用,因此代码将继续工作。
-
删除 image_experiments.py 文件,因为它将不再有用。
现在我们已经组织好了代码,我们将分析一些方面并回顾一些点,以便规划我们的下一步。
让我们以一个具有基础工作模式的 RasterData 类实例为例:
在实例化时刻,你可以传递数据,或者你可以稍后导入数据。
之后,数据将存储为 NumPy 数组在 data 属性中。
当你在类中运行任何方法时,操作将被执行,如果需要,数据将被转换,并且实例本身将连同新数据一起返回。
类中除了数据外没有存储任何信息。因此,一些方法需要手动定义参数。
数据属性是一个 NumPy 数组,所以它具有所有 NumPy 数组方法。
准备数据
我们将要使用的是由包含高程数据的四幅图像组成的样本数据。处理流程将这些图像合并,调整值以在地图上显示,裁剪图像,然后生成阴影地形图。
这个流程适合可视化,但在调整值的时候会丢失数据。
对于这项工作,我们不希望发生这种情况。我们希望保留原始的米值。所以,我们首先需要做的是构建一个适合我们需求的流程,并在最后保存结果,这样我们就不需要在接下来的测试中重复所有步骤:1. 打开 raster_data.py 文件进行编辑,并在其末尾添加 if name
== 'main': 包含以下代码的块:
if name == 'main':
高程数据 = [
'../data/ASTGTM2_S22W048_dem.tif',
'../data/ASTGTM2_S22W047_dem.tif',
'../data/ASTGTM2_S23W048_dem.tif',
'../data/ASTGTM2_S23W047_dem.tif']
roi = (-46.8, -21.7, -46.3, -22.1) # 利益区域.
iex = (-48, -21, -46, -23) # 图像范围.
data = RasterData(elevation_data, shape=(2, 2))
data.crop_image(iex, roi).write_image(
'output/dem.tif')
这与我们之前所做的是非常相似的,但是流程被简化为合并图像、裁剪并将结果写入 dem.tif 文件。它被选为 TIFF 文件,所以信息不会因为数据压缩而丢失。
- 运行代码。记住,因为它是一个新文件,你需要点击运行或按Alt + Shift + F10并选择 raster_data。你应该看到一个输出告诉你一切顺利:
处理完成,退出代码 0
从现在起,我们可以使用准备好的图像 output/dem.tif 进行测试。这仅仅是一个加快过程的问题。我们即将要做的事情可以在任何 RasterData 实例中完成。
如果由于任何原因,你无法生成 dem.tif,请将样本数据中提供的 dem.tif 复制到你的输出文件夹。
打印简单信息
我们要获取一些统计输出的第一步是探索 NumPy 能提供什么。
如我们所知,RasterData 实例的数据属性是一个 NumPy 数组,那么让我们看看我们能从中得到什么:
- 首先,检查到目前为止是否一切正常。清理 if name ==
'main': 块并添加以下新代码:
if name == 'main':
raster_data = RasterData('output/dem.tif')
打印 raster_data.data
- 使用 Shift + F10 运行代码。你应该看到以下输出:
[[ 933 935 942…, 1077 1076 1078]
[ 936 939 945…, 1075 1079 1076]
[ 935 939 946…, 1064 1072 1075]
...,
[ 780 781 781…, 1195 1193 1193]
[ 781 784 782…, 1191 1189 1188]
[ 781 784 785…, 1187 1185 1184]]
处理完成,退出代码为 0
这是包含数据的数组,单位为米。NumPy 自动抑制了一些行和列以减小输出大小。你看到这个输出是因为 NumPy 数组有一个 repr 方法,它在调用 print 函数时告诉应该显示什么。
正如我们之前对矢量数据所做的那样,我们将自定义类的 repr 方法,以便输出其中数据的一些信息。
- 编辑 RasterData 类并在 init 方法之后插入 repr 方法:
方法:
class RasterData(object):
def init(self, input_data, unchanged=True, shape=None):
...
def repr(self):
return "Hi, 我是一个栅格数据!"
- 现在,编辑 if name == 'main': 块并直接打印 RasterData 实例:
if name == 'main':
raster_data = RasterData('output/dem.tif')
print raster_data
- 运行代码并查看是否得到以下输出:
Hi, 我是一个栅格数据!
处理完成,退出代码为 0
好的,这里没有特别之处。这只是提醒大家,repr 方法不接受任何参数(除了实例 self)并且应该只返回一个字符串。此外,该方法在类中的位置没有影响。我们将其放置在 init 方法之后是为了组织上的考虑。
所有 魔法 方法都在类的开头一起。
现在我们已经设置好了一切,让我们探索数据属性中的 NumPy 数组。
为了避免重复,我将抑制代码中的类声明和 init 方法,并用 # 替换。
- 编辑 repr 方法,使其看起来如下:
...
def repr(self):
if self.data is None:
return "没有数据可显示!"
data = self.data
min = "最小值: {}".format(data.min())
mean = "平均值: {}".format(data.mean())
max = "最大值: {}".format(data.max())
return "Hi, 我是一个栅格数据!\n {} {} {}".format(
min, mean, max)
首先,要避免数据为空(None)时抛出异常。在这种情况下,该方法打印一条友好的消息,说明实例没有数据。如果实例有数据,则通过调用相应的方法准备包含最小值、平均值和最大值的三个字符串。最后,一个字符串被格式化以包含所有信息。
- 使用 Shift + F10 运行代码。你应该看到以下输出:Hi, 我是一个栅格数据!
最小值: 671 平均值: 1139.06559874 最大值: 1798
处理完成,退出代码为 0
那太好了!现在,我们有一些关于我们数据的统计信息。
但此代码笨拙,如果我们想添加或删除从 repr 返回的信息,我们需要进行大量的编辑。因此,在我们继续获取更多统计数据之前,我们将进行一些更改并自动化我们想要显示的信息的格式化过程。
格式化输出信息
在这一点上,我们正在以简单的字符串输出显示三个参数。我们想要改进这个代码,以便我们可以轻松地添加或删除输出中的参数。
在我们修改代码之前,让我们提前考虑我们可能还需要以其他格式输出这些统计数据,例如:
将数据以人类友好的格式保存到磁盘上的文件中
以计算机友好的格式(如 CSV 或 JSON)保存到磁盘上的文件中
作为传递给其他函数或方法的参数
因此,一种好的方法来准备代码以满足这些要求是将统计生成与输出分离,如下所示:
- 首先,将数据验证从 repr 方法中分离出来。创建一个新的方法来处理这个任务:
...
def _check_data(self):
"""检查是否有数据以及它是否是 Numpy 数组。"""
if self.data is None:
raise ValueError("未定义数据。")
elif not isinstance(self.data, np.ndarray):
raise TypeError("数据类型错误。")
验证更加严格,并且为每种可能的失败类型引发不同的异常。这使得代码模式既有用又安全,因为它允许在其他函数中执行错误处理,并且如果异常没有被正确捕获,它将停止程序的执行。
- 现在,创建一个新的方法来计算和收集我们迄今为止所拥有的统计数据:
...
def _calculate_stats(self):
"""从数据中计算并返回基本的统计信息。
"""
self._check_data()
data = self.data
stats = {
"最小值": data.min(),
"平均值": data.mean(),
"最大值": data.max()}
返回 stats
在这里,统计数据存储在字典中有两个原因:它允许项目具有可读的名称(包括如果您希望的话,重音符号和空格)并且它避免了名称冲突。
最后,它让我们准备一个包含计算出的统计信息的可读输出。为此,我们将使用 tabulate 模块。
- 在文件开头插入此导入:
coding=utf-8
导入 cv2 库
import numpy as np
from tabulate import tabulate
- 添加这个新方法:
...
def _format_stats(self, stats, out_format='human'):
"""以给定的输出格式格式化统计数据。
:param out_format: 'human' 或 'csv'
"""
table = []
for key, value in stats.iteritems():
table.append([key, value])
返回 tabulate(table)
tabulate 函数接受一个列表的列表,表示一个表格。然后它准备一个包含格式良好的表格字符串,其中包含这些数据。
- 最后,编辑 repr 方法:
...
def repr(self):
stats = self._calculate_stats()
stats = self._format_stats(stats)
return "栅格数据 - 基本统计。\n {}".format(stats) 6. 现在,再次使用 Shift + F10 运行代码。你应该看到以下输出:栅格数据 - 基本统计。
最小值 671
最大值 1798
平均值 1139.07
进程已退出,退出代码为 0
现在演示效果更好了。如果我们想添加或删除元素,我们只需编辑 _calculate_stats 方法中的字典。
计算四分位数、直方图和其他统计信息
我们有了数据的最小值、最大值和平均值。在我们的案例中,这是给定区域的最小值、最大值和平均值。在接下来的几个步骤中,我们将从数据中获得更多信息:
- 编辑 _calculate_stats 方法,向字典中添加更多项:
...
def _calculate_stats(self):
"""从数据中计算并返回基本统计信息。
"""
self._check_data()
data = self.data
stats = {
"最小值": data.min(),
"平均值": data.mean(),
"最大值": data.max(),
"Q1": np.percentile(data, 25),
"中位数": np.median(data),
"Q3": np.percentile(data, 75),
"方差": data.var(),
"直方图": np.histogram(data)
}
return stats
你可以向字典中添加任何值。也许,你可以从 NumPy 函数或方法或从你自己开发的功能中获取它。
注意
你可以在 NumPy 统计 中找到更多信息。
- 使用 Shift + F10 运行代码。你应该得到更多值作为输出:栅格数据 - 基本统计。
Q1 992.0
Q3 1303.0
最小值 671
方差 37075.0925323
直方图 (array([ 83917, 254729, ..., 44225, 8279, 2068]), array([ 671. , 783.7, ..., 1685.3, 1798.]))
中位数 1140.0
最大值 1798
平均值 1139.06559874
进程已退出,退出代码为 0
注意到输出中的直方图由两个数组组成:一个包含每个桶中发生的次数,另一个包含每个桶的上限。由于我们处理的是地理数据,如果这些信息被转换成每个区间的面积,会更好。
要做到这一点,我们只需将货币数量(给定范围内的像素数量)乘以每个像素表示的区域。我们将在下一节准备一些事情之后做到这一点。
将统计信息做成延迟属性
我们的统计功能现在运行良好,我们将对其进行改进。我们不再需要每次都计算所需的统计信息,而是一次性计算,并且仅在第一次需要时计算。
我们将使用两种非常有用的编程技术:延迟评估和记忆化。
懒加载是在过程或计算被延迟,并且仅在需要时才执行。记忆化是在昂贵过程的结果被存储以供以后使用,以避免每次可能需要时都重新计算。
让我们看看它是如何工作的:
1. 在 init 方法中添加一个新的 _stats 属性:
class RasterData(object):
def init(self, input_data, unchanged=True, shape=None):
"""表示为数组的栅格数据。
:param input_data: 栅格文件或 NumPy 数组。
:param unchanged: 如果为 True,则保持原始格式。
:param shape: 当使用多个输入数据时,此参数
确定组合的形状。
"""
self.data = None
self._stats = None
if isinstance(input_data, list) \
或 isinstance(input_data, tuple):
self.combine_images(input_data, shape)
else:
self.import_data(input_data, unchanged)
属性名以下划线开头。请记住,这种表示法表明该属性只能从实例本身访问。此属性将作为缓存来存储统计数据。
2. 现在添加一个返回统计信息的属性方法:
...
@property
def stats(self):
if self._stats is None:
self._stats = self._calculate_stats()
return self._stats
当访问此属性时,它将验证如果 _stats 为 None。如果是,则计算统计数据并将结果存储到 _stats 中。下次需要时,它只返回存储的内容。
当获取此信息的过程成本较高时,使属性延迟评估并添加记忆化非常重要。只有在需要给定属性时,才会使用处理能力和时间。
3. 现在,将 repr 方法更改为使用此新功能:
...
def repr(self):
stats = self._format_stats(self.stats)
return "Raster data basic statistics.\n {}".format(stats)
创建颜色分类图像
如果我们想在地图上显示图像信息,我们必须准备我们得到的结果的视觉输出。一种常见且高效的视觉表示形式是将值分为类别,并为每个类别分配不同的颜色。在我们的情况下,我们可以将数据分为高度类别。NumPy 使我们很容易做到这一点。让我们编写一个可以在管道中调用的方法来开始:
1. 在 RasterData 类中添加一个新的方法:
...
def colorize(self, style):
"""基于包含限制和颜色的样式生成 BGR 图像。
:param style: 包含限制和颜色的列表。
"""
shape = self.data.shape
limits = []
colors = []
分离限制和颜色。
for item in style:
limits.append(item[0])
colors.append(self._convert_color(item[1]))
colors = np.array(colors)
将每种颜色放入其限制中。
flat_array = self.data.flatten()
di_array = np.digitize(flat_array, limits)
di_array = di_array.reshape((shape[0], shape[1], 1))
results = np.choose(di_array, colors)
将 RGB 转换为 BGR。
results = np.asarray(results, dtype=np.uint8)
results = cv2.cvtColor(results, cv2.COLOR_RGB2BGR)
self.data = results
return self
为了实现我们的目标,这里发生了两个重要的事情。首先,数据通过 NumPy 的 digitize 函数索引到类别中。然后,每个类别都接收一个具有定义颜色的 RGB 值。这是使用 choose 函数完成的。
此方法接受一个样式作为参数。这个样式是一个限制和颜色的列表,就像地图图例一样。例如,样式可以定义为:style = [[700, "#f6eff7"],
[900, "#bdc9e1"],
[1100, "#67a9cf"],
[1300, "#1c9099"],
[1800, "#016c59"]]
这意味着所有低于 700 的值都将具有颜色 "#f6eff7",等等。颜色以十六进制表示。这种表示法在 Web 应用中很受欢迎,在这里选择它是因为它简短且易于输入或复制。
在这一点上,请注意,在这个方法内部,我们调用了 _convert_color 方法,该方法将执行颜色表示法的转换。让我们将此方法添加到类中:1. 将 _convert_color 方法添加到类中:
...
def _convert_color(self, color_code):
"""将颜色表示法进行转换。
:param color_code: 包含十六进制颜色的字符串
或者 JavaScript 表示法。
"""
if color_code[0] == "#":
result = (int(color_code[1:3], 16),
int(color_code[3:5], 16),
int(color_code[5:7], 16))
elif color_code[:3] == "rgb":
result = map(int, color_code[4:-1].split(','))
else:
raise ValueError("Invalid color code.")
return result
- 最后,编辑 if name == 'main': 块以测试我们的代码:if name == 'main':
raster_data = RasterData('output/dem.tif')
style = [[700, "#f6eff7"],
[900, "#bdc9e1"],
[1100, "#67a9cf"],
[1300, "#1c9099"],
[1800, "#016c59"]]
raster_data.colorize(style).write_image(
'output/classified.png')
- 运行代码,然后打开输出图像以查看结果:


选择地图的正确颜色
在地图中使用什么颜色是决定地图能否正确传递所需信息的一个决定性因素。为了选择一组好的颜色,应考虑以下因素:
人眼区分色调的能力——类别需要视觉上可区分,否则地图可能包含对某些人看起来相同颜色的颜色
地图将展示的媒体(例如,纸张或屏幕)——
根据媒体,颜色可能会发生轻微变化,这可能会损害地图的可读性
色盲安全性——这是一个包含度量,它允许更广泛的受众解释信息
数据类型(例如,顺序或定性)——使用与您想要展示的内容相匹配的颜色
关于这个主题有许多研究,Cynthia Brewer 的研究非常实用且在现代地图制作者中很受欢迎。她制作了一套广泛用于地图的颜色,并以 ColorBrewer 的名义提供这些信息供使用。
让我们借助 ColorBrewer 更改地图的颜色:1. 访问colorbrewer2.org/网站。您应该看到此界面:

2. 左侧面板允许您设置选择颜色的参数。在顶部,将数据类别数量更改为5,如我们的数据所示。
3. 关于我们数据的特点,两种选项都很好,顺序或发散。我将为此示例选择发散。
4. 在选择颜色方案之前,如果您愿意,可以通过色盲安全、打印友好和复印安全来过滤方案。
5. 现在,选择一个您喜欢的颜色方案,并注意面板和地图的右下角将更改以显示此方案的颜色。
6. 让我们将此方案以实用方式导出,以便在代码中使用。单击颜色右侧的导出选项卡。将打开一个新面板,如下所示:7. 注意JavaScript框中包含 RGB 值列表。我们可以在代码中轻松解析此信息。因此,我们将选择其内容并将其复制。
8. 返回我们的代码,并将颜色粘贴到名为 colorbrewer 的变量中,在 if name == 'main':块中:
if name == 'main':
colorbrewer = ['rgb(202,0,32)','rgb(244,165,130)',
'rgb(247,247,247)','rgb(146,197,222)',
'rgb(5,113,176)']
raster_data = RasterData('data/dem.tif')
style = [[700, "#f6eff7"],
[900, "#bdc9e1"],
[1100, "#67a9cf"],
[1300, "#1c9099"],
[1800, "#016c59"]]
raster_data.colorize(style).write_image(
'output/classified.png')
在这一点上,样式尚未完成。存在两个问题:颜色格式与我们所需的不同,并且我们没有与它们相关的限制。由于我们希望过程尽可能实用,我们将编写代码来解决这两个问题,而不是手动转换颜色并将它们与限制相关联。
首先,让我们实现程序接受颜色和限制分开的能力。
9. 将样式定义中存在的限制放入不同的列表中:if name == 'main':
colorbrewer = ['rgb(202,0,32)','rgb(244,165,130)',
'rgb(247,247,247)','rgb(146,197,222)',
'rgb(5,113,176)']
limits = [700, 900, 1100, 1300, 1800]
raster_data = RasterData('data/dem.tif')
raster_data.colorize(style).write_image('output/classified.png') 10. 现在编辑 colorize 方法:
..
def colorize(self, limits, raw_colors):
"""基于包含限制和颜色的样式生成 BGR 图像。
:param limits: 一系列限制。
:param raw_colors: 一系列颜色代码。"""
shape = self.data.shape
colors = []
for item in raw_colors:
colors.append(self._convert_color(item))
colors = np.array(colors)
将每个颜色放入其限制中。
flat_array = self.data.flatten()
di_array = np.digitize(flat_array, limits, right=True)
di_array = di_array.reshape((shape[0], shape[1], 1))
results = np.choose(di_array, colors)
将 RGB 转换为 BGR。
results = np.asarray(results, dtype=np.uint8)
results = cv2.cvtColor(results, cv2.COLOR_RGB2BGR)
self.data = results
return self
此方法现在接受两个参数而不是只有一个样式。唯一剩下的任务是将这种新的颜色格式转换过来。
- 编辑 _convert_color 方法:
...
def _convert_color(self, color_code):
"""转换颜色表示法。
:param color_code: 包含十六进制颜色的字符串
或者使用 JavaScript 语法。
"""
if color_code[0] == "#": result = (int(color_code[1:3], 16),
int(color_code[3:5], 16),
int(color_code[5:7], 16))
elif color_code[:3] == "rgb":
result = map(int, color_code[4:-1].split(','))
else:
raise ValueError("Invalid color code.")
return result
此方法现在能够检测和转换我们使用的两种颜色代码。如果颜色代码未被识别,它还可以引发异常。
- 为了测试代码,编辑 if name == 'main': 块以符合新的格式:
if name == 'main':
raster_data = RasterData('output/dem.tif')
colors = ['rgb(202,0,32)', 'rgb(244,165,130)',
'rgb(247,247,247)', 'rgb(146,197,222)',
'rgb(5,113,176)']
limits = [700, 900, 1100, 1300, 1800]
raster_data.colorize(limits, colors).write_image(
'output/classified.png')
- 最后,使用 Shift + F10 运行代码并检查输出。我选择的模式产生了以下结果:

尽管这是一张美丽的图片,但在颜色选择上存在错误。暖色调代表的是较低的海拔。这可能会导致混淆,因为在大多数地图上,规则是颜色越暖,数值越高。
这只是一个反转颜色的问题。让我们在我们的 colorize 方法中添加一个选项来做这个。
- 编辑 colorize 方法:
...
def colorize(self, limits, raw_colors, invert_colors=False):
"""根据包含限制和颜色的样式生成 BGR 图像。
:param limits: 一个限制列表。
:param raw_colors: 一个颜色代码列表。
:param invert_colors: 反转颜色顺序。
"""
shape = self.data.shape
colors = []
if invert_colors:
raw_colors = list(reversed(raw_colors))
转换颜色。

for item in raw_colors:
colors.append(self._convert_color(item))
colors = np.array(colors)
将每种颜色放入其限制范围内。
flat_array = self.data.flatten()
di_array = np.digitize(flat_array, limits, right=True)
di_array = di_array.reshape((shape[0], shape[1], 1))
results = np.choose(di_array, colors)
将 RGB 转换为 BGR。
results = np.asarray(results, dtype=np.uint8)
results = cv2.cvtColor(results, cv2.COLOR_RGB2BGR)
self.data = results
return self
- 现在,再次编辑 if name == 'main': 块:
if name == 'main':
raster_data = RasterData('output/dem.tif')
colors = ['rgb(202,0,32)', 'rgb(244,165,130)',
'rgb(247,247,247)', 'rgb(146,197,222)',
'rgb(5,113,176)']
limits = [700, 900, 1100, 1300, 1800]
raster_data.colorize(limits, colors, True).write_image(
'output/classified.png')
- 运行代码并查看新的输出:
图像混合
如果我们能够将着色图像与阴影高程图像结合起来,我们的结果将更加具有视觉吸引力和信息量。同样,由于我们处理的是数组,我们可以推断出这种组合可以通过在两个数组之间执行算术运算来实现。
在图像处理中,这被称为alpha 混合。基本上,对两个图像都应用了透明度,然后它们被混合成一个新的图像。在接下来的步骤中,我们将创建一个执行此操作的功能:
- 首先,为了避免多次生成阴影高程,让我们将其保存在磁盘上,并编辑 raster_data.py 文件的 if name == 'main':块:if name == 'main':
raster_data = RasterData('output/dem.tif')
raster_data.adjust_values().create_hillshade(
10, 60).write_image('output/shaded.png')
-
运行代码并检查图像是否正确写入磁盘。
-
现在,将 alpha_blend 方法添加到 RasterData 类中:
...
def alpha_blend(self, raster_data, alpha=0.5):
"""将此栅格数据与另一个数据合并。
:param raster_data: RasterData 实例。
:param alpha: 应用透明度的量。
"""
shade = cv2.cvtColor(raster_data.data, cv2.COLOR_GRAY2BGR)
result = (1-alpha) * self.data + alpha * shade
self.data = result
return self
- 最后,再次编辑 if name == 'main':块以测试代码:if name == 'main':
shaded = RasterData('output/shaded.png')
classified = RasterData('output/classified.png')
classified.alpha_blend(shaded).write_image(
'output/color_shade.png')
- 运行代码并检查输出文件夹中的图像:

你应该看到这个美丽的输出。注意阴影高程与着色图像的结合如何产生一个即使对于未经训练的眼睛也能传达大量信息的地图。

用颜色显示统计数据
地图着色仅是定义样式中的限制和颜色的问题。因此,如果我们想将统计信息转换为颜色,我们只需将我们想要的值与一系列颜色关联起来。
首先,让我们用四分位数来尝试:
- 由于所有准备工作都在我们的课程中完成,我们只需要更改 if name == 'main':块中的代码:
if name == 'main':
dem = RasterData('output/dem.tif')
shaded = RasterData('output/shaded.png')
limits = [dem.stats['Q1'],
dem.stats['Q3'],
dem.stats['Maximum']]
colors = ["#fc8d59", "#ffffbf", "#91cf60"]
dem.colorize(limits, colors).write_image('output/stats.png') dem.alpha_blend(shaded).write_image('output/shaded_stats.png') 以下图像展示了分析参数的彩色输出:

对于这张图像,你可以这样开始引导:
使用直方图对图像进行着色 我们还可以使用直方图对地图进行着色。NumPy 生成的直方图由两个一维数组组成。第一个包含给定区间内的发生次数(即像素数量)。第二个包含桶或限制。默认情况下,直方图使用 11 个桶,因此我们还需要 11 种不同的颜色来生成地图。让我们改变我们的测试来看看这是如何工作的:1. 编辑 if name == 'main':块:
if name == 'main':
dem = RasterData('data/dem.tif')
shaded = RasterData('output/shaded.png')
colors = ['rgb(103,0,31)','rgb(178,24,43)','rgb(214,96,77)',
'rgb(244,165,130)','rgb(253,219,199)',
'rgb(247,247,247)','rgb(209,229,240)',
'rgb(146,197,222)','rgb(67,147,195)',
'rgb(33,102,172)','rgb(5,48,97)']
limits = dem.stats['Histogram'][1]
dem.colorize(limits, colors, True).write_image('output/hist.png') dem.alpha_blend(shaded).write_image('output/shaded_hist.png') 这里使用的颜色也来自 ColorBrewer。它们从红色到蓝色具有发散性质。限制是通过简单地使用 stats 属性和包含桶的第二个数组从直方图中获得的。
- 运行代码并查看输出。

阴影结果应该看起来像以下图像:

使用更多的类别可以更好地表示高度变化,并使我们能够清楚地看到高海拔的峰值。
总结
在本章中,我们处理了原始栅格数据,并使用一系列技术从中提取统计信息,并将其显示在高质量地图上。这些程序使我们能够在地理空间信息方面达到高水平的沟通,因为产生的材料易于解释,即使是未经训练的眼睛也能看懂。
在下一章中,我们将进入一个新的领域,并开始关注我们代码的效率,以便及时处理大量地理空间数据集。
第八章 数据挖掘应用
随着数据量的增加,新的挑战也随之而来。大量数据带来与过度处理时间和大量内存消耗相关的问题。这些问题可能会使数据分析变得痛苦,甚至可能使其完全不可能进行。
在本章中,我们将创建一个能够以高效方式处理大量数据集的应用程序。我们将审查我们的代码,实现新的工具和技术,这将使我们的分析不仅运行得更快,而且还能更好地利用计算机硬件,使几乎任何数量的数据都能被处理。
为了实现这些目标,我们将学习如何使用数据库以及如何将数据流式传输到它们中,使计算能力的使用保持恒定和稳定,无论数据量的大小。
这些工具还将使我们能够执行更高级的搜索、计算,并从不同来源获取交叉信息,让您能够挖掘宝贵的信息。
本章将涵盖以下主题:
代码效率是什么以及如何衡量它
如何将数据导入到空间数据库中
如何将数据库数据抽象为 Python 对象
查询和从空间数据库中获取信息 理解代码效率
效率代码的构成取决于正在分析的点。当我们谈论计算效率时,有四个点可能需要考虑:
代码执行所需的时间
运行时使用的内存量
占用的磁盘空间量
代码是否使用了所有可用的计算能力
优秀的、高效的代码不仅关乎计算效率;它还关乎编写能够为开发过程带来这些有利品质的代码(仅举几个例子):
清洁且有序的代码
易读的代码
易于维护和调试
广泛的
防止误用
显然,有些点是相互矛盾的。这里只举几个例子。为了加快一个过程,您可能需要使用更多的内存。为了使用更少的内存,您可能需要更多的磁盘空间。或者,为了获得更快的代码,您可能需要放弃泛化,
编写非常具体的函数。
是开发者根据软件需求和投资于某一点或另一点所获得的收益来决定对抗性特征之间的平衡。
例如,如果可以编写出非常干净的代码,同时执行时间上的损失很小,开发者可能会选择编写干净且易于维护的代码,这将使他和他的团队能够更容易地理解。
第二组好的特性容易受到人类评估的影响,而第一组中的项目可以通过计算机进行测量和比较。
测量执行时间
为了测量一段代码的执行速度,我们需要测量其执行时间。所测量的时间是相对的,并受多种因素的影响:操作系统、是否有其他程序正在运行、硬件等。
在我们的效率测试中,我们将测量执行时间,对代码进行修改,然后再次测量。这样,我们将看到这些修改是否提高了代码效率。
让我们从简单的例子开始,并测量其运行所需的时间。
- 如前所述,在您的 geopy 项目中复制上一章的文件夹,并将其重命名为 Chapter8。您的项目结构应如下所示:
├───Chapter1
├───Chapter2
├───Chapter3
├───Chapter4
├───Chapter5
├───Chapter6
├───Chapter7
├───Chapter8
│ ├───experiments
│ ├───map_maker
│ ├───output
│ └───utils
└───data
-
点击您的实验文件夹,并在其中创建一个新的 Python 文件。将该文件命名为 timing.py。
-
现在将以下代码添加到该文件中:
coding=utf-8
def make_list1(items_list):
result = ""
for item in items_list:
template = "我喜欢{}。 \n"
text = template.format(item)
result = result + text
return result
if name == 'main':
my_list = ['培根', '千层面', '沙拉', '鸡蛋', '苹果']
print(make_list1(my_list))
- 再次运行代码。按 Alt + Shift + F10 并从列表中选择一个计时。你应该得到这个输出:
我喜欢培根。
我喜欢千层面。
我喜欢沙拉。
我喜欢鸡蛋。
我喜欢苹果。
没有什么花哨的,这是一个简单的低效函数,用于格式化文本并生成可打印的事物列表。
- 现在我们将测量执行所需的时间。修改你的代码:
coding=utf-8
from timeit import timeit
def make_list1(items_list):
result = ""
for item in items_list:
template = "我喜欢{}。 \n"
text = template.format(item)
result = result + text
return result
if name == 'main':
my_list = ['培根', '千层面', '沙拉', '鸡蛋', '苹果']
number = 100
execution_time = timeit('make_list1(my_list)',
setup='from main import make_list1, my_list',
number=number)
print("执行代码{}次花费了{}秒"。format(
execution_time, number))
- 再次使用 Shift + F10 运行代码并查看结果:执行代码 100 次花费了 0.000379365835017 秒
Process finished with exit code 0
这里我们使用 timeit 模块来测量我们函数的执行时间。
由于一些代码运行速度很快,我们需要多次重复执行以获得更精确的测量和更有意义的数字。重复执行语句的次数由数字参数给出。
- 将你的数字参数增加到 1000000 并再次运行代码:执行代码 1000000 次花费了 3.66938576408 秒
Process finished with exit code 0
现在我们有一个更一致的数字可以工作。如果你的电脑比我的快得多,你可以增加这个数字。如果它更慢,就减少它。
拿一张纸并记下那个结果。我们将更改函数,看看我们是否使代码更高效。
- 添加我们函数的另一个版本;命名为 make_list2: def make_list2(items_list):
result = ""
template = "我喜欢{}。 \n"
for item in items_list:
text = template.format(item)
result = result + text
return result
- 还要更改你的 if name == 'main': 块。我们将清楚地说明正在执行函数的哪个版本:
if name == 'main':
my_list = ['培根', '千层面', '沙拉', '鸡蛋', '苹果']
number = 1000000
function_version = 2
statement = 'make_list{}(my_list)'.format(function_version) setup = 'from main import make_list{}, my_list'.format(
function_version)
execution_time = timeit(statement, setup=setup, number=number) print("版本{}。".format(function_version))
print("执行代码{}次花费了{}秒"。format(
execution_time, number))
- 再次运行代码并查看结果。在我的电脑上,我得到了这个:版本 2。
It took 3.5384931206s to execute the code 1000000 times
Process finished with exit code 0
这是对执行时间的一点点改进。在版本 2 中,我们唯一做出的改变是将模板移出了 for 循环。
- 创建函数的第三个版本:
def make_list3(items_list):
result = ""
template = "I like "
for item in items_list:
text = template + item + ". \n"
result = result + text
返回结果
- 将你的 function_version 变量更改为 3 并再次运行代码:版本 3。
执行 1000000 次代码耗时 1.88675713574 秒
进程以退出代码 0 结束
现在我们改变了字符串"I like "的构成方式。我们不是使用字符串格式化,而是添加了字符串的部分,得到的代码比上一个版本快了近两倍。
你可以通过试验、查阅互联网上的文章或通过经验来找出哪些小的改动可以减少执行时间。但有一种更肯定、更强大的方法来找出代码中花费更多时间的地方;这被称为分析。
代码分析
通过试验,我们发现代码中最昂贵的部分是字符串格式化。
当你的代码变得更加复杂时,通过这种方法找到瓶颈变得更加困难,在某个时候变得不切实际。
解决方案是分解并分析小块代码。为了查看它们执行所需的时间,对代码进行剖析。
Python 自带了一个很好的分析工具,它可以在一定程度上自动化这个过程。
让我们在我们的代码上使用它,看看它告诉我们什么:
- 在文件开头添加此导入:
from timeit import timeit
导入 cProfile
- 编辑你的 if name == 'main':块以使用分析器:if name == 'main':
my_list = ['bacon', 'lasagna', 'salad', 'eggs', 'apples']
number = 1000000
profile = cProfile.Profile()
profile.enable()
for i in range(number):
make_list1(my_list)
profile.disable()
profile.print_stats(sort='cumulative')
- 运行代码。你应该能在控制台上看到分析器统计信息。(由于空间原因,我抑制了一些信息):
6000002 次函数调用在 4.755 秒内
按照累积时间排序
ncalls tottime percall cumtime percall
1000000 2.718 0.000 4.738 0.000 timing.py
5000000 2.019 0.000 2.019 0.000 {'format' ...}
1 0.017 0.017 0.017 0.017 {range}
1 0.000 0.000 0.000 0.000 {'disable' ...}
执行分析器有多种方式。在我们的例子中,我们实例化了 Profile 类,并使用 enable 和 disable 方法来告诉分析器从哪里开始和停止收集数据。再次强调,调用 make_list1 的次数被重复了 1000000 次,以生成更大的数字。
在输出中,我们可以看到 make_list1 被调用了 1000000 次,format 方法被调用了五百万次,耗时 2.019 秒。请注意,分析器只提供了有关方法和函数的信息。
在数据库上存储信息
在前面的章节中,基本的流程是在每次运行代码时将所有数据导入内存作为 Python 对象。当我们处理小块数据时,这完全可行且效率很高。
在某个时候,你可能已经注意到我们代码的性能受到了削弱,尤其是在我们开始导入国家边界以及所有属性时。这是因为导入属性很慢。
其次,尽管我们的过滤机制工作得相当好,但我们处理大型数据集时可能会遇到问题。
解决这些问题的公式非常简单,只包含两个基本成分:
只获取你需要的内容
使用索引搜索
第一点是只获取你需要的记录,以及获取给定分析所需的属性。
第二点关于如何找到事物。在我们的方法中,一个循环会测试每条记录的条件,直到找到所需的记录(即测试返回 True)。或者,如果计算机有关于项目位置的某种想法,搜索将会更快;这就是索引。
而不是试图自己实现这些功能,我们可以使用数据库来为我们处理这些机制;它们是这个类型工作的最先进技术。
这里我们有两个选择:
使用带有Spatialite扩展的SQLite数据库,这是一个开源、简单且非常高效的 SQL 数据库。它不需要服务器或安装,Python 自带了连接到它的连接器。
使用带有Postgis扩展的PostgreSQL数据库。这也是一个开源且强大的数据库服务器。
选择权在你,除了在设置中有一小点变化外,它不会对代码产生影响。
提示
你可以从以下链接下载 PostgreSQL:www.postgresql.org/. 为了启用 Postgis,你只需要在安装过程中在堆栈构建器中选择它。
如果你使用 Ubuntu,你可以在以下链接中找到更多详细信息:
创建对象关系映射
对象关系映射(ORM)是我们将使用的方法,将存储在数据库中的数据转换为 Python 对象。这与我们在 models.py 文件中做的没有区别,我们在那里编写了将存储在地理文件(例如,GPX 形状文件)中的数据转换为 Python 对象的代码。
这次,我们将通过将数据导入数据库来闭合这个循环,然后稍后以前那种优雅直观的方式从其中检索数据或信息。
SQL 数据库,如 SQLite,将数据存储在具有行和列的表中。以下表格说明了我们之前使用的地理缓存数据将如何以这种格式表示:
ID Geom Name
状态
所有者提示
1
(wkb)LaSalle Park 可用 John
在签名下
2
(wkb)停车场
可用的 Nina
大树
我们可以猜测这与能够导入任何类型数据的假设不相符,因为列的类型是固定的。如果我们有具有不同属性或更多属性的数据,我们需要不同的表或添加更多列以匹配所有可能性。
为了克服此架构的限制,我们将使用 SQL 的关系功能
数据库。我们将存储项目和属性在不同的表中,并将它们关联起来:点
ID 几何
1
42.89 - 78.90
2
43.00 - 78.0
属性
ID 键
值
point_id
1
名称 LaSalle Park 1
2
状态 可用
1
3
拥有者 John
1
4
提示
签名下
1
5
名称 停车场
2
6
状态 可用
2
7
拥有者 Nina
2
8
提示
大树
2
这种键/值数据模型允许每个点(或其他对象)具有任意数量和类型的属性。每个属性通过一个 ID 与其所有者关联。
你可能听说过 Django,这是一个包含所有功能的 Python 网络框架。事实上,Django 内置了一个出色的 ORM,并且它对地理空间数据库和地理空间操作有非常成熟的支持(Django 的这部分称为 GeoDjango,默认包含)。你也会注意到,从我们的模型到 Django 的过渡将非常平滑,它们将像以前一样易于使用。
准备环境
为了使用 Django 的 ORM,我们需要设置一个 Django 项目。为此,我们将准备所需的最小结构,这包括几个文件和设置。
首先,让我们设置我们的应用程序以使用 Django。
- 在你的 Chapter8 文件夹中,创建一个名为 settings.py 的新 Python 文件。如果你使用 PostgreSQL/Postgis,请将以下代码添加到文件中:DATABASES = {
'default': {
'ENGINE': 'django.contrib.gis.db.backends.postgis',
'NAME': 'postgres',
'USER': 'postgres',
'PASSWORD': 'mypassword',
'PORT': 5432
}}
第一个项目(DATABASES)是数据库设置。如果你有默认的 PostgreSQL/Postgis 安装,这将有效。只需更改你在安装过程中设置的密码。
- 如果你使用 SQLite/Spatialite,请使用以下配置:DATABASES = {
'default': {
'ENGINE': 'django.contrib.gis.db.backends.spatialite',
'NAME': 'mydatabase.db'
}}
- 在数据库配置后,添加以下项:
INSTALLED_APPS = ('django.contrib.gis', 'geodata')
SECRET_KEY = 'abc'
INSTALLED_APPS 项告诉 Django 在哪里查找模型。SECRET_KEY 用于 Django 的用户管理。尽管我们不会使用它,但需要设置(可以使用任何值作为密钥)。
- 现在创建一个 Python 包,它将成为我们的 Django 应用。在 Chapter8 文件夹下右键单击并选择 新建 | Python 包。将其命名为 geodata。
文件夹下右键单击并选择 新建 | Python 包。将其命名为 geodata。
- 在 Chapter8 内创建一个新的 Python 文件,并将其命名为 geodata_app.py。
修改我们的模型
我们已经有了基本结构,现在我们需要调整我们的模型,以便它们可以使用数据库而不是将所有信息存储在内存中。Django 的模型定义与我们非常相似。
利用 Django 提供的新功能,我们将对设计选择进行一个更改:而不是为每种类型的对象(地理藏宝、道路、边界等)创建一个类,我们将只有一个可以存储所有这些数据以及我们可以想到的其他数据的类。
- 在 geodata 文件夹内创建一个名为 models.py 的文件,并添加以下代码:
coding=utf-8
from django.contrib.gis.db import models
class GeoObject(models.Model):
geom = models.GeometryField()
atype = models.CharField(max_length=20)
objects = models.GeoManager()
GeoObject 类代表一个单独的对象(表中的一行)。它可以在 geom 字段中接受任何类型的几何形状(一个点、多边形等)。atype 属性代表对象的高级类型。这个属性将告诉我们它是一个地理藏宝点还是其他东西(我们使用 atype 而不是 type 是为了避免与内部 type()函数冲突)。
最后,objects 属性代表 GeoObject 集合(数据库中的表)。在 Django 中,这被称为管理器;不要担心,我们稍后会看到更多关于这个的内容。
- 现在我们需要为 GeoObject 添加标签;标签将包含每个属性。在 GeoObject 类之后添加另一个类。
class Tag(models.Model):
key = models.CharField(max_length=250)
value = models.CharField(max_length=250)
geo_object = models.ForeignKey(GeoObject, related_name='tags') 再次,这个类代表一个单独的对象,一个带有键和值的单独标签,通过外键与 GeoObject 连接。结果是,Tag 类有一个 GeoObject,而 GeoObject 有多个标签。
自定义管理器
如前所述,经理可以被视为数据库中表的一种表示。它包含检索记录、添加、删除以及许多其他操作的方法。
Django 附带了一个 GeoManager 类,用于包含空间对象的表。如果我们想为我们的 GeoData 管理器添加更多功能,我们只需要从 GeoManager 继承,然后在 GeoObject 类中添加一个具有其实例的类属性。实际上,我们只是替换了 objects 属性中的实例。
让我们将我们的 BaseGeoCollection 类调整为 GeoObject 类的管理器:1. 导航到你的 Chapter8/models.py 文件(我们在前面的章节中编写的那个文件)并将其重命名为 Chapter8/old_models.py。通过这样做,我们可以避免混淆,不清楚我们在谈论哪个模型。
- 在 geodata 文件夹内创建一个名为 managers.py 的文件。将以下代码添加到该文件中:
coding=utf-8
from django.contrib.gis.db.models import GeoManager
from utils.geo_functions import open_vector_file
class GeoCollection(GeoManager):
"""这个类代表空间数据集合。"""
Pass
这是迁移我们的 BaseGeoCollection 类的第一步。请注意,我们将其命名为 GeoCollection,因为它将不再是基类。我们将简化我们的代码,这样这个类将管理所有类型的地理对象。为此,我们将从 BaseGeoCollection 类中添加 import_data 方法,并将其与 PointCollection 类中的 _parse_data 方法结合。在我们继续之前,让我们看看这些方法现在的样子(你不需要输入此代码):
...
def import_data(self, file_path):
"""打开与 OGR 兼容的矢量文件并解析数据。
:param str file_path: 文件的完整路径。
"""
features, metadata = open_vector_file(file_path)
self._parse_data(features)
self.epsg = metadata['epsg']
print("文件导入成功: {}".format(file_path))
...
def _parse_data(self, features):
"""将数据转换为 Geocache 对象。
:param features: 特征列表。
"""
for feature in features:
coords = feature['geometry']['coordinates']
point = Point(float(coords[1]), float(coords[0]))
attributes = feature['properties']
cache_point = Geocache(point, attributes=attributes)
self.data.append(cache_point)
注意,import_data 函数首先打开矢量文件,然后将特征发送到 _parse_data 函数,该函数遍历数据,创建点并将特征属性放入字典中。如果我们成功导入任何类型的几何形状并将特征属性传递给标签模型,我们最终将得到一段代码,它可以服务于任何类型的地理空间对象。
- 再次编辑 geodata/managers.py 中的代码。无论你是想复制并编辑提到的函数还是从头开始输入新的 import_data 方法,都取决于你。
生成的代码应该是以下内容:
coding=utf-8
from django.contrib.gis.db.models import GeoManager
from django.db import IntegrityError, DataError
from utils.geo_functions import open_vector_file
from shapely.geometry import shape
class GeoCollection(GeoManager):
"""这个类代表一组空间数据。”
def import_data(self, file_path, atype):
"""打开与 OGR 兼容的矢量文件并解析数据。
:param str file_path: 文件的完整路径。
"""
features, metadata = open_vector_file(file_path)
for feature in features:
geom = shape(feature['geometry'])
geo_object = self.model(geom=geom.wkt, atype=atype)
geo_object.save()
for key, value in feature['properties'].iteritems():
try:
geo_object.tags.create(key=key, value=value)
except (IntegrityError, DataError):
pass
print("文件导入成功: {}".format(file_path))
我们使用了 Shapley 的 shape 函数,直接将 feature['geometry'](一个类似于 GeoJSON 几何的字典)转换为正确的 shapely 几何类型。
然后,我们使用该几何形状来获取其 WKT 表示形式。
方法中包含了 atype 参数,我们可以用它来定义 GeoObject 的类型。请记住,atype 不是一个几何类型;它代表对象的较高层次类型(地理缓存、边界、道路、河流、航点等)。
在语句 geo_object = self.model(geom=geom.wkt, atype=atype)中,我们看到了 Django 管理器的伟大特性:相同的管理器可以被多个模型使用,self.model 包含了对从该管理器被调用的类的引用。
如果我们决定采用另一种设计模式,并为每种对象类型使用一个类,我们仍然可以使用相同的管理器来管理所有这些。
按顺序,模型被保存,然后遍历属性字典,并为每个项目创建一个标签。我们在这里捕获异常,因为我们有两个可能发生的特殊条件:如果属性的值为 None,它将引发 IntegrityError;如果值的长度大于 250,它将引发 DataError。如果你对长字段感兴趣,例如来自地理藏宝数据的日志,你可以增加字段 max_length 或尝试不同的字段类型。
- 我们在这里不使用元数据,读取它可能会在 Windows 用户之间引起库不兼容错误。因此,我们将从 open_vector_file 函数中删除它。编辑你的 utils/geo_functions.py 文件以更改此函数。此外,让我们打印读取到的要素数量:def open_vector_file(file_path):
"""打开与 OGR 兼容的矢量文件或 GPX 文件。
返回要素列表和有关文件的信息。
:param str file_path: 文件的完整路径。
"""
datasource = ogr.Open(file_path)
检查文件是否已打开。
if not datasource:
if not os.path.isfile(file_path):
message = "Wrong path."
else:
message = "File format is invalid."
raise IOError('Error opening the file {}\n{}'.format(
file_path, message))
file_name, file_extension = os.path.splitext(file_path)
检查它是否是 GPX,如果是,则读取它。
if file_extension in ['.gpx', '.GPX']:
features = read_gpx_file(file_path)
如果不是,请使用 OGR 获取要素。
else:
features = read_ogr_features(datasource.GetLayerByIndex(0)) print("{} features.".format(len(features)))
return features
- 最后,编辑 geodata/models.py 以导入和使用新的管理器:
coding=utf-8
from django.contrib.gis.db import models
from managers import GeoCollection
class GeoObject(models.Model):
geom = models.GeometryField()
atype = models.CharField(max_length=20)
objects = GeoCollection()
class Tag(models.Model):
key = models.CharField(max_length=250)
value = models.CharField(max_length=250)
geo_object = models.ForeignKey(GeoObject, related_name='tags') 我们几乎准备好开始测试了。此时,你的第八章结构应该是这样的:
+---Chapter8
| geocaching_app.py
| geodata_app.py
| map_maker_app.py
| models_old.py
| raster_data.py
| settings.py
| settings.pyc
| init.py
|
+---experiments
|
+---geodata
| | managers.py
| | models.py
| | init.py
|
+---map_maker
|
+---output
|
---utils
check_plugins.py
data_transfer.py
geo_functions.py
生成表和导入数据 现在是时候让 Django 为我们生成数据库表了。由于我们的模型已经定义,我们只需要调用一对命令,Django 就会施展其魔法。
- 返回 geodata_app.py 文件并添加一些内容:
coding=utf-8
import os
import django
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings") django.setup()
from django.core.management import call_command
from geodata.models import *
def prepare_database():
"""在设置数据库或更改模型时调用此命令。”
"""
call_command('makemigrations', 'geodata')
call_command('migrate', 'geodata')
if name == 'main':
prepare_database()
在我们导入 os 和 django 之后,我们需要指定它应该查找哪个设置文件。之后,django.setup() 初始化 Django。
The prepare_database 函数调用两个负责数据库创建的 Django 管理命令。每次我们更改我们的模型时,都需要调用它。内部,Django 会记录所做的更改并自动生成执行数据库修改的 SQL 查询。
- 现在运行你的代码。如果一切顺利,你应该会在输出中看到数据库迁移的结果:
Migrations for 'geodata':
0001_initial.py:
-
创建模型 GeoObject
-
创建模型 Tag
需要执行的操作:
Apply all migrations: geodata
Running migrations:
Rendering model states… DONE
Applying geodata.0001_initial… OK
Process finished with exit code 0
- 现在,再次编辑 geodata_app.py 以添加一个导入一些数据的方便函数。
我们将使用地理藏宝数据作为测试:
coding=utf-8
import os
import django
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings") django.setup()
from django.core.management import call_command
from geodata.models import *
def prepare_database():
"""在设置数据库或更改模型时调用此命令。
"""
call_command('makemigrations', 'geodata')
call_command('migrate', 'geodata')
def import_initial_data(input_file, atype):
"""将新数据导入数据库。”
print("Importing {}...".format(atype))
GeoObject.objects.import_data(input_file, atype)
print("Done!")
if name == 'main':
prepare_database()
import_initial_data("../data/geocaching.gpx", 'geocaching') 这个新函数仅是一个方便函数,以减少输入,因为我们很快就会导入大量数据。我们正在注释掉 prepare_database() 语句,因为我们稍后会使用它。
- 现在运行你的代码(确保只运行一次以避免重复条目)。在你的输出中你应该看到以下内容:
Importing geocaching…
112 个特征。
Done!
Process finished with exit code 0
过滤数据
现在我们数据库中有了一些数据,是时候测试它并看看我们是否能像之前那样过滤一些点。
- 编辑你的 if name == 'main': 块(记得注释掉之前的命令):
if name == 'main':
prepare_database()
从'../data/geocaching.gpx'导入初始数据('geocaching') points = GeoObject.objects.filter(atype='geocaching',
tags__key='状态',
tags__value='可用')
打印(points 的长度)
对于 points[0].tags.all()中的每个 tag:
打印(tag.key, tag.value)
在这里,我们使用我们的管理器继承的过滤方法来过滤地理藏宝类型的相关记录。此外,我们通过在属性名后使用双下划线来访问相关的标签,以过滤仅可用的地理藏宝。这是通过打印返回的第一个点的所有标签来完成的。
- 运行你的代码,你应该会看到一个类似这样的标签列表:224
(u'类型', u'其他')
(u'提示', u'在签名下')
(u'时间', u'2013-09-29T00:00:00Z')
(u'州', u'纽约')
(u'国家', u'美国')
(u'URL', u'http://www.opencaching.us/viewcache.php?cacheid=1728') (u'名称', u'LaSalle Park No 1')
(u'容器', u'虚拟')
(u'来源', u'www.opencaching.us')
(u'@xmlns', u'http://geocaching.com.au/geocache/1')
(u'desc', u'LaSalle Park No 1 by Mr.Yuck, Unknown Cache (1/1)') (u'urlname', u'LaSalle Park No 1')
(u'owner', u'Mr.Yuck')
(u'difficulty', u'1')
(u'sym', u'Geocache')
(u'terrain', u'1')
(u'status', u'Available')
Process finished with exit code 0
导入大量数据
Now that our environment is ready, we can begin working with bigger datasets. Let’s start by profiling the import process and then optimize it. We will start with our small geocaching dataset and after the code is optimized we will move to bigger sets.
- In your geodata_app.py file, edit the if name == 'main': block to call the profiler.
if name == 'main':
profile = cProfile.Profile()
profile.enable()
import_initial_data("../data/geocaching.gpx", 'geocaching') profile.disable()
profile.print_stats(sort='cumulative')
2. Run the code and see the results. Don’t worry about duplicated entries in the 数据库 now, we will clean it later. (I removed some information from the following output for space reasons.)
Importing geocaching…
112 features.
Done!
1649407 function calls (1635888 primitive calls) in 5.858 seconds cumtime percall filename:lineno(function)
5.863 5.863 geodata_app.py:24(import_initial_data)
5.862 5.862 managers.py:11(import_data)
4.899 0.002 related.py:749(create)
4.888 0.002 manager.py:126(manager_method)
3.621 0.001 base.py:654(save)
3.582 0.001 base.py:737(save_base)
3.491 0.001 query.py:341(create)
1.924 0.001 base.py:799(_save_table)
ncalls tottime percall cumtime percall filename:lineno(function) 1 0.001 0.001 5.863 5.863 (import_initial_data) 1 0.029 0.029 5.862 5.862 (import_data)
2497 0.018 0.000 4.899 0.002 related.py:749(create) Take a look at ncalls and cumtime for each of the functions. The create function is called a lot of times and accumulates almost five seconds on my computer. This is the function (method) called when we add a tag to a GeoObject. The time spent on this function is relevant when we import geocaching data because every point has a lot of attributes. Maybe we can make this process more efficient.
优化数据库插入
As we saw in the profiler, the method we are using to insert the tags into the database creates a bottleneck when we import geocaching data with our current code. If we can change how it’s done, we can make the code run faster.
1. 前往您的管理器并编辑 GeoCollection 管理器的 import_data 方法:
class GeoCollection(GeoManager):
"""This class represents a collection of spatial data."""
def import_data(self, file_path, atype):
"""打开与 OGR 兼容的矢量文件并解析数据。
:param str file_path: The full path to the file.
"""
from models import Tag
features = open_vector_file(file_path)
tags = []
for feature in features:
geom = shape(feature['geometry'])
geo_object = self.model(geom=geom.wkt, atype=atype)
geo_object.save()
geoo_id = geo_object.id
for key, value in feature['properties'].iteritems():
tags.append(Tag(key=key, value=value,
geo_object_id=geoo_id))
Tag.objects.bulk_create(tags)
现在不是逐个创建标签,而是将它们添加到列表中而不触及数据库;只有在最后才调用 bulk_create,这将在单个请求中插入所有条目。注意,Tag 模型的导入语句位于函数内部。
这将避免循环导入错误,因为模型也导入管理器。
- 运行你的代码并查看发生了什么:
django.db.utils.DataError: value too long for type character varying(250)
Process finished with exit code 1
由于 bulk_insert 将所有内容一起发送到数据库,我们无法捕获单个标签的异常。
解决方案是在插入之前验证标签。在这个阶段,我们正在权衡通用性和性能,因为验证可能会根据数据类型失败,而错误捕获可能由多种原因触发。
- 再次编辑代码:
class GeoCollection(GeoManager):
"""此类表示一组空间数据。”
def import_data(self, file_path, atype):
"""打开与 OGR 兼容的矢量文件并解析数据。”
:param str file_path: 文件的完整路径。
"""
from models import Tag
features = open_vector_file(file_path)
tags = []
for feature in features:
geom = shape(feature['geometry'])
geo_object = self.model(geom=geom.wkt, atype=atype)
geo_object.save()
geoo_id = geo_object.id
for key, value in feature['properties'].iteritems():
if value and (isinstance(value, unicode)
or isinstance(value, str)):
if len(value) <= 250:
tags.append(Tag(key=key, value=value,
geo_object_id=geoo_id))
Tag.objects.bulk_create(tags)
- 现在再次运行 geodata_app.py 并查看分析结果:1.144 秒内进行了 506679 次函数调用(506308 次基本调用),排序依据:累积时间
ncalls cumtime percall filename:lineno(function)
1 1.144 1.144 geodata_app.py:24(import_initial_data)
1 1.142 1.142 managers.py:12(import_data)
1 0.556 0.556 geo_functions.py:91(open_vector_file)
1 0.549 0.549 geo_functions.py:9(read_gpx_file)
1 0.541 0.541 xmltodict.py:155(parse)
1 0.541 0.541 {内置方法 Parse}
6186 0.387 0.000 pyexpat.c:566(StartElement)
6186 0.380 0.000 xmltodict.py:89(startElement)
112 0.317 0.003 base.py:654(save)
112 0.316 0.003 base.py:737(save_base)
14/113 0.290 0.003 manager.py:126(manager_method)
12487 0.278 0.000 collections.py:38(init)
113 0.235 0.002 query.py:910(_insert)
113 0.228 0.002 compiler.py:969(execute_sql)
6186 0.178 0.000 xmltodict.py:84(_attrs_to_dict)
1 0.170 0.170 query.py:356(bulk_create)
现在导入速度提高了五倍。注意分析结果的变化。过程中的数据库部分现在排在列表底部,而现在最耗时的部分是将 XML(GPX 文件)转换为字典。
查看输出,我们还可以看到另一个。
到目前为止,我们有了更高效的代码,我们不会改变处理 XML 的方式
转换已完成。相反,我们将继续测试和优化过程,而不是
其他类型的数据。
优化数据解析
记住我们在代码中创建了一个分支来导入 GPX 文件,因为 OGR/GDAL
无法导入这些文件中的嵌套数据。因此,我们应该预计在导入 shapefile 或 GML 文件时,我们将有不同的代码执行时间配置文件。让我们试试:
1. 现在我们将使用世界边界数据集测试代码。更改 if name == 'main':
== 'main': block of geodata_app.py:
if name == 'main':
profile = cProfile.Profile()
profile.enable()
import_initial_data("../data/world_borders_simple.shp",
'boundary')
profile.disable()
profile.print_stats(sort='cumulative')
2. 运行代码:
ValueError: A LinearRing must have at least 3 coordinate tuples Process finished with exit code 1
嗯,这不起作用。这里发生的事情是 Shapely 正在抱怨传递给它的几何形状。这是因为这段代码分支传递了一个 WKT 几何形状而不是坐标。
Django 可以接收 WKT 格式的几何形状,我们正在使用 Shapely 进行转换。
这可能是一个耗时的步骤,我们将消除它。在这个阶段,我们只是在用常识优化代码:步骤越少,代码运行越快。
1. 编辑 GeoCollection 管理器:
class GeoCollection(GeoManager):
"""这个类表示一组空间数据。”
def import_data(self, file_path, atype):
"""打开与 OGR 兼容的矢量文件并解析数据。
:param str file_path: 文件的完整路径。
"""
from models import Tag
features = open_vector_file(file_path)
tags = []
for feature in features:
geo_object = self.model(geom=feature['geom'],
atype=atype)
geo_object.save()
geoo_id = geo_object.id
for key, value in feature['properties'].iteritems():
if value and (isinstance(value, unicode)
or isinstance(value, str)): if len(value) <= 250:
tags.append(Tag(key=key, value=value,
geo_object_id=geoo_id))
Tag.objects.bulk_create(tags)
我们消除了 Shapely 的使用(你也可以从导入中删除它),并更改了从字典中检索几何形状的方式。
2. 现在转到 geo_functions.py 并编辑 read_ogr_features 函数:def read_ogr_features(layer):
"""将图层中的 OGR 特征转换为字典。
:param layer: OGR 图层。
"""
features = []
layer_defn = layer.GetLayerDefn()
layer.ResetReading()
type = ogr.GeometryTypeToName(layer.GetGeomType())
for item in layer:
attributes = {}
for index in range(layer_defn.GetFieldCount()):
field_defn = layer_defn.GetFieldDefn(index)
key = field_defn.GetName()
value = item.GetFieldAsString(index)
attributes[key] = value
feature = {
"geom": item.GetGeometryRef().ExportToWkt(),
"properties": attributes}
features.append(feature)
return features
作为一般化和性能之间的权衡,我们将特征字典从通用的 GeoJSON 格式更改为只包含两个键:具有 WKT 几何形状的 geom 和属性。
- 现在编辑 read_gpx_file 函数,使其符合新格式:def read_gpx_file(file_path):
"""读取包含地理藏宝点(geocaching points)的 GPX 文件。
:param str file_path: 文件的完整路径。
"""
with open(file_path) as gpx_file:
gpx_dict = xmltodict.parse(gpx_file.read())
output = []
for wpt in gpx_dict['gpx']['wpt']:
geometry = "POINT(" + wpt.pop('@lat') + " " + wpt.pop('@lon') + ")"
第四章:如果 geocache 不在字典中,则跳过此 wpt。
try:
geocache = wpt.pop('geocache')
except KeyError:
continue
attributes = {'status': geocache.pop('@status')}
合并字典。
attributes.update(wpt)
attributes.update(geocache)
构建一个 GeoJSON 特征并将其追加到列表中。
feature = {
"geom": geometry,
"properties": attributes}
output.append(feature)
return output
- 再次运行您的代码(如果您愿意,也可以再次测试导入点,您将获得几毫秒的改进)。查看结果:导入边界…
245 个特征。
完成!
90746 次函数调用(90228 次原始调用)在 5.164 秒内完成

导入 OpenStreetMap 的兴趣点
OpenStreetMap (OSM) 是一个协作制图项目,每个人都可以创建账户并参与地图制作。它就像维基百科,但社区制作的是地图而不是文章。
数据全部可供下载,一些地区的地图非常详细。
我们在这里想要获取的是 兴趣点 (POI)。这些点是代表餐厅、超市、银行等位置的点。
查看以下蒙特利尔圣劳伦斯大道的截图。那些小图标中的每一个都是一个 POI:
可以通过其 API 轻松获取 OSM 数据,该 API 被称为 Overpass API。它允许用户进行高级查询并过滤感兴趣的数据。
获取到的数据是针对 OSM 需要的 XML 格式。我们将使用 overpy,这是一个 Python 包,它将此数据转换为 Python 对象。
到目前为止,我必须承认,在我的职业生涯中,我深受 OSM 的启发
以及其数据格式。它简单而灵活,以至于 OSM 中的所有内容都由相同的模式表示。
OSM 由节点组成,有很多节点。实际上,到这个日期,它已有 3,037,479,553 个节点。

那就是了,超过三十亿的节点。节点可以是点,也可以是与其他节点相关联的部分,作为线或多边形表示的某个事物的组成部分。
每个节点都可以有任意数量的标签,由键/值对组成,就像我们的数据一样。查看从 POI 获得的信息:因此,将 OpenStreetMap POI 存储到我们的数据库中将非常直接。首先,让我们创建一个实用函数来下载感兴趣区域的点。
1. 前往 utils 文件夹,并创建一个名为 osm_data.py 的新 Python 文件。
2. 将以下代码添加到该文件中:
coding=utf-8
import overpy
def get_osm_poi(bbox):
"""从 OpenStreetMap 下载兴趣点。
:param bbox: 获取点的区域的边界框。
"""
api = overpy.Overpass()
result = api.query("""
""".format(**bbox))
print("找到 {} POI".format(len(result.nodes)))
return result
if name == "main":
bbox = {"xmin":-71.606, "ymin":46.714,
"xmax":-71.140, "ymax":48.982}
result = get_osm_poi(bbox)
print(result.nodes[0].tags)
这是一个简单的 overpy 包装器,用于查询给定区域内的所有具有 amenity 键的点。在 if name == 'main': 块中,我们进行简单的测试,获取一些点,并打印其中一个点的标签。
注意
您可以在本网站上获取有关 Overpass API 的更多信息:
wiki.openstreetmap.org/wiki/Overpass_API/Language_Guide.
3. 运行此文件中的代码。请记住按下 Alt + Shift + F10 来选择不同的文件,并在列表中选择 osm_data。你应该得到如下输出:找到 3523 个 POI
{'operator': 'Desjardins', 'amenity': 'bank', 'atm': 'yes', 'name':
'Caisse Populaire Desjardins'}
进程以退出代码 0 完成
如果您尚未安装 overpy,只需在代码中点击它,按 Alt + F10 并选择“安装包”
现在,让我们将此数据导入我们的数据库。打开您的 manage.py 文件。我们将为 GeoCollection 管理器创建一个新方法,它与 import_data 非常相似,但特定于 OSM 数据。
4. 编辑您的 manage.py 文件,并将此新方法添加到 GeoCollection 类中:
...
def import_osm_data(self, result):
"""导入 OpenStreetMap 的兴趣点。
:param str file_path: 文件的完整路径。
"""
from models import Tag
tags = []
for node in result.nodes:
geometry = "POINT(" + str(node.lat) + " " + \ str(node.lon) + ")"
geo_object = self.model(geom=geometry, atype="poi") geo_object.save()
geoo_id = geo_object.id
for key, value in node.tags.iteritems():
tags.append(Tag(key=key, value=value, geo_object_id=geoo_id))
Tag.objects.bulk_create(tags)
我们本可以重用 import_data 和 import_osm_data 两个函数共有的代码,但在这个章节中,我们强调速度,正如之前所述,有时使用特定的函数可以更容易地实现更好的执行时间。
在这种情况下,我们能够在创建标签时删除验证,使循环运行更快。
现在,让我们测试这个新方法:
5. 打开 geodata_app.py 文件,并在文件开头添加此导入:from utils.osm_data import get_osm_poi
6. 现在编辑 if name == 'main': 块:
if name == 'main':
bbox = {"xmin":-71.206, "ymin":47.714,
"xmax":-71.140, "ymax":48.982}
result = get_osm_poi(bbox)
GeoObject.objects.import_osm_data(result)
points = GeoObject.objects.filter(atype='poi')
print(len(points))
7. 最后,运行代码并查看是否得到以下类似输出(点的数量可能因你而异):
找到 14 个 POI
14
进程已结束,退出代码为 0
删除测试数据
在我们继续导入真实数据之前,让我们清理数据库中所有用于测试的数据。让我们在我们的应用程序中为此任务创建一个简单的函数:1. 在 geodata_app.py 中添加此函数:
def clean_database():
"""从数据库中删除所有记录。”
from django.db import connection
cursor = connection.cursor()
cursor.execute('DELETE FROM geodata_tag;')
cursor.execute('DELETE FROM geodata_geoobject;')
在这里,我们直接在数据库上调用 SQL 命令,以避免所有 Django 开销并获得更好的性能。
2. 现在,从 if name == 'main':块中调用它:
if name == 'main':
clean_database()
3. 运行代码;完成可能需要一段时间。
4. 保留它作为资源,以防你想进行其他测试或需要从头开始。
用真实数据填充数据库
现在是时候将真实数据放入我们的数据库中。我们将导入迄今为止使用的所有数据,以及额外的数据:
地理藏宝点(扩展版)
世界边界
加拿大区域边界
加拿大兴趣点
1. 前往你的 geodata_app.py 文件并编辑 if name == 'main':块:if name == 'main':
import_initial_data("../data/canada_div.gml", 'canada') import_initial_data("../data/world_borders_simple.shp", 'world') import_initial_data("../data/geocaching_big.gpx", 'geocaching') 这次我们为我们的数据设置了更具体的数据类型,以便更容易进行查询。
1. 现在,运行代码开始导入。最后,你应该得到以下输出:导入加拿大…
293 个要素。
完成!
导入世界…
245 个要素。
完成!
导入地理藏宝游戏数据…
1638 个要素。
完成!
进程已结束,退出代码为 0
现在是时候从 OpenStreetMap 获取兴趣点并将其添加到我们的数据库中。
2. 将此函数添加到你的 geodata_app.py:
def import_from_osm(district):
tags = Tag.objects.filter(value="Montreal")
borders = GeoObject.objects.get(atype='canada',
tags__key='CDNAME',
tags__value=district)
extent = borders.geom.extent
print("范围: {}".format(extent))
bbox = {"xmin": extent[0], "ymin": extent[1],
"xmax": extent[2], "ymax": extent[3]}
osm_poi = get_osm_poi(bbox)
GeoObject.objects.import_osm_data(osm_poi)
print("Done!")
此函数接受一个区域名称。从我们的数据库中获取它并使用其范围来查询 OSM API。
3. 修改 if name == 'main':块:
if name == 'main':
import_from_osm('Montréal')
4. 现在,运行代码。从 OSM 下载数据可能需要一些时间。完成后,你的输出应该类似于以下内容(要素的数量可能因你而异):
范围: (-73.9763757739999, 45.4021292300001, -73.476065978, 45.703747476)
找到 5430 个 POI
完成!
--- 内存错误 ----
在这一点上,您可能将首次接触代码优化的另一个问题:内存消耗。除非您有大量的 RAM,否则您将面临 Python 的 MemoryError。这意味着在解析从 OSM 获取的大量 POI 时,您的计算机已耗尽内存。
这是因为整个 OSM 的 XML 都被解析成了 Python 对象,然后又转换成了 Django 对象,并且它们都被同时存储在内存中。
这里的解决方案是逐个读取 XML 标签。如果是节点,就将其放入数据库,获取其标签,并释放内存。为此,我们将使用样本数据中可用的 XML 文件,因此我们不需要再次下载它。
1. 打开 managers.py 文件,并在文件开头添加此导入:import xml.etree.cElementTree as ET
2. 前往您的 GeoCollection 管理器并编辑 import_osm_data 方法:
...
...
def import_osm_data(self, input_file):
"""导入 OpenStreetMap 的兴趣点。
:param str input_file: 文件的完整路径。
"""
from models import Tag
tags = []
tags_counter = 0
nodes_counter = 0
xml_iter = ET.iterparse(input_file)
for event, elem in xml_iter:
if elem.tag == 'node':
lat, lon = elem.get('lat'), elem.get('lon')
geometry = "POINT(" + str(lat) + " " + str(lon) + ")"
geo_object = self.model(geom=geometry, atype="poi") geo_object.save()
geoo_id = geo_object.id
nodes_counter += 1
if nodes_counter % 10000 == 0:
print("{} Nodes…".format(nodes_counter))
print("创建标签...") Tag.objects.bulk_create(tags)
tags = []
for child_tag in elem:
key = child_tag.get('k')
value = child_tag.get('v')
if len(value) <= 250:
tags.append(Tag(key=key,
value=value,
geo_object_id=geoo_id))
tags_counter += 1
elem.clear()
print("创建标签...")
Tag.objects.bulk_create(tags)
print("导入了{}个节点和{}个标签。".format(
nodes_counter, tags_counter))
print("完成!")
ElementTree是一个用于 XML 解析的 Python 模块;cElementTree 具有相同的功能,但由 C 实现。使用 cElementTree 的限制仅在于当 C 库加载不可用时,这里的情况并非如此。
注意,优化标签创建的解决方案是将标签累积到一个列表中,每 10,000 个节点批量创建标签,然后清空列表。
3. 编辑 geodata_app.py 文件中的 if name == 'main': 块以测试代码:
if name == 'main':
GeoObject.objects.import_osm_data("../data/osm.xml") 4. 现在运行它。在等待的过程中,您可以打开 Windows 任务管理器,或者在 Ubuntu 上打开系统监视器,查看您的计算机资源的使用情况,并/或在控制台输出中查看进度:
10000 Nodes…
创建标签…
20000 Nodes…
创建标签…
30000 Nodes…
创建标签…
40000 Nodes…
创建标签…
50000 Nodes…
...
导入了 269300 个节点和 1272599 个标签。
完成!
如果你正在监控计算机资源,你应该已经看到内存消耗在某个值周围波动。由于内存没有随着越来越多的节点导入而持续增加,因此我们可以导入任何给定数量的点,无论文件大小如何,因为代码是稳定的,没有内存泄漏。
在我的计算机上,Python 在程序执行期间消耗了大约 100 Mb 的内存。处理器核心保持在 5%的负载(Python 和 PostgreSQL),硬盘在 100%占用中,用于数据库写入。
可以调整数据库以获得更好的性能,但这超出了本书的范围。
记住,如果你想进行更多测试,你可以始终使用我们之前创建的函数清理数据库。只是记得在我们继续之前重新导入所有数据。
搜索数据和交叉
信息
现在我们已经用一些数据填充了数据库,是时候从中获取一些信息了;让我们探索所有这些 POI 包含的信息类型。我们知道我们下载的点至少包含便利设施或商店键中的一个。
OSM 将便利设施描述为任何类型的社区设施。作为一个练习,让我们看看我们从这些点中获得的一些便利设施类型列表:
- 编辑 geodata_app.py 文件的 if name == 'main':块:if name == 'main':
amenity_values = Tag.objects.filter(
key='amenity').distinct('value').values_list('value')
for item in amenity_values:
打印(item[0])
在这里,我们使用 Tag 模型,访问其管理器(objects),然后过滤出键='amenity'的标签。然后我们只分离出不同的值(从查询中排除重复的值)。最后一部分——values_list('value')——告诉 Django 我们不想创建 Tag 模型,我们只想得到一个值列表。
- 运行代码,看看大量便利设施类型列表:自动柜员机,燃料
自动柜员机;电话
听力学家
会议室
汽车修理店
车友会
汽车俱乐部
婴儿护理室
保释债券
面包店
洗球器
芭蕾舞
音乐厅
银行
银行建筑
宴会厅
酒吧
酒吧/食物
理发师
理发店
洗手间
烧烤
美容
美容服务
钟楼
长椅
赌博
自行车越野

自行车停车场
自行车停车场;自行车租赁
自行车停车场;银行
自行车租赁
...
你也可以发现一些对 OSM 标签的误用,因为人们错误地将街道名称、商业名称等放在便利设施类型上。
注意
你可以查看 OpenStreetMap 维基百科中的常见便利设施类型列表:
wiki.openstreetmap.org/wiki/Key:amenity.
使用边界过滤
现在,让我们尝试只获取位于蒙特利尔的便利设施。程序与之前类似。我们将使用一个已知的谓词通过几何关系过滤对象,但这次搜索由数据库和空间索引提供支持,使它们变得非常快。
注意
请参阅第四章的几何关系部分,了解改进应用搜索能力的谓词列表。
if name == 'main':
获取蒙特利尔对象。
montreal = GeoObject.objects.get(atype='canada',
tags__key='CDNAME',
tags__value='Montréal')
过滤位于蒙特利尔的 POI 标签。
amenities = Tag.objects.filter(
key='amenity', geo_object__geom__within=montreal.geom)
仅过滤不同的值。
amenities = amenities.distinct('value')
获取'values'列表
amenity_values = amenities.values_list('value')
for item in amenity_values:
print(item[0])
在这里,我将每个部分分开成不同的语句,以方便理解。
无论您是将所有内容组合在一起还是保持分离,Django 查询集都是懒加载的(有点像我们在第七章 从栅格数据中提取信息中做的),并且它们只在需要值时才被评估。这意味着 Django 在我们开始遍历值(for item in amenity_values)时只会击中数据库一次。
- 运行代码。你应该会得到一个更简短的设施类型列表:艺术中心
自动柜员机
听力学家
面包店
银行
酒吧
烧烤
长椅
自行车停车场
自行车租赁
广告牌
兑换处
公交站
咖啡馆
汽车租赁
汽车修理
共享汽车
洗车
儿童看护
电影院
市政厅
诊所
时钟
学院
...
现在,让我们找出在蒙特利尔可以找到多少家电影院(电影院):2. 编辑 if name == 'main':块:
if name == 'main':
montreal = GeoObject.objects.get(atype='canada',
tags__key='CDNAME',
tags__value='Montréal')
cinemas = GeoObject.objects.filter(atype='poi',
geom__within=montreal.geom,
tags__key='amenity',
tags__value='cinema')
print("{} cinemas.".format(cinemas.count()))
注意
注意,我们正在使用 count 方法而不是 Python 的 len 函数。这使得计数在数据库上发生,并且只返回输出值。
这比先获取所有对象然后用 Python 计数要快得多。
- 现在运行它并检查输出:
16 家电影院。
处理完成,退出代码为 0
摘要
在本章中,我们探讨了关于代码效率的入门概念以及如何衡量它。有了正确的工具,我们优化了代码,使其运行更快。
我们不是将数据存储到 Python 对象中,而是转向 SQL 数据库。因此,我们通过最先进的过滤功能增强了我们的应用程序,并以高效的方式获取信息。
后来,我们遇到了大量无法用普通计算机导入的数据。我们再次优化了代码,使其内存高效且稳定,使我们能够导入这些数据。最后,我们查询了数据,测试了新功能的功能。
在下一章中,我们将面临类似的速度和内存问题,但这次是图像(栅格)数据。这要求我们开发新的和创造性的解决方案。
第九章. 处理大图像
处理卫星图像(或其他遥感数据)是两个原因的计算挑战:通常,图像很大(许多兆字节或吉字节),并且需要结合许多图像来生成所需的信息。
打开和处理大量大图像可能会消耗大量计算机内存。这种状况为用户在耗尽内存之前能做什么设定了严格的限制。
在本章中,我们将关注如何进行可持续的图像处理,以及如何在保持低内存消耗的同时,使用高效代码打开和计算大量大图像。
以下主题将涵盖:
卫星图像和 Landsat 8 数据简介
如何选择和下载 Landsat 8 数据
当我们处理图像时,计算机内存会发生什么变化?
如何分块读取图像
Python 迭代器和生成器是什么?
如何遍历图像
如何使用新技术创建色彩组合

处理卫星图像
卫星图像是一种遥感数据形式。它们由卫星收集的信息组成,并以图像文件的形式提供给用户。就像我们之前工作的数字高程模型一样,这些图像由像素组成,每个像素代表特定地理范围内给定属性的值。
这些图像可以用真实颜色来可视化地球上的特征,或者可以用肉眼看不见的光谱部分来识别各种特征。
为了跟随示例,我们将使用来自 Landsat 8 卫星的照片。它们在互联网上免费提供。让我们看看这个卫星的一些特性。
Landsat 8 携带两种仪器:操作陆地成像仪(OLI)和热红外传感器(TIRS)。
这些传感器可以收集总共 10 个不同波段的数据,以 4096 个可能级别(12 位)的分辨率进行处理。数据被编码成 16 位 TIFF 图像,并缩放到 55000。
可能的值。
波长
分辨率
波段
常见用途
(微米)
(米)
波段 1—海岸
浅海海岸水研究和估算
0.43 - 0.45
30
雾霾
大气中雾霾的浓度
波段 2—蓝色
0.45 - 0.51
30
可见光蓝色通道,区分土壤和植被
波段 3—绿色
0.53 - 0.59
30
可见光绿色通道
波段 4—红色
0.64 - 0.67
30
可见光红色通道
波段 5—近红外 0.85 - 0.88
30
生物量估算
(近红外)
波段 6—短波红外 1
1.57 - 1.65
30
土壤湿度
波段 7—短波红外 2
2.11 - 2.29
30
土壤湿度
波段 8—全色 0.50 - 0.68
15
更高的分辨率
波段 9—卷云
1.36 - 1.38
30
检测卷云污染
波段 10—热
10.60 - 11.19
30
热成像和估算土壤湿度
红外线 (TIRS) 1
波段 11—热
11.50 - 12.51
30
热成像和估算土壤湿度
红外线 (TIRS) 2

获取 Landsat 8 图像
Landsat 8 图像在互联网上免费提供,有一些很好的工具可以查找和下载这些图像。对于本书,我们将使用美国地质调查局(USGS)的 EarthExplorer。这是一个包含大量资源以获取地理数据的网络应用程序。
为了跟随书中的示例,我们将下载与上一章获得兴趣点相同的蒙特利尔(魁北克,加拿大)区域的数据。这些数据包含在书的样本数据中,如果您愿意可以跳过这些步骤。
首先,我们将打开网站并按照以下方式选择我们的兴趣区域:1. 访问earthexplorer.usgs.gov/网站。您将看到一个地图,顶部有一些选项,以及左侧带有搜索工具的面板:2. 在右上角,您将看到一个登录/注册按钮。如果您没有账户,点击注册创建一个新的账户。否则,登录系统。
- 下一步是搜索感兴趣的位置。您可以通过在框中输入蒙特利尔并点击显示来搜索。将出现一个包含搜索结果的列表。
点击列表中的蒙特利尔。将出现一个标记,并将坐标设置好。
-
点击数据集按钮以显示此坐标的可用数据。
-
在下一屏,展开Landsat 档案项,选择L8 OLI/TIRS,然后点击附加标准按钮。


-
现在,让我们确保我们得到云层较少的图像。使用滚动条找到云层覆盖项并选择小于 10%。现在,点击结果以查看找到的内容。
-
将打开一个新标签页显示结果。请注意,每个项目都包含一个带有图标集的小工具栏。点击某些图像的脚图标以查看其在地图上的范围:
-
对于我们的示例,我们只需要一个数据集:14 路径,28 行的数据集。找到数据

对于这组行和列(您可以使用任何日期的图像;这取决于您)然后点击迷你工具栏上的下载选项按钮(它是指向硬盘的绿色箭头图标)。
- 将弹出一个包含下载选项的窗口。点击下载级别 1
GeoTIFF 数据产品。
注意
美国地质调查局有一个可以管理和恢复大型下载的应用程序。更多信息请查看lta.cr.usgs.gov/BulkDownloadApplication。
- 下载完成后,在您的数据文件夹中创建一个新的文件夹,并将其命名为 landsat。解压缩此文件夹中的所有图像。
每个包包含 12 个.tif 图像和一个包含元数据的文本文件。每个图像名称由行、列、日期和图像波段组成。请注意,波段 8
图像(B8)比其他图像大得多。这是因为它具有更好的分辨率。
BQA 是一个质量评估波段。它包含有关图像中每个像素质量的信息。我们稍后会看到更多关于这个波段的内容。
内存和图片
首先,我们将检查打开图片如何影响随机访问内存(RAM)的使用。在我们的第一个例子中,我们将尝试使用之前相同的技术打开 Landsat 数据的波段 8:
- 通过复制第八章来为第九章准备工作环境。
在你的 geopy 项目中创建一个新的文件夹。将复制的文件夹命名为 Chapter9\。
-
在 Chapter9 文件夹中,打开 experiments 文件夹并删除其中的所有文件。
-
在 experiments 文件夹中,创建一个新的 Python 文件并命名为 images.py。打开它进行编辑。
-
现在将以下代码输入此文件:
coding=utf-8
import cv2 as cv
def open_image(img_path):
image = cv.imread(img_path)
print(type(image))
raw_input("Press any key.")
if name == 'main':
image_path = "../../data/landsat/LC80140282015270LGN00_B8.TIF"
open_image(image_path)
-
运行代码。按Alt + Shift + F10并选择列表中的图片。
-
根据你的计算机内存和 OpenCV 版本,你可能成功。
否则,你会看到这个漂亮的异常:
OpenCV 错误:内存不足(在 cv::OutOfMemoryError 中失败分配 723585188 字节),文件 ......\opencv-2.4.11\modules\core\src\alloc.cpp,行 52
追踪回溯(最后最近调用):
文件 "Chapter9/experiments/images.py",第 14 行,在
文件 "experiments/images.py",第 6 行,在 open_image 中:image = cv.imread(img_path)
cv2.error: ......\opencv-2.4.11\modules\core\src\alloc.cpp:52: error: (-4) Failed to allocate 723585188 bytes in function
cv::OutOfMemoryError
进程以退出代码 1 结束
这是因为我们正在使用 Python 解释器的 32 位版本(即 x86),程序无法分配足够的内存一次性打开整个图片。
- 让我们尝试一个文件大小更小的波段。更改文件名以匹配任何图片的波段 1。它可能是 LC80140282015270LGN00_B1.TIF。

- 再次运行代码。你应该会看到一个提示让你按任意键:
<type 'numpy.ndarray'>
按任意键。
这是有意为之,以便在图片仍在内存中时停止程序执行。
-
现在,如果你使用的是 Windows,请按Ctrl + Alt + Del并打开任务管理器。如果你使用的是 Ubuntu Linux,请打开系统监视器。
-
查找 Python 进程并查看它使用了多少内存。你应该看到如下内容:
这没关系。图片已打开,并且没有消耗太多内存。
-
在控制台中按任意键以完成程序执行。
-
现在,让我们模拟打开多张图片并看看会发生什么。更改你的 open_image 函数:
def open_image(img_path):
image = cv.imread(img_path)
image2 = cv.imread(img_path)
image3 = cv.imread(img_path)
image4 = cv.imread(img_path)
image5 = cv.imread(img_path)
raw_input("Press any key.")
-
再次运行代码并检查 Python 使用的内存。对我来说,是 872 MB。
-
在控制台中按任意键退出程序并释放内存。
-
对于我们的最后一次测试,再次打开图像以查看会发生什么:def open_image(img_path):
image = cv.imread(img_path)
image2 = cv.imread(img_path)
image3 = cv.imread(img_path)
image4 = cv.imread(img_path)
image5 = cv.imread(img_path)
image6 = cv.imread(img_path)
raw_input("Press any key.")
- 运行代码并查看结果:
cv2.error: D:\Build\OpenCV\OpenCV-
2.4.11\modules//python//src2//cv2.cpp:201: 错误:(-2) 在函数中无法创建 typenum=2,ndims=3 的 numpy 数组
NumpyAllocator::allocate
Process finished with exit code 1
再次,程序未能分配足够的内存来打开图像。
这些实验的目的是为了展示,在处理图像时,有很大可能会遇到内存问题。对于波段 8,甚至无法开始处理,因为我们无法打开它。
对于波段 1,我们模拟了一个常见的情况,我们想要执行涉及许多图像的计算,并且这个计算有子步骤。内存消耗会不断增加,直到程序崩溃。
Python 允许使用的最大内存量受操作系统和 Python 版本(64 位或 32 位)的限制。可能,如果你正在运行 64 位版本的 Python,或者使用 Linux,你在这几个示例中不会遇到任何错误。
无论这个程序是否能运行,这些示例展示的代码的成功与图像大小有关。即使在一个拥有 32GB RAM 的 Linux 机器上运行 64 位 Python,如果图像太大且处理过程复杂,程序也可能耗尽内存。记住,一些卫星图像可能相当大。
分块处理图像
我们将修改代码,以便我们可以打开任何大小的图像。原理与上一章中应用的方法相同:为了读取和导入任意数量的点,我们让程序在读取、导入和释放内存后,对每小批次的点进行操作。
我们将不再读取点,而是从图像中读取一小部分,进行一些计算,将输出写入磁盘,并在重复下一部分之前释放内存。
使用 GDAL 打开图像
读取给定图像选定区域的过程并不容易。许多因素都相关,例如数据在图像中的编码方式、数据类型、如何读取数据等等。幸运的是,GDAL 配备了强大的函数和方法,可以抽象出大部分底层过程。让我们来实验一下:1. 在 images.py 文件中,在文件开头导入 GDAL:import gdal
- 现在,创建一个新的函数来使用 GDAL 打开 Landsat 波段 8:def open_image_gdal(img_path):
dataset = gdal.Open(img_path)
cols = dataset.RasterXSize
rows = dataset.RasterYSize
print "Image dimensions: {} x {}px".format(cols, rows) raw_input("Press any key.")
3. Change the if name == 'main': block to use the new function: if name == 'main':
image_path = "../../data/landsat/LC80140282015270LGN00_B8.TIF"
open_image_gdal(image_path)
4. Run your code and check the output:
Image dimensions: 15401 x 15661px
Press any key.
We simply opened the image and printed its dimensions. You should have noticed that the code ran incredibly fast and with no errors. If you wish, you can check how much memory the Python process is using (using the Task Manager or the system monitor).
What happened this time is that the data wasn’t read when the file was opened. GDAL
only got the information about the image, but the actual data wasn’t touched.
Let’s try reading a few pixels from this image:
1. Press any key to exit the program.
2. Edit the function:
def open_image_gdal(img_path):
dataset = gdal.Open(img_path)
cols = dataset.RasterXSize
rows = dataset.RasterYSize
print "Image dimensions: {} x {}px".format(cols, rows) middle_col = int(cols / 2)
middle_row = int(rows / 2)
array = dataset.ReadAsArray(xoff=middle_col - 50,
yoff=middle_row - 50,
xsize=100, ysize=100)
print(array)
print(array.shape)
3. Run the code again and check the output: Image dimensions: 15401 x 15661px
[[8826 8821 8846…, 8001 7965 7806]
[8842 8838 8853…, 7982 7931 7676]
[8844 8860 8849…, 8050 7958 7693]
...,
[7530 7451 7531…, 7471 7457 7494]
[7605 7620 7555…, 7533 7519 7610]
[7542 7542 7499…, 7620 7947 7728]]
(100, 100)
Process finished with exit code 0
We just read a chunk of 100 x 100 pixels from the centre of the image. Again, the code ran fast and little memory was consumed.
Now let’s try something fancier. Read a region from the image and save it on the disk, so we can visualize it.
4. First, delete all the files from the Chapter9/output folder. We will save our image here.
5. Add the adjust_values function and edit the code of the open_image_gdal function: def adjust_values(array, img_range=None):
"""Projects a range of values into a grayscale image.
:param array: A Numpy array containing the image data.
:param img_range: specified range of values or None to use
the range of the image (minimum and maximum).
"""
if img_range:
min = img_range[0]
max = img_range[1]
else:
min = array.min()
max = array.max()
interval = max - min
factor = 256.0 / interval
output = array * factor
return output
def open_image_gdal(img_path):
dataset = gdal.Open(img_path)
cols = dataset.RasterXSize
rows = dataset.RasterYSize
print "Image dimensions: {} x {}px".format(cols, rows) middle_col = int(cols / 2)
middle_row = int(rows / 2)
array = dataset.ReadAsArray(xoff=middle_col - 50,
yoff=middle_row - 50,
xsize=1000, ysize=1000)
print(array.shape)
greyscale_img = adjust_values(array)
cv.imwrite('../output/landsat_chunk.jpg', greyscale_img)

adjust_values 函数与之前我们用来调整高程数据灰度值以便可视化的函数相同。
我们使用 OpenCV 来写入 JPG 图像有两个原因:默认情况下,GDAL
在 Windows 上无法写入 JPG 文件,而且在这个简单的情况下,OpenCV 更容易使用。
6. 运行代码并在输出文件夹中打开图像。如果你和我使用的是相同的 Landsat 数据,你应该看到魁北克农村地区的这幅美丽图像:

遍历整个图像
我们看到我们可以读取图像的特定部分。利用这个概念,我们可以一次处理图像的一部分。通过这样做,我们可以进行涉及多个波段的计算。我们只需要读取每个波段相同区域,获取结果,写入它们,释放内存,然后移动到下一部分。
在 Python 中遍历某个对象的最明显方式是使用 for 循环。我们可以遍历列表的元素、字符串中的字符、字典的键、矢量图层上的要素等等。
此前的图像来自 http://nvie.com/posts/iterators-vs-generators/
你可能已经听说过可迭代对象、迭代器和生成器的概念。可迭代对象,如列表,在用于 for 循环时成为迭代器。但我们不想创建图像块列表,因为为了做到这一点,我们需要预先读取整个图像来生成列表。这就是迭代器的特殊特性凸显出来的时候:它们是惰性的。
迭代器不过是一个具有特定魔法方法的类。在每次循环中,这个类的 next() 方法被调用,并返回一个新的值。Python 有创建迭代器的便捷工具,这正是我们要看到生成器的时候。让我们写一些代码:
1. 在你的 images.py 文件中,添加一个新的函数:
def create_image_generator(dataset):
cols = dataset.RasterXSize
rows = dataset.RasterYSize
for row_index in xrange(0, rows):
yield dataset.ReadAsArray(xoff=0, yoff=row_index,
xsize=cols, ysize=1)
2. 现在编辑 if name == 'main': 块:if name == 'main':
base_path = "../../data/landsat"
img_name = "LC80140282015270LGN00_B8.TIF"
img_path = os.path.join(base_path, img_name)
dataset = gdal.Open(img_path)
img_generator = create_image_generator(dataset)
print(img_generator)
print(type(img_generator))
3. 运行代码并检查输出:
<generator object create_image_generator at 0x0791D968>
<type 'generator'>
Process finished with exit code 0
由于 for 循环中的 yield 关键字,我们的 create_image_generator 函数具有特殊的行为。当我们遍历由该函数创建的生成器对象时,yield 语句会暂停函数执行并在每次循环中返回一个值。在我们的例子中,生成器/迭代器将一次返回一行图像。
4. 只是为了检查它是否工作,在 if name == 'main': 块中尝试这个:if name == 'main':
base_path = "../../data/landsat"
img_name = "LC80140282015270LGN00_B8.TIF"
img_path = os.path.join(base_path, img_name)
dataset = gdal.Open(img_path)
img_generator = create_image_generator(dataset)
print(img_generator)
print(type(img_generator))
for row in img_generator:
print(row)
- 运行代码并查看输出:
...
[[0 0 0…, 0 0 0]]
[[0 0 0…, 0 0 0]]
[[0 0 0…, 0 0 0]]
[[0 0 0…, 0 0 0]]
[[0 0 0…, 0 0 0]]
[[0 0 0…, 0 0 0]]
Process finished with exit code 0
你看到的是 Python 打印了很多数组,每个数组都包含一行数据。你看到零是因为图像的边缘是黑色,而所有其他值都是
被 NumPy 抑制以适应控制台。让我们进行一些测试来探索迭代器的特性:
- 现在,尝试这个概念测试,只是为了检查迭代器的另一个特性:if name == 'main':
base_path = "../../data/landsat"
base_path = "C:/Users/Pablo/Desktop/landsat"
img_name = "LC80140282015270LGN00_B8.TIF"
img_path = os.path.join(base_path, img_name)
dataset = gdal.Open(img_path)
img_generator = create_image_generator(dataset)
print(img_generator[4])
- 运行代码,将引发错误:
Traceback (most recent call last):
文件 "Chapter9/experiments/images.py",第 98 行,在
TypeError: 'generator' object has no attribute 'getitem'
记住迭代器是惰性的,它们的行为不像序列(例如列表)。元素是逐个计算的,我们无法直接获取第 5 个元素。
- 现在,为了检查它是否真的起作用,让我们逐行复制图像。创建这个新函数:
def copy_image(src_image, dst_image):
try:
os.remove(dst_image)
except OSError:
pass
src_dataset = gdal.Open(src_image)
cols = src_dataset.RasterXSize
rows = src_dataset.RasterYSize
driver = gdal.GetDriverByName('GTiff')
new_dataset = driver.Create(dst_image, cols, rows,
eType=gdal.GDT_UInt16)
gdal_array.CopyDatasetInfo(src_dataset, new_dataset)
band = new_dataset.GetRasterBand(1)
for index, img_row in enumerate(
create_image_generator(src_dataset)):
band.WriteArray(xoff=0, yoff=index, array=img_row)
为了复制图像,我们使用 GDAL 的 GTiff 驱动器创建了一个新的数据集。新的数据集具有相同的行数、列数和数据类型(无符号 16 位整数)。
为了确保复制具有与源相同的投影信息,我们使用了
函数 gdal_array.CopyDatasetInfo,从而节省了我们大量的代码。
最后,使用我们的生成器,我们逐行读取并将其写入输出波段。
- 编辑 if name == 'main' 块并运行以下代码以测试它:if name == 'main':
base_path = "../../data/landsat"
img_name = "LC80140282015270LGN00_B8.TIF"
img_path = os.path.join(base_path, img_name)
img_copy = "../output/B8_copy.TIF"
copy_image(img_path, img_copy)
打开两张图像(原始图像和复制图像),只是为了检查它们是否看起来相同。
创建图像组合
现在我们已经了解了通过迭代图像的基本知识,这允许我们处理多个波段而不会耗尽内存,让我们产生一些更复杂的结果。
真色组合
由于我们有 Landsat 的红、绿、蓝波段,我们可以创建一个具有真色的图像。这意味着图像的颜色与我们直接观察场景时相似(例如,草地是绿色的,土壤是棕色的)。为此,我们将进一步探索 Python 的迭代器。
Landsat 8 RGB 波段分别是 4、3 和 2 波段。遵循我们想要自动化任务和流程的概念,我们不会为每个波段重复命令。我们将用以下方式编程 Python:
- 在文件开头编辑导入,使其如下所示:import os
import cv2 as cv
import itertools
from osgeo import gdal, gdal_array
import numpy as np
- 现在添加这个新函数。它将为我们准备波段路径:def compose_band_path(base_path, base_name, band_number):
return os.path.join(
base_path, base_name) + str(band_number) + ".TIF"
- 要检查此函数和导入的 itertools 的目的,请使用以下代码编辑 if name == 'main':块:
if name == 'main':
base_path = "../../data/landsat"
base_name = 'LC80140282015270LGN00_B'
bands_numbers = [4, 3, 2]
bands = itertools.imap(
compose_band_path,
itertools.repeat(base_path),
itertools.repeat(base_name),
bands_numbers)
print(bands)
for item in bands:
print(item)
- 现在运行代码并检查结果:
<itertools.imap object at 0x02DE9510>
../../data/landsat/LC80140282015270LGN00_B4.TIF
../../data/landsat/LC80140282015270LGN00_B3.TIF
../../data/landsat/LC80140282015270LGN00_B2.TIF
Process finished with exit code 0
组合波段路径只是将基本路径、波段名称和波段编号连接起来,以便输出带有路径的波段文件名。
而不是在 for 循环中调用函数并将结果追加到列表中,我们使用了 itertools.imap 函数。这个函数将另一个函数作为第一个参数,并将任何可迭代对象作为其他参数。它创建一个迭代器,将在每次迭代中调用该函数并传递参数。itertools.repeat 函数负责在迭代时重复给定值无限次。
- 现在,我们将编写一个将波段组合成 RGB 图像的函数。将此函数添加到您的文件中:
def create_color_composition(bands, dst_image):
try:
os.remove(dst_image)
except OSError:
pass
Part1
datasets = map(gdal.Open, bands)
img_iterators = map(create_image_generator, datasets)
cols = datasets[0].RasterXSize
rows = datasets[0].RasterYSize
Part2
driver = gdal.GetDriverByName('GTiff')
new_dataset = driver.Create(dst_image, cols, rows,
eType=gdal.GDT_Byte,
bands=3,
options=["PHOTOMETRIC=RGB"])
gdal_array.CopyDatasetInfo(datasets[0], new_dataset)
Part3
rgb_bands = map(new_dataset.GetRasterBand, [1, 2, 3])
for index, bands_rows in enumerate(
itertools.izip(*img_iterators)):
for band, row in zip(rgb_bands, bands_rows):
row = adjust_values(row, [0, 30000])
band.WriteArray(xoff=0, yoff=index, array=row)
在第一部分,Python 的内置 map 函数与 itertools.imap 类似,但它不是创建一个迭代器,而是创建一个包含结果的列表。这意味着所有项目都已计算并可用。首先,我们通过在所有波段上调用 gdal.Open 来创建 GDAL 数据集列表。然后,使用 map 函数创建一个图像迭代器列表,每个波段一个。
在第二部分,我们创建输出数据库就像之前做的那样。但这次,我们告诉驱动程序创建一个包含三个波段的数据集,每个波段的数据类型为字节(256
possible values)。我们还告诉它是一个 RGB 照片,在选项中。
在第三部分,我们再次使用 map 函数来获取数据集中波段的引用。在第一个 for 循环中,每次迭代都会得到一个索引,即行号,以及包含每个波段行的元组。
在嵌套的 for 循环中,每次迭代都会获取输出图像的一个波段和输入波段的一行。然后,使用我们的 adjust_values 函数将行的值从 16 位转换为 8 位(字节)。为了调整值,我们传递了一个魔法

number 以获得更亮的图像。最后,将行写入输出波段。
- 最后,让我们测试代码。编辑你的 if name == 'main': 块:if name == 'main':
base_path = "../../data/landsat/"
base_name = 'LC80140282015270LGN00_B'
bands_numbers = [4, 3, 2]
bands = itertools.imap(
compose_band_path,
itertools.repeat(base_path),
itertools.repeat(base_name),
bands_numbers)
dst_image = "../output/color_composition.tif"
create_color_composition(bands, dst_image)
- 现在运行它。完成后,在输出文件夹中打开图像(color_composition.tif)。你应该看到这张美丽的彩色图像:
你可以调整传递给 adjust_values 函数的数字。尝试更改下限和上限;你将得到不同亮度的不同变体。
处理特定区域
现在,让我们修改代码以自动裁剪图像,这样我们可以更好地查看蒙特利尔地区周围的细节。这就像我们之前做的那样。但是,我们不会在处理图像后裁剪图像,而是只处理感兴趣的区域,使代码更加高效。
- 编辑 create_image_generator 函数:
def create_image_generator(dataset, crop_region=None):
if not crop_region:
cols = dataset.RasterXSize
rows = dataset.RasterYSize
xoff = 0
yoff = 0
else:
xoff = crop_region[0]
yoff = crop_region[1]
cols = crop_region[2]
rows = crop_region[3]
for row_index in xrange(yoff, yoff + rows):
yield dataset.ReadAsArray(xoff=xoff, yoff=row_index,
xsize=cols, ysize=1)
现在,该函数接收一个可选的 crop_region 参数,如果传递了该参数,则只产生感兴趣区域的行。如果没有传递,则产生整个图像的行。
- 将 create_color_composition 类更改为处理裁剪数据:def create_color_composition(bands, dst_image, crop_region=None): try:
os.remove(dst_image)
except OSError:
pass
datasets = map(gdal.Open, bands)
img_iterators = list(itertools.imap(
create_image_generator, datasets,
itertools.repeat(crop_region)))
if not crop_region:
cols = datasets[0].RasterXSize
rows = datasets[0].RasterYSize
else:
cols = crop_region[2]
rows = crop_region[3]
driver = gdal.GetDriverByName('GTiff')
new_dataset = driver.Create(dst_image, cols, rows,
eType=gdal.GDT_Byte,
bands=3,
options=["PHOTOMETRIC=RGB"])
gdal_array.CopyDatasetInfo(datasets[0], new_dataset)
rgb_bands = map(new_dataset.GetRasterBand, [1, 2, 3])
for index, bands_rows in enumerate(
itertools.izip(*img_iterators)):

for band, row in zip(rgb_bands, bands_rows):
row = adjust_values(row, [1000, 30000])
band.WriteArray(xoff=0, yoff=index, array=row)
注意,当创建 img_iterators 时,我们用 itertools.imap 替换了 map 函数,以便能够使用 itertools.repeat 函数。由于我们需要 img_iterators 成为一个迭代器的列表,所以我们使用了 list 函数。
- 最后,编辑 if name == 'main':块以传递我们的感兴趣区域:if name == 'main':
base_path = "../../data/landsat/"
base_name = 'LC80140282015270LGN00_B'
bands_numbers = [4, 3, 2]
bands = itertools.imap(
compose_band_path,
itertools.repeat(base_path),
itertools.repeat(base_name),
bands_numbers)
dst_image = "../output/color_composition.tif"
create_color_composition(bands, dst_image,
(1385, 5145, 1985, 1195))
运行代码。你现在应该拥有这张漂亮的蒙特利尔图像:
假彩色组合
颜色组合是信息可视化的一项强大工具,我们甚至可以用它来看到人类肉眼难以察觉的事物。
Landsat 8 和其他卫星提供的数据是在光谱范围内反射或吸收特定对象更多或更少的范围。例如,茂密的植被反射大量的近红外辐射,因此如果我们正在寻找植被覆盖率或植物生长的信息,我们应该考虑这个波段。
除了对不同波段的计算分析外,我们还可以通过替换红色、蓝色和绿色成分来可视化它们。让我们尝试以下操作:1. 只需编辑 if name == 'main':块,以便我们使用近红外(波段 5)作为 RGB 图像的绿色成分:
if name == 'main':
base_path = "../../data/landsat/"
base_name = 'LC80140282015270LGN00_B'
bands_numbers = [4, 5, 2]
bands = itertools.imap(
compose_band_path,
itertools.repeat(base_path),
itertools.repeat(base_name),
bands_numbers)
dst_image = "../output/color_composition.tif"
create_color_composition(bands, dst_image,
(1385, 5145, 1985, 1195))
- 运行代码并查看输出图像:


- 您可以有其他许多组合。只需更改 band_numbers 变量以实现不同的结果。尝试将其更改为[6, 5, 2]。运行代码并查看农田如何从其他特征中脱颖而出。
注意
您可以通过点击以下链接查看更多有趣的波段组合:
landsat.gsfc.nasa.gov/?page_id=5377
blogs.esri.com/esri/arcgis/2013/07/24/band-combinations-for-landsat-8/
总结
正如我们在兴趣点所做的那样,我们通过将负载分割成片段来管理过度的计算资源消耗问题。具体来说,我们不是读取和处理整个图像,而是创建了 Python 迭代器,允许我们逐行遍历这些图像,而不触及计算机的内存限制。
使用这种技术,我们能够一次处理三个 Landsat 8 波段,以生成对数据可视化有价值的彩色图像。
在这一点上,我们能够将我们的处理任务分割成可以独立处理的片段。我们可以用向量、数据库访问以及现在用图像来做这件事。
通过这种方式,我们为下一章完全铺平了道路,在下一章中,我们将将这些部分发送给不同的处理器核心同时计算,从而执行所谓的并行处理。
第十章. 并行处理
在本章中,我们将进一步优化代码;我们将尝试使用多个处理器核心进行计算的可能性。
使用上一章的卫星图像,我们将使用 Python 的 multiprocessing 库来分配任务并使它们并行运行。作为一个例子,我们将尝试不同的技术来从 Landsat 8 生成真彩色合成图像
数据,具有更好的分辨率和更高的细节水平。
为了实现我们的目标,我们将通过以下主题:
多进程是如何工作的
如何遍历二维图像块
图像缩放和重采样
图像操作中的并行处理
图像拉伸增强
多进程基础
我们使用的 Python 实现,CPython,有一个称为全局 解释器锁(GIL)的机制。GIL 的目的是使 CPython 线程安全;它通过防止代码一次由多个线程执行来实现。
由于这种限制,Python 中的多进程通过复制正在运行的程序(例如,复制程序的状态)并将其发送到另一个计算机核心来实现。因此,新进程会带来一些开销。
让我们尝试一段简单的代码:
-
首先,将你的 geopy 项目中的上一章文件夹复制一份,并将其重命名为 Chapter10\。
-
清理 Chapter10/output 文件夹(删除其中的所有文件)。
-
展开 Chapter10/experiments 文件夹,右键单击它,并创建一个新的 Python 文件。将其命名为 parallel.py。
-
将此代码添加到新文件中:
coding=utf-8
from datetime import datetime
import multiprocessing as mp
def an_expensive_function(text):
for i in range(500):
out = "{} {} {}"
out.format(text, text, text)
return "dummy output"
这是一个简单的函数,它接收文本并多次执行字符串格式化。这个函数的唯一目的是消耗 CPU 时间,这样我们就可以测试通过运行并行进程来加速我们的代码。
- 现在,在文件末尾创建一个 if name == 'main':块,以便我们可以测试代码并测量其执行时间。
if name == 'main':
texts = []
for t in range(100000):
texts.append('test text')
t1 = datetime.now()
result = map(an_expensive_function, texts)
print("Execution time: {}".format(datetime.now() - t1)) 这段代码创建了一个包含 100,000 个字符串的列表,然后将这个列表映射到函数上;这意味着 an_expensive_function 被调用了 100,000 次。注意,在这里我们使用了一种更简单的技术来测量这段代码的执行时间;t1 保存了开始时间,最后从当前时间中减去。这避免了使用分析器的开销,并且比 timeit 模块更适合我们即将要做的事情。
overhead of using a profiler and is also more suitable for what we are going to do than the timeit module.
- 运行代码并在控制台检查结果:
Execution time: 0:00:35.667500
Process finished with exit code 0
我的计算机运行 100,000 次函数大约需要 35 秒;可能你的结果会有所不同。如果你的计算机速度更快,请更改此数字以获得至少 10 秒的执行时间。注意你的结果。
- 现在编辑 if name == 'main':块,以便我们可以并行执行此代码:
if name == 'main':
texts = []
for t in range(100000):
texts.append('test text')
multi = True
t1 = datetime.now()
if multi:
my_pool = mp.Pool(processes=8)
result = my_pool.map(an_expensive_function, texts)
else:
result = map(an_expensive_function, texts)
print("Execution time: {}".format(datetime.now() - t1)) Pool 类代表一个工作进程池;它们待命,等待我们提交一些待完成的任务。
为了使用你处理器的所有核心,你需要创建与处理器核心数量相同或更多的进程。或者,如果你不想完全加载你的计算机处理器,请使用少于核心数量的进程。这是通过更改 processes 参数来完成的。
我们将代码放在一个 if 块中,这样我们就可以轻松地在并行和单进程之间切换。
- 运行你的代码并查看差异:
Execution time: 0:00:08.373000
Process finished with exit code 0
我的代码运行速度大约快了四倍。
- 现在,打开你的任务管理器或系统监视器并打开 CPU 负载


graphs.
- 再次运行代码,使用 multi=True,并查看 CPU 负载图:5. 将 multi 更改为 False 并再次运行。现在检查图:注意,当使用多进程时,所有核心在短时间内都完全占用。然而,当使用单个进程时,一些核心只部分占用。
长时间。此模式可能因计算机架构而异。

块迭代
TIFF 格式是一种灵活的图像格式,可以根据非常多样的需求进行定制。文件由一个标题、至少一个图像文件目录和任意数量的图像数据组成。简单来说,标题告诉文件中第一个目录的位置。目录包含有关图像的信息,说明如何读取与它相关的数据,并说明下一个目录的位置。每个目录和图像数据的组合都是一个图像,因此单个 TIFF 文件可能包含多个图像。
每个图像数据(即整个图像)都包含数据块(即图像的部分),这些数据块可以单独读取,每个块代表图像的特定区域。这使用户能够按块读取图像,就像我们之前做的那样。
数据块是不可分割的;为了从图像中返回数据,读取它的程序需要至少读取一个完整的块。如果所需的区域小于一个块,仍然会读取整个块,进行解码和裁剪;然后数据将被返回给用户。
数据块可以以条带或瓦片的形式存在。条带包含整个图像行中的数据,可能是一行或多行。瓦片具有宽度和长度(必须是 16 的倍数),它们很有趣,因为它们允许我们无需读取整个行即可检索特定区域。
在我们之前的例子中,我们编写了一个能够逐行读取图像的函数;现在我们将改进这个函数,以便能够读取任何大小的块。这将使我们能够在接下来的主题中用图像做更复杂的事情。
这次,我们将采用不同的方法来迭代图像。
-
在你的 Chapter10/experiments 文件夹内,创建一个名为 block_generator.py 的新文件。
-
编辑此文件并插入以下代码:
coding=utf-8
导入 os 模块
from pprint import pprint
from osgeo import gdal, gdal_array
def create_blocks_list(crop_region, block_shape):
"""创建一个块读取坐标列表。
:param crop_region: 目标区域的偏移量和形状。
(xoff, yoff, xsize, ysize)
:param block_shape: 每个块的宽度和高度。
"""
img_columns = crop_region[2]
img_rows = crop_region[3]
blk_width = block_shape[0]
blk_height = block_shape[1]
获取块的数量。
x_blocks = int((img_columns + blk_width - 1) / blk_width)
y_blocks = int((img_rows + blk_height - 1) / blk_height)
print("Creating blocks list with {} blocks ({} x {}).".format(
x_blocks * y_blocks, x_blocks, y_blocks))
blocks = []
for block_column in range(0, x_blocks):
Recalculate the shape of the rightmost block.
if block_column == x_blocks - 1:
valid_x = img_columns - block_column * blk_width
else:
valid_x = blk_width
xoff = block_column * blk_width + crop_region[0]
loop through Y lines
for block_row in range(0, y_blocks):
Recalculate the shape of the final block.
if block_row == y_blocks - 1:
valid_y = img_rows - block_row * blk_height
else:
valid_y = blk_height
yoff = block_row * blk_height + crop_region[1]
blocks.append((xoff, yoff, valid_x, valid_y))
return blocks
3. Before some explanation, let’s see this function working. Add the if name ==
'main': block at the end of the file with this code:
if name == 'main':
blocks_list = create_blocks_list((0, 0, 1024, 1024), (32, 32)) pprint(blocks_list)
4. Run the code. Since we are running a different file from before, remember to press Alt + Shift + F10 to select the file to run. Check the output:
Creating blocks list with 1024 blocks (32 x 32).
[(0, 0, 32, 32),
(0, 32, 32, 32),
(0, 64, 32, 32),
(0, 96, 32, 32),
(0, 128, 32, 32),
(0, 160, 32, 32),
(0, 192, 32, 32),
...
(992, 928, 32, 32),
(992, 960, 32, 32),
(992, 992, 32, 32)]
Process finished with exit code 0
The sole purpose of this function is to create a list of block coordinates and dimensions; each item on the list contains the offset and the size of a block. We need the size because the blocks on the edges may be smaller than the desired size.
The intention of this design choice, instead of iterating through an image directly, was to hide this low-level functionality. This function is extensive and unintuitive; we don’t want it mixed with higher-level code, making our programs much cleaner. As a bonus, we may gain a little speed when iterating multiple images because the list only needs to be produced once.
1. Now, let’s adapt the function to copy the image. To use the iteration by blocks, add this code to the file:
def copy_image(src_image, dst_image, block_shape):
try:
os.remove(dst_image)
except OSError:
pass
src_dataset = gdal.Open(src_image)
cols = src_dataset.RasterXSize
rows = src_dataset.RasterYSize
driver = gdal.GetDriverByName('GTiff')
new_dataset = driver.Create(dst_image, cols, rows,
eType=gdal.GDT_UInt16)
gdal_array.CopyDatasetInfo(src_dataset, new_dataset)
band = new_dataset.GetRasterBand(1)
blocks_list = create_blocks_list((0, 0, cols, rows), block_shape) n_blocks = len(blocks_list)
for index, block in enumerate(blocks_list, 1):
if index % 10 == 0:
print("Copying block {} of {}.".format(index, n_blocks)) block_data = src_dataset.ReadAsArray(*block)
band.WriteArray(block_data, block[0], block[1])
2. Edit the if name == 'main': block to test the code (we are also going to measure its execution time):
if name == 'main':
base_path = "../../data/landsat/"
img_name = "LC80140282015270LGN00_B8.TIF"
img_path = os.path.join(base_path, img_name)
img_copy = "../output/B8_copy.tif"
t1 = datetime.now()
copy_image(img_path, img_copy, (1024, 1024))
打印“执行时间:{}".format(datetime.now() - t1) 3.现在,运行它并检查输出:
创建包含 256 个块的块列表(16 x 16)。
复制第 10 块,共 256 块。
复制第 20 块,共 256 块……
复制第 240 块,共 256 块。
复制第 250 块,共 256 块。
执行时间:0:00:26.656000
进程以退出代码 0 结束
我们使用了 1024x1024 像素的块来复制图片。首先要注意的是,这个过程非常慢。这是因为我们正在读取比图片中块的大小还要小的块,导致大量的读写开销。
因此,让我们调整我们的函数以检测块大小并优化读取。
4.编辑copy_image函数:
def copy_image(src_image, dst_image, block_width=None,
block_height=None):
尝试:
删除dst_image
除了OSError:
pass
src_dataset = gdal.Open(src_image)
cols = src_dataset.RasterXSize
rows = src_dataset.RasterYSize
src_band = src_dataset.GetRasterBand(1)
src_block_size = src_band.GetBlockSize()
print("Image shape {}x{}px. Block shape {}x{}px.")
cols, rows, *src_block_size)
block_shape = (block_width or src_block_size[0],
block_height or src_block_size[1])
driver = gdal.GetDriverByName('GTiff')
new_dataset = driver.Create(dst_image, cols, rows,
eType=gdal.GDT_UInt16)
gdal_array.CopyDatasetInfo(src_dataset, new_dataset)
band = new_dataset.GetRasterBand(1)
blocks_list = create_blocks_list((0, 0, cols, rows), block_shape) n_blocks = len(blocks_list)
对于blocks_list中的每个block,使用enumerate(blocks_list, 1):
如果index % 10 == 0:
打印“复制第{}块,共{}块。”。format(index, n_blocks) block_data = src_dataset.ReadAsArray(*block)
band.WriteArray(block_data, block[0], block[1])
我们将块形状参数分离为宽度和高度,并使它们成为可选参数。然后我们得到了在图片中定义的块的大小(形状)。如果块宽或高没有作为参数传递,则使用图像值。
我们有一个提示,这张图片被分成了条纹。记住,当我们逐行复制图片时,速度很快。所以,我们将尝试一次读取多行。
5.编辑if __name__ == '__main__':块:
如果__name__ == '__main__':
base_path = "../../data/landsat/"
img_name = "LC80140282015270LGN00_B8.TIF"
img_path = os.path.join(base_path, img_name)
img_copy = "../output/B8_copy.tif"
t1 = datetime.now()
copy_image(img_path, img_copy, block_height=100)
打印“执行时间:{}".format(datetime.now() - t1) 6.运行代码并查看差异:
图像大小 15401x15661 像素。块大小 15401x1 像素。
创建包含 157 个块的块列表(1 x 157)。
复制第 10 块,共 157 块。
复制第 20 块,共 157 块。
复制第 30 块,共 157 块……
复制第 130 块,共 157 块。
复制第 140 块,共 157 块。
复制块 150/157。
执行时间:0:00:02.083000
进程以退出代码 0 完成
已确认,对于 Landsat 8 图像,每个块是图像的一行。通过读取整个行,我们达到了与之前相同级别的速度。
您可以尝试调整块高度参数;而不是读取 100 行,尝试读取 1
或 1000 行,看看它是否对执行时间有任何影响。
提高图像分辨率
为了获得更好的图像以进行视觉分析,我们可以结合不同的技术来提高图像分辨率。第一种技术是改变图像的大小,并通过插值重新计算缺失数据。第二种技术使用更高分辨率的波段(在我们的例子中是波段 8)——与较低分辨率的波段结合——以产生改进的真彩色地图。
图像重采样
图像缩放或重采样是一种改变图像大小的技术。通过这样做,我们改变了其中的像素数量(即样本数量)或反之亦然。
随着图像大小的增加,我们需要为之前不存在的像素提供一个值。这是通过插值完成的;新像素的值基于其周围像素的值。这就是为什么我们需要二维块的原因。
在我们的第一次试验中,我们将将一个 30 米分辨率的波段重采样为 15 米分辨率的图像。
由于我们将进行很多测试,让我们首先创建一种实用的方法来查看和比较我们的结果。为此,我们将裁剪图像并将其保存到磁盘,这样我们就可以轻松地可视化相同的区域。
- 编辑文件开头的导入:
coding=utf-8
from datetime import datetime
import os
import itertools
import numpy as np
from pprint import pprint
import functools
import multiprocessing as mp
from osgeo import gdal, gdal_array
from images import adjust_values, compose_band_path
from images import create_color_composition
import cv2 as cv
- 将此新函数添加到您的文件中:
def crop_and_save(image_path, prefix=""):
dataset = gdal.Open(image_path)
array = dataset.ReadAsArray(4209, 11677, 348, 209)
array = adjust_values(array, (10000, 30000))
array = array.astype(np.ubyte)
preview_path, preview_file = os.path.split(image_path)
preview_file = "preview_" + prefix + preview_file cv.imwrite(os.path.join("../output/", preview_file), array) 这次我们将放大图像以显示蒙特利尔的市中心,包括皇家山和老港。作为参考,下一张图像是我们感兴趣区域的 Bing 地图提取的高分辨率图像:

- 现在,将重采样函数添加到您的文件中:
def resample_image(src_image, dst_image,
block_width=None, block_height=None, factor=2,
interpolation=cv.INTER_LINEAR):
"""通过一个因子改变图像分辨率。
:param src_image: 输入图像。
:param dst_image: 输出图像。
:param block_width: 处理块的像素宽度。
:param block_height: 处理块的像素高度。
:param factor: 图像尺寸乘数。
:param interpolation: 插值方法。
"""
t1 = datetime.now()
print("开始处理 -> {}".format(dst_image)) try:
os.remove(dst_image)
except OSError:
pass
src_dataset = gdal.Open(src_image, gdal.GA_ReadOnly)
cols = src_dataset.RasterXSize
rows = src_dataset.RasterYSize
src_band = src_dataset.GetRasterBand(1)
src_block_size = src_band.GetBlockSize()
print("图像形状 {}x{}像素。块形状 {}x{}像素。".format(
cols, rows, *src_block_size)
block_shape = (block_width or src_block_size[0],
block_height or src_block_size[1])
driver = gdal.GetDriverByName('GTiff') new_dataset = driver.Create(dst_image, cols * factor,
rows * factor,
eType=gdal.GDT_UInt16)
gdal_array.CopyDatasetInfo(src_dataset, new_dataset)
band = new_dataset.GetRasterBand(1)
blocks_list = create_blocks_list((0, 0, cols, rows), block_shape) new_block_shape = (block_shape[0] * factor,
block_shape[1] * factor)
new_blocks_list = create_blocks_list((0, 0,
cols * factor,
rows * factor),
new_block_shape)
n_blocks = len(blocks_list)
for index, (block, new_block) in enumerate(
zip(blocks_list, new_blocks_list), 1):
if index % 10 == 0:
print("正在复制第 {} 个块中的 {} 个块。".format(index, n_blocks)) block_data = src_dataset.ReadAsArray(*block)
block_data = cv.resize(block_data, dsize=(0, 0),
fx=factor, fy=factor,
interpolation=interpolation)
band.WriteArray(block_data, new_block[0], new_block[1])
return dst_image, t1
此函数创建一个按定义的因子缩放的输出数据集。它从源图像中读取每个块,通过此相同的因子改变其大小,并将其写入输出。请注意,输出块的大小也会根据乘数重新计算并缩放。插值方法为可选,默认使用线性插值。
代替仅仅测试这个函数,让我们生成每种可能的插值方法的预览,这样我们可以直观地比较它们,并看到哪个返回最佳结果。由于我们将使用多进程,我们还需要一个回调函数,以便我们可以计时每个作业的执行时间。
- 将此函数添加到您的文件中:
def processing_callback(args):
t2 = datetime.now() - args[1]
print("处理完成 {}. {}".format(args[0], t2)) 5. 最后,编辑 if name == 'main': 块:
if name == 'main':
base_path = "../../data/landsat/"
img_name = "LC80140282015270LGN00_B4.TIF"
img_path = os.path.join(base_path, img_name)
interpolation_methods = {
"nearest": cv.INTER_NEAREST,
"linear": cv.INTER_LINEAR,
"area": cv.INTER_AREA,
"bicubic": cv.INTER_CUBIC,
"lanczos": cv.INTER_LANCZOS4}
output_images = []
multi = True
my_pool = mp.Pool(processes=8)
total_t1 = datetime.now()
for name, inter_method in interpolation_methods.iteritems(): out_image = "../output/" + name + '_B4.tif'
output_images.append(out_image)
if multi:
my_pool.apply_async(
resample_image, (img_path, out_image),
{'block_height': 100,
'interpolation': inter_method},
processing_callback)
else:
result = resample_image(img_path, out_image,
block_height=100,
interpolation=inter_method)
处理回调函数processing_callback(result)
如果multi:
关闭池,不再有工作。
my_pool.close()
等待所有结果准备就绪。
my_pool.join()
print("Total time: {}".format(datetime.now() - total_t1)) map(crop_and_save, output_images)
在这里,我们使用另一种技术向队列中添加工作。使用apply_assinc,我们一次添加一个工作,表示我们希望计算异步进行。最后,my_pool.join()使程序等待直到池中的所有工作都完成。
- 当
multi = True(启用多进程)时,运行代码并查看输出:开始处理 -> ../output/bicubic_B4.tif
开始处理 -> ../output/nearest_B4.tif
开始处理 -> ../output/lanczos_B4.tif
开始处理 -> ../output/linear_B4.tif
开始处理 -> ../output/area_B4.tif
完成处理 ../output/nearest_B4.tif. 0:00:33.924000
完成处理 ../output/area_B4.tif. 0:00:37.263000
完成处理 ../output/linear_B4.tif. 0:00:37.700000
完成处理 ../output/bicubic_B4.tif. 0:00:39.546000
完成处理 ../output/lanczos_B4.tif. 0:00:41.361000
总时间:0:00:42.264000
处理完成,退出代码 0
- 现在,通过将
multi = False禁用多进程,再次运行代码:开始处理 -> ../output/bicubic_B4.tif
完成处理 ../output/bicubic_B4.tif. 0:00:02.827000
开始处理 -> ../output/nearest_B4.tif
完成处理 ../output/nearest_B4.tif. 0:00:07.841000
开始处理 -> ../output/lanczos_B4.tif
完成处理 ../output/lanczos_B4.tif. 0:00:09.729000
开始处理 -> ../output/linear_B4.tif
完成处理 ../output/linear_B4.tif. 0:00:09.160000
开始处理 -> ../output/area_B4.tif
完成处理 ../output/area_B4.tif. 0:00:09.939000
总时间:0:00:39.498000
处理完成,退出代码 0
比较两次试验的输出,我们看到执行模式不同。当使用多进程时,所有进程都启动,它们执行时间较长,几乎同时完成。当不使用多进程时,每个进程在下一个进程开始之前启动和完成。
在我的电脑上,使用多进程执行代码时耗时更长。这是因为我们的工作使用了密集的读写操作,我的硬盘是硬件瓶颈,而不是 CPU。因此,在使用多进程时,我们增加了大量的额外劳动,并强制执行文件的并发读写,这减少了硬盘的
效率。
当硬件在满负荷运行时,没有方法可以克服硬件瓶颈。正如这个例子中发生的那样,我们需要写入 2.30 GB 的重采样图像数据,因此程序至少需要写入 2.30 GB 到磁盘所需的时间。
以下是我任务管理器在程序执行期间的截图,展示了所描述的情况:


这些结果可能因计算机而异,尤其是如果你使用的是具有多个存储媒体的配置,其中 IO 也可能并行发生。
打开你的输出文件夹,看看我们有什么:
面积插值:
双三次插值:



Lanczos 插值:
线性插值:
最近邻插值:


最后,看看 8 波段,全色波段,15 米分辨率,作为参考:

全色增强
通过重采样,我们能够生成像素为 15 米的图像,但在图像中对象的细节上我们取得了很少的改进。
为了克服这一限制,可以使用一种称为全色增强的技术来生成具有更好分辨率的彩色图像。其原理是使用全色波段(Landsat 波段 8)来提高合成图像的分辨率。
在这里,我们将使用一种方法,该方法包括将图像的颜色表示从 RGB 转换为 HSV——色调、饱和度、亮度。
如图像所示,值分量可以解释为颜色的亮度或强度。因此,在颜色表示转换后,值分量可以用更高分辨率的全色波段替换,从而得到具有更好定义的图像。
要做到这一点,我们需要使用 RGB 波段创建真彩色合成,就像我们之前做的那样,但这次使用的是重采样图像。然后我们改变颜色表示,替换值分量,将颜色表示转换回 RGB,并将图像保存到磁盘。
- 由于我们大部分函数已经准备好了,首先编辑 if name ==
'main': block. 删除旧的测试并添加此代码:
if name == 'main':
base_path = "../../data/landsat/"
base_name = 'LC80140282015270LGN00_B'
bands_numbers = [2, 3, 4]
bands_paths = itertools.imap(
compose_band_path,
itertools.repeat(base_path),
itertools.repeat(base_name),
bands_numbers)
output_images = list(itertools.imap(
compose_band_path,
itertools.repeat("../output/"),
itertools.repeat("15m_B"),
bands_numbers))
1) 重采样 RGB 波段。
for source, destination in zip(bands_paths, output_images): resample_image(source, destination, block_height=200)
2) 使用重采样波段创建真彩色合成。
这张图像仅用于比较。
create_color_composition(list(output_images),
'../output/preview_resampled_composition.tif',
(4209, 11677, 348, 209))
3) 裁剪所有波段。
output_images.append(
"../../data/landsat/LC80140282015270LGN00_B8.TIF") for source in output_images:
crop_and_save(source)
4) 使用裁剪后的图像进行全色增强。
band8 = "../output/preview__LC80140282015270LGN00_B8.TIF"
bgr_bands = itertools.imap(
compose_band_path,
itertools.repeat("../output/"),
itertools.repeat("preview__15m_B"),
bands_numbers)
pan_sharpen(list(bgr_bands),
band8, "../output/pan_sharpened.tif")
生成包含文件名的迭代器的过程与之前使用的方法相同。
在第一部分,RGB 波段的重采样将使用默认的线性插值。
在第二部分,我们将使用重采样的 RGB 波段创建真彩色组合。
我们不会使用这张图像进行全色增强;我们创建它只是为了比较结果。
在第三部分,我们裁剪了所有波段。通过这样做,我们也在调整灰度值的从 16 位到 8 位的值。最后,在第四部分执行全色增强。

- 现在将 pan_sharpen 函数添加到您的文件中:
def pan_sharpen(bgr_bands, pan_band, out_img):
bgr_arrays = []
将图像读入 Numpy 数组。
for item in bgr_bands:
array = cv.imread(item, flags=cv.CV_LOAD_IMAGE_GRAYSCALE)
bgr_arrays.append(array)
pan_array = cv.imread(pan_band, flags=cv.CV_LOAD_IMAGE_GRAYSCALE)
创建 RGB(BGR)组合并将其转换为 HSV。
bgr_composition = np.dstack(bgr_arrays)
hsv_composition = cv.cvtColor(bgr_composition, cv.COLOR_BGR2HSV)
拆分波段并删除原始值分量,
我们不会使用它。
h, s, v = np.dsplit(hsv_composition, 3)
h, s = np.squeeze(h), np.squeeze(s)
删除 v
使用全色波段作为 V 分量。
pan_composition = np.dstack((h, s, pan_array))
将图像转换回 BGR 并写入磁盘。
bgr_composition = cv.cvtColor(pan_composition, cv.COLOR_HSV2BGR) cv.imwrite(out_img, bgr_composition)
该过程很简单。波段的连接和拆分是通过 NumPy 的 dstack 和 dsplit 函数完成的。颜色转换是通过 cvtcolor 函数完成的。请注意,OpenCV 使用 BGR 波段序列而不是 RGB。
- 运行代码并在输出文件夹中打开彩色组合以查看结果。
重采样的组合:
全色增强的图像:

我可以说我们取得了令人印象深刻的成果。全色增强的图像非常清晰,我们可以很容易地识别出图像上的城市特征。
总结
在本章中,我们看到了如何将任务分配到多个处理器核心,从而使程序能够使用所有可用的计算能力。
尽管并行处理是一种很好的资源,但我们在示例中发现它并不适用于所有情况。具体来说,当瓶颈不是 CPU 时,多进程可能会降低程序速度。
在我们的示例过程中,我们使用了低分辨率卫星图像,并通过重采样和全色增强,提高了它们的分辨率和细节水平,从而获得了对视觉分析具有更高价值的图像。
索引
A
抽象 / 使数据同质化
关于 / 抽象概念
高级机载热辐射和反射辐射计 (ASTER) /
处理遥感图像和数据
颜色混合 / 混合图像
应用
与应用集成 / 与应用集成
所有国家的面积
计算 / 转换坐标系并计算所有国家的面积
国家
属性和关系
通过 / 通过属性和关系进行过滤
属性值
获取 / 获取属性值
B
波段 / 处理遥感图像和数据
基本统计信息,栅格数据
关于 / 获取基本统计信息
数据,准备 / 准备数据
简单信息,打印 / 打印简单信息
输出信息,格式化 / 格式化输出信息
计算四分位数,直方图和其他统计信息 / 计算四分位数、直方图和其他统计信息
计算直方图,直方图和其他统计信息 / 计算四分位数、直方图和其他统计信息
其他统计信息,计算 / 计算四分位数、直方图和其他
统计
创建,懒属性 / 使统计成为懒属性
块迭代
关于 / 块迭代
书籍项目
创建 / 创建书籍项目
C
最近点
搜索 / 查找最近点
代码注释
关于 / 记录您的代码
代码分析
关于 / 代码分析
彩色分类图像
创建 / 创建颜色分类图像
为地图选择正确的颜色 / 为地图选择正确的颜色
上下文管理器 / 从 URL 下载数据
坐标系
转换 / 转换坐标系并计算所有国家的面积
所有国家
国家
按面积大小排序 / 按面积大小排序国家
当前位置
设置 / 设置您的当前位置
D
数据
使数据同质化 / 使数据同质化
导入 / 生成表格并导入数据
过滤 / 过滤数据
搜索数据,交叉信息 / 搜索数据和交叉信息
过滤,使用的边界 / 使用边界进行过滤
数据库
信息,存储在 / 在数据库中存储信息
用真实数据填充 / 用真实数据填充数据库
数据库插入
优化 / 优化数据库插入
数据解析
优化 / 优化数据解析
数字高程模型 (DEM)
关于 / 处理遥感图像和数据
参考 / 处理遥感图像和数据
文档字符串
关于 / 文档化你的代码
E
ElementTree / 用真实数据填充数据库
ESRI 形状文件 / 表示地理数据
执行时间
测量 / 测量执行时间
F
文件
打开 / 打开文件并获取其内容
内容,获取 / 打开文件并获取其内容
内容,准备分析 / 准备内容以进行分析
过滤器
链接 / 链接过滤器
第一个示例
编程 / 编程和运行第一个示例
运行 / 编程和运行第一个示例
函数
组合,到应用程序中 / 将函数组合到应用程序中
G
GDAL
关于 / 安装 GDAL 和 OGR
安装,在 Windows 上 / Windows
安装,在 Ubuntu Linux 上 / Ubuntu Linux
地理藏宝点
抽象 / 抽象地理藏宝点
地理藏宝应用程序
基本应用程序结构,构建 / 构建基本应用程序结构
应用程序树结构,创建 / 创建应用程序树结构
函数 / 函数和方法
方法 / 函数和方法
代码,文档化 / 文档化你的代码
应用程序入口点,创建 / 创建应用程序入口点
地理藏宝数据
下载 / 下载地理藏宝数据
直接下载 / 下载地理藏宝数据
REST API / 下载地理藏宝数据
数据源 / 地理藏宝数据源
信息,从 REST API 获取 / 从 REST API 获取信息
从 URL 下载 / 从 URL 下载数据
手动下载 / 手动下载数据
下载链接 / 手动下载数据
抽象 / 抽象地理藏宝数据
导入 / 导入地理藏宝数据
GPX 属性,读取 / 读取 GPX 属性
同质数据,返回 / 返回同质数据
转换,为 Geocache 对象 / 将数据转换为 Geocache 对象
多个来源,合并 / 合并多个数据来源
GeoDjango / 创建对象关系映射
地理数据
表示 / 表示地理数据
几何形状
表示 / 表示几何形状
几何关系
关于 / 几何关系
接触 / 接触
交叉 / 交叉
包含 / 包含
在内 / 在内部
等于或几乎等于 / 等于或几乎等于
相交 / 相交
不相交 / 不相交
地理对象
导出 / 导出地理对象
全局解释器锁 (GIL)
全景锐化 / 多进程基础
GPX 格式 / 表示地理数据
H
硬件瓶颈 / 图像重采样
阴影渲染 / 创建阴影渲染图像
直方图
用于地图着色 / 使用直方图着色图像
I
IDE
关于 / 安装 IDE
安装 / 安装 IDE
在 Windows 上安装 / Windows
在 Linux 上安装 / Linux
图像组合
创建 / 创建图像组合
真彩色组合 / 真彩色组合
特定区域,处理 / 处理特定区域
假彩色组合 / 假彩色组合
图像处理管道
构建 / 构建图像处理管道
图像分辨率
提高 / 提高图像分辨率
图像重采样 / 图像重采样
全景锐化 / 全景锐化
图像
表示 / 理解图像的表示方式
打开,使用 OpenCV / 使用 OpenCV 打开图像
数值类型 / 了解数值类型
混合 / 混合图像
内存使用 / 内存和图像
分块处理 / 分块处理图像
打开,GDAL 使用 / 使用 GDAL 打开图像
遍历整个图像 / 遍历整个图像
迭代器 / 遍历整个图像
迭代器 / 遍历整个图像
J
Java 拓扑套件 (JTS)
关于 / 使用 Shapely 处理几何形状
JSON (JavaScript Object Notation) / 表示地理数据
L
Landsat 8 图像
关于 / 获取 Landsat 8 图像
获取 / 获取 Landsat 8 图像
懒加载 / 将统计信息转换为懒属性
线条
导入 / 导入线条
Linux
IDE,安装 / Linux
M
Map Maker 应用程序
创建 / 创建 Map Maker 应用程序
使用 PythonDatasource / 使用 PythonDatasource
使用过滤功能 / 使用应用程序进行过滤
Mapnik
关于 / 安装 Mapnik,Windows,了解 Mapnik
安装 / 安装 Mapnik
在 Windows 上安装 / Windows
在 Ubuntu Linux 上安装 / Ubuntu Linux
使用 / 了解 Mapnik 进行实验
使用纯 Python 制作地图 / 使用纯 Python 制作地图
使用样式表制作地图 / 使用样式表制作地图
地图
样式化 / 样式化地图
样式选项 / 样式化地图
添加图层 / 向地图添加图层
大量数据
导入 / 导入大量数据
缓存 / 将统计信息转换为懒属性
记忆化 / 转换空间参考系统和单位
瓦片拼接 / 拼接图像
多个属性
通过多个属性过滤 / 通过多个属性过滤
多进程
基础 / 多进程基础
关于 / 多进程基础
N
新功能
集成到应用程序中 / 将新功能集成到应用程序中
Noun Project
参考 / 点样式
Numpy
关于 / 安装 NumPy
安装 / 安装 NumPy
在 Windows 上安装 / Windows
在 Ubuntu Linux 上安装 / Ubuntu Linux
Numpy 文档
参考 / 瓦片拼接图像
O
对象关系映射 (ORM)
创建 / 创建对象关系映射
环境,准备 / 准备环境
模型,更改 / 更改我们的模型
管理器,自定义 / 自定义管理器
OGR 驱动程序
关于 / 安装 GDAL 和 OGR
Opencaching 节点 / Geocaching 数据源
Open Computer Vision (OpenCV) 软件包 / 自动预览地图
OpenCV
关于 / OpenCV
开放地理空间联盟 (OGC)
关于 / 了解知名文本
OpenStreetMap / 表示几何形状
OpenStreetMap 的兴趣点
导入 / 导入 OpenStreetMap 的兴趣点
OpenStreetMap 维基
URL / 搜索数据和交叉信息
操作性陆地成像仪 (OLI) / 处理卫星图像
其他软件包
从 pip 安装 / 直接从 pip 安装其他软件包
安装,在 Windows 上 / Windows
安装,在 Ubuntu Linux 上 / Ubuntu Linux
Overpass API / 导入 OpenStreetMap 的兴趣点
P
绘图模型 / 多边形样式
全色增强 / 全色增强
PEP-8
关于 / 转换坐标系并计算所有区域的面积
国家
URL / 转换坐标系并计算所有区域的面积
国家
像素
关于 / 了解图像的表示方式
兴趣点 (POI) / 导入 OpenStreetMap 的兴趣点
多边形
使用 / 处理多边形
导入 / 导入多边形
Postgis 扩展 / 在数据库中存储信息
PostgreSQL
URL / 在数据库中存储信息
PostgreSQL 数据库 / 在数据库中存储信息
Poços de Caldas / 处理遥感图像和数据
处理管道 / 构建图像处理管道
性能分析 / 测量执行时间
Python
安装 / 安装 Python
安装,在 Windows 上 / Windows
安装,在 Ubuntu Linux 上 / Ubuntu Linux
Python 术语表
参考 / 使用 Python 对象作为数据源
Python 对象
作为数据源使用 / 使用 Python 对象作为数据源
Python 软件包
关于 / Python 软件包和软件包管理器
软件包管理器 / Python 软件包和软件包管理器
存储库,在 Windows 上 / Windows 的 Python 软件包存储库
安装 / 安装软件包和所需软件
所需软件 / 安装软件包和所需软件
安装,在 Windows 上 / Windows
安装,在 Ubuntu Linux 上 / Ubuntu Linux
R
栅格数据
基本统计 / 获取基本统计信息
RasterData 类
创建 / 创建 RasterData 类
高程阴影 / 创建阴影高程图像
遥感图像
处理 / 处理遥感图像和数据
拼接 / 拼接图像
值,调整 / 调整图像的值
裁剪 / 裁剪图像
阴影高程图像,创建 / 创建阴影高程图像
REST (表示性状态传输) / 下载地理藏宝数据
reStructuredText
参考 / 记录你的代码
S
卫星图像
关于 / 处理卫星图像
处理 / 处理卫星图像
可伸缩矢量图形 (SVG) 文件 / 点样式
Shapely
关于 / 安装 Shapely, 使用 Shapely 处理几何形状
在 Windows 上安装 / Windows
在 Ubuntu Linux 上安装 / Ubuntu Linux
处理几何形状 / 使用 Shapely 处理几何形状
意面数据 / 表示几何形状
Spatialite 扩展 / 在数据库中存储信息
空间参考系统
单位,转换 / 转换空间参考系统和单位
SpatiLite / 表示地理数据
SQLite 数据库 / 在数据库中存储信息
统计
以颜色显示 / 用颜色显示统计数据
样式选项,地图
地图样式 / 地图样式
多边形样式 / 多边形样式
线样式 / 线样式
文本样式 / 文本样式
点样式 / 点样式
SVG 转换
参考 / 点样式
T
表格
生成 / 生成表格和导入数据
测试数据
删除 / 删除测试数据
热红外传感器 (TIRS) / 处理卫星图像
TIFF 格式 / 块迭代
U
美国地质调查局 (USGS) 地球探索者 / 获取 Landsat 8 图像
Ubuntu
URL / 在数据库中存储信息
Ubuntu Linux
Python,安装 / Ubuntu Linux
Python 软件包,安装 / Ubuntu Linux
Numpy,安装 / Ubuntu Linux
GDAL,安装 / Ubuntu Linux
Mapnik,安装 / Ubuntu Linux
Shapely,安装 / Ubuntu Linux
其他包,安装 / Ubuntu Linux
通用函数
创建,用于生成地图 / 创建生成地图的实用函数
数据源,运行时更改 / 运行时更改数据源
地图,自动预览 / 自动预览地图
W
已知二进制 (WKB)
关于 / 了解已知文本
已知文本 (WKT)
关于 / 了解已知文本
Windows
Python,安装 / Windows
Python 包,安装 / Windows
Numpy,安装 / Windows
GDAL,安装 / Windows
Mapnik,安装 / Windows
Shapely,安装 / Windows
其他包,安装 / Windows
IDE,安装 / Windows
文档大纲
-
使用 Python 通过示例进行地理空间开发
-
致谢
-
关于作者
-
关于审稿人
-
www.PacktPub.com
-
支持文件、电子书、折扣优惠等
-
为什么要订阅?
-
Packt 账户持有者的免费访问
-
前言
-
本书涵盖的内容
-
本书所需内容
-
本书面向的对象
-
约定
-
读者反馈
-
客户支持
-
下载示例代码
-
下载本书的颜色图像
-
勘误表
-
盗版
-
问题
-
- 准备工作环境
-
安装 Python
-
Windows
-
Ubuntu Linux
-
Python 包和包管理器
-
Windows 的 Python 包仓库
-
安装包和所需软件
-
OpenCV
-
Windows
-
Ubuntu Linux
-
安装 NumPy
-
Windows
-
Ubuntu Linux
-
安装 GDAL 和 OGR
-
Windows
-
Ubuntu Linux
-
安装 Mapnik
-
Windows
-
Ubuntu Linux
-
安装 Shapely
-
Windows
-
Ubuntu Linux
-
直接从 pip 安装其他包
-
Windows
-
Ubuntu Linux
-
安装集成开发环境
-
Windows
-
Linux
-
创建书籍项目
-
编程和运行第一个示例
-
转换坐标系并计算所有国家的面积
-
按面积大小排序国家
-
总结
-
- 地理藏宝应用
-
构建基本应用程序结构
-
创建应用程序树结构
-
函数和方法
-
记录你的代码
-
创建应用程序入口点
-
下载地理藏宝数据
-
地理藏宝数据源
-
从 REST API 获取信息
-
从 URL 下载数据
-
手动下载数据
-
打开文件并获取其内容
-
准备分析内容
-
将函数组合到应用程序中
-
设置你的当前位置
-
找到最近点
-
总结
-
- 结合多个数据源
-
表示地理数据
-
表示几何形状
-
使数据同质化
-
抽象的概念
-
抽象化地理藏宝点
-
抽象化地理藏宝数据
-
导入地理藏宝数据
-
读取 GPX 属性
-
返回同质数据
-
将数据转换为地理藏宝对象
-
合并多个数据源
-
将新功能集成到应用程序中
-
总结
-
改进应用搜索功能
-
处理多边形
-
了解知名文本
-
使用 Shapely 处理几何形状
-
导入多边形
-
获取属性值
-
导入线条
-
转换空间参考系统和单位
-
几何关系
-
接触点
-
交叉
-
包含
-
内部
-
等于或几乎等于
-
相交
-
不连接
-
按属性和关系过滤
-
按多个属性过滤
-
链式过滤
-
与应用程序集成
-
摘要
-
- 制作地图
-
了解 Mapnik
-
使用纯 Python 制作地图
-
使用样式表制作地图
-
创建生成地图的实用函数
-
在运行时更改数据源
-
自动预览地图
-
样式化地图
-
地图样式
-
多边形样式
-
线样式
-
文本样式
-
向地图添加图层
-
点样式
-
使用 Python 对象作为数据源
-
导出地理对象
-
创建 Map Maker 应用
-
使用 PythonDatasource
-
使用带有过滤的应用程序
-
摘要
-
- 与遥感图像一起工作
-
理解图像的表示方式
-
使用 OpenCV 打开图像
-
了解数值类型
-
处理遥感图像和数据
-
镶嵌图像
-
调整图像的值
-
裁剪图像
-
创建阴影地形图
-
构建图像处理管道
-
创建 RasterData 类
-
摘要
-
- 从栅格数据中提取信息
-
获取基本统计数据
-
准备数据
-
打印简单信息
-
格式化输出信息
-
计算四分位数、直方图和其他统计数据
-
使统计数据成为懒属性
-
创建颜色分类图像
-
为地图选择合适的颜色
-
混合图像
-
用颜色显示统计数据
-
使用直方图对图像进行着色
-
摘要
-
- 数据挖掘应用
-
测量执行时间
-
代码分析
-
在数据库上存储信息
-
创建对象关系映射
-
准备环境
-
改变我们的模型
-
自定义管理器
-
生成表格并导入数据
-
过滤数据
-
导入大量数据
-
优化数据库插入
-
优化数据解析
-
导入 OpenStreetMap 的兴趣点
-
移除测试数据
-
用真实数据填充数据库
-
搜索数据和交叉信息
-
使用边界进行过滤
-
摘要
-
- 处理大图像
-
处理卫星图像
-
获取 Landsat 8 图像
-
内存和图像
-
分块处理图像
-
使用 GDAL 打开图像
-
遍历整个图像
-
创建图像组合
-
真彩色组合
-
处理特定区域
-
假彩色组合
-
摘要
-
- 并行处理
-
多进程基础
-
块迭代
-
提高图像分辨率
-
图像重采样
-
全色增强
-
摘要
-
索引


浙公网安备 33010602011771号