1. 从shp到osm再到sumo:路网构建的完整思路拆解
做交通仿真的人迟早会撞上一个很现实的问题:手头拿到的路网数据是shp格式的,但仿真工具要的是别的格式。尤其是用sumo做微观交通仿真的时候,它原生支持的路网格式是net.xml,而net.xml通常由osm文件或者netconvert工具从其他格式转换而来。shp文件作为GIS领域最常见的数据格式之一,天然带着完整的几何信息和属性字段,但sumo并不直接吃shp。所以中间需要一个桥梁,osm文件就是这个桥梁的最佳选择。
这个流程的核心逻辑其实不复杂:shp文件里存的是矢量化的道路线要素,每条线有坐标串、有道路等级、有车道数等属性;osm文件是一种基于XML的开放地图数据格式,用node和way来描述路网拓扑;sumo的netconvert工具能读取osm文件,自动识别道路连接关系、生成交叉口、分配车道,最终输出可仿真的路网文件。整条链路是:shp → osm → net.xml → sumo仿真。
为什么非要绕这一圈?直接用sumo的netconvert读shp不行吗?实测下来,netconvert对shp的支持非常有限,它主要设计来读osm、vissim、openDRIVE等格式,shp不在其原生支持列表里。即便有些版本能通过插件读取,属性映射和拓扑构建也经常出问题。所以把shp先转成osm,让osm承载完整的道路属性和拓扑信息,再交给netconvert处理,是目前最稳妥的路线。
这个流程适合谁?做城市交通仿真的研究生、交通规划院的工程师、智慧城市项目的开发人员,以及任何需要把GIS路网数据导入微观仿真工具的人。前提是你对GIS软件的基本操作有了解,知道什么是属性表、什么是坐标系,同时对sumo的基本概念(节点、边、车道、连接)有初步认知。如果这两块都是零基础,建议先补一下QGIS和sumo的基础教程,否则中间任何一个环节卡住都会很痛苦。
我自己的经验是,这个流程的难点不在单个步骤,而在数据质量的把控和属性字段的映射。shp文件里的道路属性五花八门,字段名可能是中文、可能是拼音缩写、可能根本没有车道数字段。osm文件对道路类型有固定的tag体系,sumo又对车道数、限速、优先级有明确要求。这三者之间的信息损失和补全,才是真正花时间的地方。下面我会把每个环节拆开讲,包括我踩过的坑和最终跑通的方案。
2. 核心工具选型与数据准备要点
2.1 工具链的确定:QGIS + JOSM + sumo netconvert
整个流程涉及三个核心工具,每个都有替代方案,但我最终选的是这套组合,理由如下。
QGIS负责shp文件的读取、属性检查、坐标转换和初步处理。它是开源GIS工具里最成熟的一个,对shp的支持无可挑剔,插件生态也丰富。替代方案有ArcGIS,但ArcGIS是商业软件,而且导出osm的插件不如QGIS灵活。QGIS还有一个关键优势:它的Python控制台可以直接调用GDAL/OGR库,批量处理属性字段非常方便。
JOSM是OpenStreetMap的桌面编辑器,用来打开和编辑osm文件。为什么不用其他osm编辑器?因为JOSM对路网拓扑的校验能力最强,它能自动检测断头路、重复节点、无效连接等问题,而且支持自定义tag预设。当你从shp转换过来的osm文件存在拓扑错误时,JOSM是最趁手的修复工具。
sumo netconvert是sumo自带的命令行工具,负责把osm转成net.xml。它的参数非常多,但核心就那么几个:道路类型映射、车道数推断、连接生成规则。后面会详细讲参数配置。
注意:三个工具的版本要匹配。QGIS建议用3.28以上,JOSM用最新稳定版,sumo用1.18以上。版本差异可能导致osm文件格式兼容性问题,尤其是JOSM保存的osm版本和netconvert读取的版本不一致时,会出现莫名其妙的解析错误。
2.2 shp文件的预处理:坐标系与属性字段
拿到shp文件后,第一件事不是急着转换,而是检查两样东西:坐标系和属性表。
坐标系方面,osm文件使用的是WGS84地理坐标系(EPSG:4326),单位是经纬度。如果你的shp文件用的是投影坐标系(比如CGCS2000的某个高斯投影带,或者Web Mercator),必须先转换到WGS84。在QGIS里操作很简单:右键图层 → 导出 → 保存要素为 → 选择CRS为EPSG:4326。但这里有个坑:转换后的坐标精度会损失,尤其是从投影坐标转地理坐标时,小数点后位数要保留足够多,建议至少7位小数,否则路网会出现明显的偏移。
属性表方面,你需要确认几个关键字段是否存在:
| 字段用途 | 常见字段名 | 是否必须 | 缺失时的处理 |
|---|---|---|---|
| 道路等级 | type, road_type, fclass | 必须 | 根据道路宽度或名称推断 |
| 车道数 | lanes, lane_num, chedao | 建议有 | 按道路等级默认赋值 |
| 限速 | speed, maxspeed, xiansu | 建议有 | 按道路等级默认赋值 |
| 单双向 | oneway, danxiang | 建议有 | 默认双向 |
| 道路名称 | name, road_name | 可选 | 留空即可 |
如果车道数字段缺失,后面在osm里就得手动补或者用netconvert的默认推断。我试过完全没有车道数的情况,netconvert会根据道路等级自动分配,但结果往往偏少,导致仿真时道路通行能力不足。所以如果原始数据没有车道数,建议在QGIS里用字段计算器按道路等级批量赋值,比如快速路默认6车道、主干路4车道、次干路2车道、支路2车道。
2.3 数据清洗:去除无效要素与拓扑检查
shp文件里经常混着一些不需要的要素:断头路、重复线、悬挂线、自相交线。这些在GIS里可能不影响显示,但转到osm后会导致拓扑错误,netconvert处理时会报错或者生成奇怪的路网。
在QGIS里做清洗的步骤:
- 删除重复几何:矢量 → 几何工具 → 删除重复几何体。重复线会导致osm里出现重叠way,netconvert会生成多余的车道。
- 修复几何有效性:矢量 → 几何工具 → 修复几何。自相交的线在osm里会变成无效way。
- 提取有效范围:如果你的shp覆盖范围很大,但只需要某个区域的路网,用裁剪工具先裁出目标区域,减少后续处理量。
- 检查断头路:用拓扑检查器插件,找出悬挂节点(dangling node)。这些在osm里会变成死胡同,如果实际不是死胡同,需要手动连接。
实操心得:我一般会在QGIS里先做一个缓冲区分析,把距离小于1米的平行线找出来,这些往往是重复采集或者偏移的道路。处理掉之后,osm的way数量能减少10%到20%,netconvert的处理速度明显提升。
3. shp转osm的实操过程与关键环节
3.1 用QGIS导出osm格式的完整步骤
QGIS本身没有直接的“导出为osm”菜单,但可以通过插件或者Python脚本实现。我试过三种方法,最终推荐用QGIS的OSM导出插件或者GDAL的ogr2ogr命令行。
方法一:QGIS插件导出
- 在QGIS里加载清洗后的shp文件。
- 安装插件:插件 → 管理并安装插件 → 搜索“OSM” → 安装“OSM Export”或类似插件。
- 右键图层 → 导出 → 导出为OSM文件。
- 在导出对话框中,需要做字段映射:把shp的字段映射到osm的tag。比如把type字段映射到highway tag,把lanes字段映射到lanes tag。
这个方法的优点是可视化操作,适合不熟悉命令行的用户。缺点是字段映射不够灵活,复杂属性容易丢失。
方法二:ogr2ogr命令行
这是我最推荐的方式,因为可控性最强。GDAL的ogr2ogr工具支持直接转换shp到osm,命令如下:
ogr2ogr -f "OSM" output.osm input.shp -dsco COMPRESS=NO -lco COLUMN_TYPES=highway=String,lanes=Integer但ogr2ogr直接转osm有个问题:它生成的osm文件里,way的tag是直接从shp字段名来的,不会自动映射到osm的标准tag。比如shp里字段叫“road_type”,ogr2ogr会生成<tag k="road_type" v="主干路"/>,而osm标准应该是<tag k="highway" v="primary"/>。所以转换后还需要用脚本做tag重映射。
我通常的做法是:先用ogr2ogr转出基础osm,然后用Python脚本批量替换tag键值。脚本逻辑很简单,读入osm文件,遍历所有way的tag,按照映射表替换k和v。
import xml.etree.ElementTree as ET tag_mapping = { 'road_type': 'highway', 'chedao': 'lanes', 'xiansu': 'maxspeed' } value_mapping = { '主干路': 'primary', '次干路': 'secondary', '支路': 'residential', '快速路': 'motorway' } tree = ET.parse('output.osm') root = tree.getroot() for way in root.findall('way'): for tag in way.findall('tag'): k = tag.get('k') v = tag.get('v') if k in tag_mapping: tag.set('k', tag_mapping[k]) if v in value_mapping: tag.set('v', value_mapping[v]) tree.write('output_mapped.osm', encoding='utf-8', xml_declaration=True)3.2 osm文件的结构与tag体系详解
osm文件的基本结构是三层:node、way、relation。node是节点,有经纬度坐标;way是一串node的引用,代表一条道路;relation用于描述复杂关系,比如公交线路、多边形区域。路网转换主要涉及node和way。
一个典型的道路way在osm里长这样:
<way id="123456"> <nd ref="1001"/> <nd ref="1002"/> <nd ref="1003"/> <tag k="highway" v="primary"/> <tag k="lanes" v="4"/> <tag k="maxspeed" v="60"/> <tag k="oneway" v="yes"/> <tag k="name" v="示例路"/> </way>其中highway tag是最关键的,它决定了道路的类型和默认属性。osm的highway tag有一套完整的等级体系,常用的有:
| osm highway值 | 含义 | sumo默认车道数 | sumo默认限速(km/h) |
|---|---|---|---|
| motorway | 高速公路 | 2 | 120 |
| trunk | 快速路 | 2 | 100 |
| primary | 主干路 | 2 | 80 |
| secondary | 次干路 | 2 | 60 |
| tertiary | 支路 | 1 | 50 |
| residential | 居住区道路 | 1 | 30 |
| service | 服务道路 | 1 | 20 |
注意:netconvert在读取osm时,如果way上没有lanes tag,它会用上表的默认值。这就是为什么前面强调要在shp里补全车道数,否则默认值往往偏小。
3.3 用JOSM修复拓扑与补充属性
从shp转出来的osm文件,拓扑上通常有几个问题:节点没有合并(两条路的交叉点有两个重合但独立的node)、way的方向不一致、缺少连接关系。JOSM能自动修复大部分问题。
打开JOSM,加载osm文件,然后:
- 合并重合节点:编辑 → 合并节点 → 选择“在容差范围内合并”。容差设0.5米左右,太小合并不了,太大会把不该合并的节点也合并了。
- 修复way方向:工具 → 反转way方向。对于单行道,确保方向正确。
- 添加缺失的tag:选中way,在右侧属性面板里添加highway、lanes、maxspeed等tag。
- 运行验证:验证 → 验证数据。JOSM会列出所有拓扑错误和警告,逐条修复。
JOSM还有一个很实用的功能:从地理数据导入。如果你有GPS轨迹或者参考底图,可以叠加显示,手动修正偏移的道路。不过对于批量处理,还是以自动修复为主。
实操心得:JOSM的验证功能会报很多“way end node near other way”的警告,这些是断头路。如果实际路网中这些路是连通的,就用“连接节点”工具把它们连上。如果确实是断头路(比如小区内部道路),可以忽略。但netconvert对断头路很敏感,可能会生成无法通行的死胡同,影响仿真效果。
4. osm导入sumo:netconvert参数配置与路网生成
4.1 netconvert的核心参数解析
netconvert是sumo路网转换的核心工具,命令格式如下:
netconvert --osm-files input.osm --output-file output.net.xml但这样直接跑,生成的路网往往不能用。必须加上一系列参数来控制转换行为。我常用的参数组合:
netconvert \ --osm-files input.osm \ --output-file output.net.xml \ --geometry.remove \ --ramps.guess \ --junctions.join \ --tls.guess-signals \ --tls.discard-simple \ --tls.join \ --no-turnarounds \ --offset.disable-normalization \ --lefthand false \ --speed-in-kmh \ --lane-numbers-from-osm逐个解释这些参数的作用:
--geometry.remove:移除不必要的几何节点,简化路网。osm里很多way有大量中间节点,这些节点对仿真没用,反而增加计算量。--ramps.guess:自动识别匝道。对于高速公路和快速路,这个参数能正确生成出入口。--junctions.join:合并距离很近的交叉口。osm里经常有多个节点表示同一个交叉口,这个参数能把它们合并成一个。--tls.guess-signals:根据道路等级和连接数自动推断信号灯位置。不是所有交叉口都需要信号灯,这个参数让netconvert智能判断。--tls.discard-simple:丢弃简单的信号灯,只保留复杂的。避免生成大量不必要的信号灯。--no-turnarounds:禁止掉头。除非你的仿真需要掉头,否则禁止掉头能减少大量无效连接。--offset.disable-normalization:禁用坐标归一化。保持原始坐标,方便后续与GIS数据对齐。--lefthand false:右侧通行。国内是右侧通行,这个必须设对。--speed-in-kmh:限速单位用km/h。osm里的maxspeed默认是km/h,但netconvert有时会按m/s处理,这个参数强制单位。--lane-numbers-from-osm:从osm的lanes tag读取车道数。如果不加这个,netconvert会用默认值。
4.2 道路类型映射与车道数推断
netconvert内置了一套osm道路类型到sumo道路类型的映射表,但默认映射不一定符合国内情况。比如osm的“trunk”在国内通常对应快速路,但netconvert可能把它当成高速公路处理,限速设得过高。
要自定义映射,需要用--type-files参数指定一个类型映射文件。这个文件是XML格式,定义了每种osm highway值对应的sumo道路类型、车道数、限速、优先级等。
一个典型的类型映射文件片段:
<types> <type name="primary" priority="10" numLanes="4" speed="60" /> <type name="secondary" priority="8" numLanes="3" speed="50" /> <type name="tertiary" priority="6" numLanes="2" speed="40" /> <type name="residential" priority="4" numLanes="2" speed="30" /> </types>使用时加上--type-files mytypes.xml即可。这样netconvert就会按照你的定义来生成路网,而不是用默认值。
注意:类型映射文件里的numLanes是默认车道数,如果osm的way上有lanes tag,且你加了
--lane-numbers-from-osm,那么osm的lanes会覆盖类型映射里的值。所以两个参数要配合使用。
4.3 交叉口连接生成与信号灯配置
netconvert最复杂也最容易出问题的部分是交叉口连接生成。一个交叉口有多条进口道和出口道,每条进口道的每个车道可以连接到哪些出口道,这决定了仿真时车辆能否正确转向。
默认情况下,netconvert会自动计算连接关系,但结果往往不理想。常见问题包括:左转车道没有左转连接、直行车道被分配到转弯连接、连接冲突导致死锁。
要精细控制连接,可以用--connection-files参数指定连接文件。但这个文件格式复杂,手动写很痛苦。我的做法是:先用netconvert自动生成,然后在sumo的netedit工具里可视化检查和修改。netedit能直观显示每个交叉口的连接关系,拖拽就能调整。
信号灯方面,--tls.guess-signals会自动推断,但推断结果需要人工校验。在netedit里可以打开信号灯编辑器,调整相位和配时。如果只是做路网构建,信号灯配时可以先用默认值,后续在sumo的additional文件里再细调。
4.4 生成路网的验证与常见错误处理
netconvert跑完后,用sumo-gui打开net.xml,检查几个关键点:
- 路网是否连通:随机选几个点,看能否找到路径。如果大量道路不连通,说明拓扑有问题。
- 车道数是否正确:对比原始shp的车道数,看是否有遗漏。
- 限速是否合理:检查主要道路的限速,是否与实际情况相符。
- 交叉口是否正常:看交叉口的连接线是否合理,有没有明显的缺失或多余。
常见的netconvert错误和解决方法:
| 错误信息 | 原因 | 解决方法 |
|---|---|---|
| "Could not find node" | osm里way引用了不存在的node | 用JOSM验证并修复 |
| "Duplicate edge" | 两条way的起点终点相同 | 删除重复way或合并 |
| "No connection between edges" | 交叉口连接生成失败 | 用netedit手动添加连接 |
| "Invalid lane number" | lanes tag值不是整数 | 修正osm里的lanes值 |
| "Speed out of range" | maxspeed值过大或过小 | 修正osm里的maxspeed值 |
实操心得:netconvert的日志会输出大量警告和错误,不要忽略它们。我一般会把日志重定向到文件,然后逐条检查。很多错误看起来不起眼,但会导致生成的路网在仿真时出现奇怪的行为,比如车辆突然消失、路径规划失败等。
5. 常见问题排查与避坑经验实录
5.1 坐标偏移与路网变形问题
这是最常见的问题之一。shp转osm时坐标系没转对,或者转换过程中精度损失,导致生成的路网整体偏移或者变形。
排查方法:在QGIS里同时加载原始shp和生成的net.xml(net.xml可以用sumo的netconvert转成shp,或者用sumo-gui截图后叠加),看是否重合。如果不重合,检查三个地方:shp的原始坐标系、QGIS导出时的目标坐标系、netconvert的offset参数。
我遇到过一次典型情况:shp用的是地方坐标系,带号搞错了,转成WGS84后整体偏移了几百米。后来在QGIS里重新指定了正确的带号才解决。所以坐标系这件事,一定要在第一步就确认清楚,不要等到最后才发现。
5.2 车道数丢失与属性映射错误
shp里的车道数字段是中文“车道数”,ogr2ogr转osm时生成的tag是<tag k="车道数" v="4"/>,netconvert不认识中文tag,直接忽略,结果所有道路都用默认车道数。
解决方法:在ogr2ogr转换后用脚本把中文tag键替换成英文标准tag。或者更彻底一点,在QGIS里先把字段名改成英文,再导出。
类似的还有限速字段、单双向字段。原则就是:osm的tag键必须是英文标准值,netconvert只认标准tag。
5.3 交叉口连接混乱与死锁
netconvert自动生成的交叉口连接,有时候会出现两个车道互相连接,形成死锁。仿真时车辆在交叉口里转圈,永远出不去。
这个问题在netedit里很容易发现:打开netedit,加载net.xml,看交叉口的连接线。如果看到连接线形成闭环,那就是死锁。解决方法是删除多余的连接,或者调整车道连接关系。
预防措施:在netconvert参数里加上--no-turnarounds和--junctions.join,能减少大部分死锁情况。另外,--check-lane-foes.all参数可以在生成时检查车道冲突,提前发现问题。
5.4 路网规模过大导致转换失败
如果shp覆盖整个城市,路网有几万条道路,netconvert可能会内存溢出或者跑几个小时都跑不完。
解决方法:分区域处理。在QGIS里按行政区或者网格把shp切成小块,每块单独转osm和net.xml,最后用sumo的netmerge工具合并。或者用--simplify参数简化路网,减少细节。
我处理过一个中等城市的案例,原始shp有8万多条道路。直接跑netconvert跑了40分钟还没完,后来切成12个区域,每个区域单独处理,总共20分钟搞定,而且每个区域的路网质量更好控制。
5.5 仿真时车辆无法上路或路径规划失败
路网生成后,在sumo里加载车流,发现车辆不动或者报“no route”错误。这通常是路网连通性问题。
排查步骤:
- 用sumo的
--net-file加载net.xml,在sumo-gui里看路网是否完整。 - 用
duarouter工具测试路径规划,随便选两个点,看能否找到路径。 - 检查net.xml里的edge是否有正确的from和to节点。
- 检查交叉口的连接是否允许车辆通过。
常见原因是交叉口连接缺失,导致某些方向无法通行。在netedit里补上连接即可。
实操心得:我习惯在netconvert之后,用sumo自带的
netcheck工具做一次全面检查。它会输出路网的统计信息和潜在问题,包括孤立节点、无效连接、车道数异常等。这个工具很多人不知道,但非常实用。
6. 从路网到仿真:后续扩展与优化建议
路网建好只是第一步,真正做仿真还需要加载车流、配置信号灯、设置检测器等。但路网的质量直接决定了后续仿真的可信度。如果路网本身有拓扑错误或者属性缺失,后面做再多调整都是白搭。
如果你需要更精细的路网,可以考虑在osm里补充更多tag:比如turn:lanes描述转向车道、traffic_signals描述信号灯位置、crossing描述人行横道。这些tag netconvert都能识别,生成的路网会更贴近现实。
另外,sumo的net.xml是可以手动编辑的,但我不建议直接改XML。更好的方式是用netedit可视化编辑,或者用sumo的additional文件在仿真时动态修改路网属性。这样原始net.xml保持干净,后续调整也方便。
这个流程我跑过不下几十次,从最初的到处报错到现在基本一次跑通,最大的体会就是:数据预处理的时间要占总时间的70%。shp的清洗、坐标系的确认、属性字段的映射,这些前期工作做扎实了,后面的转换和仿真就是水到渠成。反过来,如果前期偷懒,后面会在各种奇怪的地方卡住,而且排查起来非常痛苦。
最后分享一个小技巧:把整个流程写成脚本,从ogr2ogr转换到Python tag映射到netconvert参数,全部串起来。这样下次有新的shp数据,改几个路径就能直接跑,效率提升非常明显。我现在的脚本里还加了日志和错误检查,每一步的输出都记录,出问题能快速定位。这套流程跑顺了之后,一个中等规模的路网从shp到可仿真状态,半天时间足够了。