如果您收集了 GPS 轨迹,您就会知道结果可能具有不同的准确性。沿路线收集的轨迹点并不总是在路上,可能会很紧张。
如果您是物流、送货或出租车公司——这会带来一个大问题。使用这些点计算的距离将不准确——尤其是如果这些点是间隔开的。此外,您无法比较在不同设备或人员处收集的轨迹,因为即使它们在同一条路线上,它们的几何形状也会不同。
此问题的解决方案是将每个点捕捉到最近的路段。虽然这在原则上听起来很容易,但准确地做到这一点是具有挑战性的。你不能为一个点选择最近的路段——因为最近的点可能在交叉的街道上。您需要考虑上一个点和下一个点之间的路线,以找到最合理的捕捉位置。
幸运的是,一个名为Open Source Routing Machine (OSRM)的开源项目通过快速且可扩展的算法解决了这个问题。我们可以使用 OSRM 的匹配服务将 GPS 点捕捉到最合适的路段。OSRM 引擎使用来自 OpenStreetMap (OSM) 项目的数据。OSM 在世界大部分地区拥有相当不错的街道网络覆盖,并且还在不断改进。通过利用来自 OSM 的开放数据和来自 OSRM 的开放路由算法,我们可以实现捕捉服务。
OSRM 的工作原理是通过HTTP API获取输入,计算结果并通过 JSON 对象返回它们。
OSRM 提供了一个演示服务器和一个演示 HTTP 服务。但是我发现演示服务器经常过载,不适合用于偶尔测试以外的用途。
如果您想在您的项目中使用 OSRM 引擎,最好的选择是在您的计算机或服务器上运行您自己的服务。运行您自己的服务实例可能听起来很吓人,但使用 Docker 设置它非常简单。该文档有很好的说明。以下是我使用印度班加罗尔市的数据运行本地实例的步骤。
获取数据
在城市级别获取 OpenStreetMap 提取的一种简单方法是Interline。如果您需要国家和大陆级别的数据,可以从GeoFabrik下载。
我注册了一个免费的 API 密钥,并为班加罗尔下载了作为begaluru_india.osm.pbf文件的提取物。我在我的系统上创建了一个新文件夹,将数据文件复制到那里,启动 Docker 并在终端中运行以下命令。文档中唯一的变化是–max-matching-size参数,我将其增加到 5000,以便我们可以匹配大型 GPS 轨迹。
<span style="color:#1e1e1e"><span style="background-color:#ffffff"><code>docker run -t -v "${PWD}:/data" osrm/osrm-backend osrm-extract -p /opt/car.lua /data/bengaluru_india.osm.pbf
docker run -t -v "${PWD}:/data" osrm/osrm-backend osrm-partition /data/bengaluru_india.osrm
docker run -t -v "${PWD}:/data" osrm/osrm-backend osrm-customize /data/bengaluru_india.osrm
docker run -t -i -p 5000:5000 -v "${PWD}:/data" osrm/osrm-backend osrm-routed --algorithm mld --max-matching-size 5000 /data/bengaluru_india.osrm
</code></span></span>
运行最后一条命令后,服务器将在您的机器上启动,它可以接受 URL http://127.0.0.1:5000 的匹配请求
匹配请求的格式如下,其中关键部分是 {coordinates} 参数,它是轨迹上每个点的坐标,格式为longitude1, latitude1;longitude2, latitude2。
docker run -t -v "${PWD}:/data" osrm/osrm-backend osrm-extract -p /opt/car.lua /data/bengaluru_india.osm.pbf
docker run -t -v "${PWD}:/data" osrm/osrm-backend osrm-partition /data/bengaluru_india.osrm
docker run -t -v "${PWD}:/data" osrm/osrm-backend osrm-customize /data/bengaluru_india.osrm
docker run -t -i -p 5000:5000 -v "${PWD}:/data" osrm/osrm-backend osrm-routed --algorithm mld --max-matching-size 5000 /data/bengaluru_india.osrm
我们需要通过读取 GPS 轨迹以编程方式编译此 URL,并将其发送到我们在上一步中启动的本地匹配服务。还需要对结果进行处理并转换为轨迹线进行可视化。这就是 QGIS 的用武之地。使用 PyQGIS,我们可以编写一个处理脚本,使这种交互变得简单直观。
打开 QGIS。转到处理 → 工具箱 → 创建新脚本
在脚本编辑器中复制/粘贴以下代码并将其保存为snap_to_road.py
import requests
from PyQt5.QtCore import QCoreApplication
from qgis.core import (QgsProcessing, QgsProcessingAlgorithm,
QgsProcessingParameterFeatureSource, QgsProcessingParameterFeatureSink,
QgsProcessingParameterString, QgsProcessingParameterNumber, QgsWkbTypes,
QgsGeometry, QgsFeatureSink, QgsFields, QgsPoint, QgsFeature)
from PyQt5.QtXml import QDomDocument
class ExportLayoutAlgorithm(QgsProcessingAlgorithm):
"""Exports the current map view to PDF"""
INPUT = 'INPUT'
OUTPUT = 'OUTPUT'
SERVICE = 'SERVICE'
TOLERANCE = 'TOLERANCE'
def flags(self):
return super().flags() | QgsProcessingAlgorithm.FlagNoThreading
def initAlgorithm(self, config=None):
self.addParameter(
QgsProcessingParameterFeatureSource(
'INPUT',
self.tr('Input vector layer'),
types=[QgsProcessing.TypeVectorPoint]
)
)
self.addParameter(
QgsProcessingParameterString(
self.SERVICE,
self.tr('OSRM Service URL'),
'http://127.0.0.1:5000'
)
)
self.addParameter(
QgsProcessingParameterNumber(
self.TOLERANCE,
self.tr('Snapping Tolerance (meters)'),
QgsProcessingParameterNumber.Integer,
10
)
)
self.addParameter(
QgsProcessingParameterFeatureSink(
self.OUTPUT,
'Snapped Line',
QgsProcessing.TypeVectorLine
)
)
def processAlgorithm(self, parameters, context, feedback):
source = self.parameterAsSource(parameters, self.INPUT, context)
service = self.parameterAsString(parameters, self.SERVICE, context)
tolerance = self.parameterAsInt(parameters, self.TOLERANCE, context)
sink, dest_id = self.parameterAsSink(
parameters,
self.OUTPUT,
context,
QgsFields(),
QgsWkbTypes.LineString,
source.sourceCrs()
)
# Compute the number of steps to display within the progress bar and
# get features from source
total = 100.0 / source.featureCount() if source.featureCount() else 0
features = source.getFeatures()
coordinate_list = []
for current, f in enumerate(features):
# Stop the algorithm if cancel button has been clicked
if feedback.isCanceled():
break
geom = f.geometry().asPoint()
coordinates = '{},{}'.format(geom.x(), geom.y())
coordinate_list.append(coordinates)
feedback.setProgress(int(current * total))
coordinate_str = ';'.join(coordinate_list)
radius = ['{}'.format(tolerance)]
radius_str = ';'.join(radius*len(coordinate_list))
service_url = '/match/v1/driving/{}'.format(coordinate_str)
request_url = service + service_url
payload = {'geometries': 'geojson', 'steps': 'false', 'radiuses': radius_str}
r = requests.get(request_url, params=payload)
results = r.json()
for match in results['matchings']:
coords = match['geometry']['coordinates']
point_list = [QgsPoint(coord[0], coord[1]) for coord in coords]
out_f = QgsFeature()
out_f.setGeometry(QgsGeometry.fromPolyline(point_list))
sink.addFeature(out_f, QgsFeatureSink.FastInsert)
return {self.OUTPUT: sink}
def name(self):
return 'snap_to_roads'
def displayName(self):
return self.tr('Snap to Roads')
def shortHelpString(self):
return self.tr('Snaps GPS Trackpoints to OSM roads using OSRM service')
def group(self):
return self.tr(self.groupId())
def groupId(self):
return ''
def tr(self, string):
return QCoreApplication.translate('Processing', string)
def createInstance(self):
return ExportLayoutAlgorithm()
保存后,新算法将出现在 Processing → Toolbox → Scripts → Snap To Roads 中。在 QGIS 中加载您的 GPS 跟踪点并双击脚本以运行它。
生成的捕捉道路线将添加到 QGIS 图层面板。您可以看到 OSRM 的工作非常有魅力,并且结果正如人们所期望的那样。