Continuity-based Road Extraction from OSM
Reconstruct lane-level road and intersection polygons from OpenStreetMap (OSM) centerline data for Delft, Netherlands.
This repository contains the source code of the ConRE-OSM tool, developed as part of a thesis study using road centerline data from OpenStreetMap for the city of Delft, Netherlands. ConRE-OSM reconstructs topologically continuous, lane-level road surface polygons and intersection areas directly from OSM way geometry, without requiring additional surveyed or manually prepared inputs. The pipeline derives lane offsets, corrects geometric connectivity at forks and bi-directional junctions, and generates semantically labelled road and intersection polygons that preserve the continuity and topology of the underlying street network.
This repository accompanies the MSc thesis:
Rao, C. (2024). Reconstructing a high-detailed 2D areal representation of road network based on OSM data [Master's thesis, Delft University of Technology]. TU Delft Repository. https://resolver.tudelft.nl/uuid:0bae72e4-ff16-4921-8f6e-d92fa4a377d7
If you use this work ConRE-OSM, please cite the following:
@mastersthesis{rao2024roadreconstructing,
author = {Rao, Chengzhi},
title = {Reconstructing a high-detailed 2D areal representation of road network based on OSM data},
school = {Delft University of Technology},
year = {2024},
type = {Master's thesis},
note = {Faculty of Architecture and the Built Environment, Programme: Geomatics},
url = {https://resolver.tudelft.nl/uuid:0bae72e4-ff16-4921-8f6e-d92fa4a377d7}
}@software{conre_osm_2026,
author = {[Chengzhi Rao]},
title = {{ConRE-OSM}: Continuity-based Road Extraction from OSM},
year = {2026},
url = {[repository URL]},
license = {GPL-3.0-or-later}
}The following works informed the design of ConRE-OSM:
- osmnx — Boeing, G. (2017). OSMnx: New Methods for Acquiring, Constructing, Analyzing, and Visualizing Complex Street Networks. Computers, Environment and Urban Systems, 65, 126–139. https://doi.org/10.1016/j.compenvurbsys.2017.05.004
- momepy — Fleischmann, M. (2019). momepy: Urban Morphology Measuring Toolkit. Journal of Open Source Software, 4(43), 1807. https://doi.org/10.21105/joss.01807
- GeoPandas — Jordahl, K. et al. (2014). GeoPandas: Python tools for geographic data. https://github.com/geopandas/geopandas
- Shapely — Gillies, S. et al. (2007). Shapely: manipulation and analysis of geometric objects. https://github.com/shapely/shapely
- COINS — Tripathy, P., et al. (2020). An open-source tool to extract natural continuity and hierarchy of urban street networks. Environment and Planning B. https://doi.org/10.1177/2399808320967680
- StreetGen — Martin, S., et al. (2015). StreetGen: In-base, city-scale procedural generation of streets. Proceedings of the Eurographics Workshop on Urban Data Modelling and Visualisation.
- A/B Street — https://github.com/a-b-street/abstreet
- OpenStreetMap — OpenStreetMap contributors (2017). Planet dump retrieved from https://planet.osm.org, https://www.openstreetmap.org
| File | Description | Rows |
|---|---|---|
data/output/road_intersections.geojson |
Intersection polygons | 108 |
data/output/road_segments.geojson |
Road polygons with intersections removed | 329 |
All files use EPSG:28992 (RD New, metres).
pip install -r requirements.txtPython 3.10+. Place your OSM export at:
data/input/delft_osm_ways.geojson
This is the only external input required. It should be an OSM ways export in WGS84 (EPSG:4326) covering the study area. Filtering to the required highway types and reprojection to EPSG:28992 happen automatically inside the pipeline.
python run.py # run all 5 stages
python run.py --stages 1 2 # run only stages 1 and 2
python run.py --from 3 # run stages 3 through 5
python run.py --no-mid # skip saving mid-results (faster)All output paths are defined at the top of run.py. Change them there to relocate data.
data/input/delft_osm_ways.geojson ──► Stage 1, Stage 4, Stage 5
Stage 1 ──► data/output/stage/s1_road_attributes.geojson ──► Stage 2, 3, 4
Stage 2 ──► data/output/stage/s2_lane_centerlines.geojson ──► Stage 3
──► data/output/stage/s2_lane_polygons.geojson ──► Stage 3
──► data/output/stage/s2_lane_boundaries.geojson ──► Stage 3
──► data/output/stage/s2_road_dict.json ──► Stage 3
Stage 3 ──► data/output/stage/s3_lane_segments.geojson ──► Stage 4, Stage 5
Stage 4 ──► data/output/stage/s4_road_polygons.geojson ──► Stage 5
──► data/output/stage/s4_network_nodes.geojson ──► Stage 5
Stage 5 ──► data/output/road_intersections.geojson (FINAL)
──► data/output/road_segments.geojson (FINAL)
Input: data/input/delft_osm_ways.geojson
Stage result → stages 2, 3, 4:
| File | Rows | Role |
|---|---|---|
data/output/stage/s1_road_attributes.geojson |
554 | Road attribute table: lane counts, turn:lanes, bearings, group_index, oneway, fork_type per road segment |
Mid results (data/output/mid/):
| File | After which step |
|---|---|
s1_cycle_extract.geojson |
Cycleway extraction from parent-road attributes |
s1_backward_split.geojson |
Bi-directional roads split into _forward/_backward |
s1_turn_normalized.geojson |
turn:lanes normalised to pipe-separated strings |
s1_grouped.geojson |
Initial grouping (superseded by stage result) |
Input: data/output/stage/s1_road_attributes.geojson
Stage results → stage 3:
| File | Description |
|---|---|
data/output/stage/s2_lane_centerlines.geojson |
Offset lane centerlines (id_raw, lane_type, lane_order, geometry) |
data/output/stage/s2_lane_polygons.geojson |
Buffered lane polygons, same structure |
data/output/stage/s2_lane_boundaries.geojson |
Two outermost boundary lines per road |
data/output/stage/s2_road_dict.json |
Per-road lane data (393 entries): ids, types, turns, offsets, widths, groups |
Mid results: none.
Inputs: s1_road_attributes.geojson + all s2_* files (stage 2)
Stage result → stages 4, 5:
| File | Rows | Role |
|---|---|---|
data/output/stage/s3_lane_segments.geojson |
659 | Per-lane metadata with corrected geometry: id_raw, id_lane, type, highway, order, turn_lanes, offset, width, fork_type, geometry |
Mid results (data/output/mid/):
| File | Contents |
|---|---|
s3_fork_aligned.geojson |
Fork lane centerlines after start-node alignment |
s3_fork_buffered.geojson |
Buffered fork polygons |
s3_cycle_lines.geojson |
Cycleway centerlines after endpoint alignment |
s3_cycle_polys.geojson |
Cycleway polygons after alignment |
s3_pre_bifork_lines.geojson |
Lane centerlines after fork+cycle+accessor fixes, pre-bi-fork pass |
s3_pre_bifork_polys.geojson |
Same state, polygons |
s3_final_lane_lines.geojson |
Final lane centerlines — all passes complete |
s3_final_lane_polys.geojson |
Final lane polygons |
s3_final_boundaries.geojson |
Final road boundary lines |
Inputs: data/input/delft_osm_ways.geojson + s1_road_attributes.geojson + s3_lane_segments.geojson
degree_true(corrected intersection degree for interior nodes) is computed directly from the road graph geometry — no additional external file required.
Stage results → stage 5:
| File | Rows | Role |
|---|---|---|
data/output/stage/s4_network_nodes.geojson |
408 | Network nodes with degree (momepy count) and degree_true (corrected for interior nodes). Stage 5 overrides degree with degree_true where set. |
data/output/stage/s4_road_polygons.geojson |
395 | Per-road polygons after all trim passes. Stage 5 clips these by intersection polygons. |
Mid results (data/output/mid/):
| File | After which trim pass |
|---|---|
s4_trim_degree2.geojson |
Degree-2 pseudo-intersection node trim |
s4_trim_bikelanes.geojson |
Mixed vehicle+cycleway boundary trim |
s4_fixed_polys.geojson |
Merged state after pseudo + mixed trims |
s4_endpoint_trim.geojson |
Degree-3 dead-end stub trim |
s4_fixed_polys2.geojson |
State after endpoint trim |
s4_multitype_trim.geojson |
Final multi-type trim (cycle + vehicle at same node) |
Inputs: data/input/delft_osm_ways.geojson + s4_road_polygons.geojson + s4_network_nodes.geojson + s3_lane_segments.geojson
Final outputs:
| File | Rows | Contents |
|---|---|---|
data/output/road_intersections.geojson |
108 | Intersection polygons (vehicle and cycleway, including roundabouts), each buffered 0.005 m |
data/output/road_segments.geojson |
329 | Road polygons with intersections removed; type, width, lanes, fork_type, bridge, tunnel, junction, name, oneway |
Mid results: none.
| Type | Location | Description | Can delete? |
|---|---|---|---|
| Stage result | data/output/stage/ |
Required input for the next stage | No, until downstream stage completes |
| Mid result | data/output/mid/ |
Inspection snapshots for debugging | Yes, after verification |
| Final output | data/output/ |
End product of the pipeline | Keep |
ConRE-OSM/
├── run.py ← CLI entry point; all paths defined here
├── README.md
├── LICENSE ← GPL-3.0-or-later (code)
├── LICENSE-DATA ← CC BY-NC 4.0 (output data)
├── requirements.txt
├── pipeline/
│ ├── stage1_centerlines.py
│ ├── stage2_lane_offset.py
│ ├── stage3_lane_geometry.py
│ ├── stage4_road_polygon.py
│ └── stage5_intersections.py
└── data/
├── input/
│ └── delft_osm_ways.geojson ← place your OSM export here
└── output/
├── stage/ ← stage results (auto-created)
├── mid/ ← mid results (auto-created)
├── road_intersections.geojson
└── road_segments.geojson
Code — GNU General Public License v3.0 or later (GPL-3.0-or-later) See LICENSE · Full text
Output data — Creative Commons Attribution-NonCommercial 4.0 International (CC BY-NC 4.0) See LICENSE-DATA · Full text
Input data — © OpenStreetMap contributors, Open Database License (ODbL 1.0) Any use of the output data must retain OSM attribution.