Hello. I’m an architectural designer at Differential, a small computational design studio, and we’ve just released Urban OpenGen, an open project that uses OpenStreetMap to study urban form at the scale of a few blocks. I want to share the part that’s relevant here, which is less the machine learning and more the renderer and what sixty cities of OSM data looked like when we tried to use them uniformly.
What we built

osm_tiles.py takes any centre or address and renders a four-class raster straight from Overpass: building footprints in grey, with the grey level encoding height, streets in red, planted ground in green, everything else black. Every pixel is exactly one class. A city is a 10 km window cut into a 25 by 25 grid: 625 tiles of 400 m at 128 px, so 3.125 m per pixel, plus the whole window as one 3200 by 3200 raster. render_cities.py runs a list of cities unattended and resumably.
The conventions are written next to every output rather than buried in the code: every street class is drawn 8 m wide, height comes from height or building:height, else building:levels × 3 m, else a 7 m default, green comes from the usual landuse, leisure and natural tags, and the draw order is green, building, street. The dataset is ODbL, © OpenStreetMap contributors.
The same renderer runs at training time and at inference time, which is the point: give it an address and you get tiles encoded exactly like the ones the models learned from.
What the data looked like across sixty cities
These are the findings I feel are useful to this forum on their own.
Fetching is the bottleneck, and it’s the server, not the query. Our first version split the bounding box whenever a mirror came back busy, which multiplied the load and made the throttling worse. The version that works asks /api/status for a free slot, rotates four mirrors with backoff, and splits a box only when the server says the query itself timed out. A city took from 4 minutes on a quiet day to nearly two hours on a loaded one, 29 hours of rendering for the sixty. Mumbai, Manila and Athens got 504s from the two main mirrors and were served by a third. Well past this scale, I think the honest answer is Geofabrik extracts parsed locally rather than Overpass.
Feature counts don’t measure coverage. Beijing returned 21,582 buildings and Jakarta 291,609, and both are fine: Beijing’s are 2,000 m² slabs, Jakarta’s are 600 m² houses. We judge a city by rendered coverage and buildings per tile instead.
Heights are the weak channel, and unevenly so. The share of footprints with neither height nor building:levels, which therefore sit at our 7 m default: Jakarta 84%, Beijing 52%, Dar es Salaam 43%, Berlin 19%. A model trained naively on this learns that Jakarta is flat. It’s the biggest data problem we hit.
Some cities aren’t mapped for this purpose yet. Luanda returned 1,787 buildings against 8,758 street segments: the network is there, the buildings mostly aren’t. It’s in the release as rendered, and it’s the first city we’d replace in a next version.
One pleasant surprise. An earlier version of the dataset came out of a Grasshopper3d pipeline with OSM. The same tiles rendered from OSM Overpass carry 77 to 118 distinct height levels where the old ones had 12 to 24. Overpass turned out to be the better source, not the fallback.
What we did with it
Two generative models. A GAN draws a whole 400 m tile conditioned on which of the sixty cities it should resemble. A diffusion model takes a real site with an area cut out and rebuilds that area so the streets and blocks continue into what’s around it. Both are coarse at 3 m per pixel and the documentation says so. That part is in the repository; I didn’t want to lead with it here.
Four questions I’d value this forum’s view on
- Heights. For cities where most footprints carry no height and no levels, what would you do? We see three options: drop those cities, mask the height loss for untagged buildings so the model never learns a fake flat, or keep them and document it. We haven’t decided. Is there a better default than levels × 3 m, and is there an ODbL-compatible height source worth fusing in?
- Street widths. We draw every class at a flat 8 m because it was the only thing we could apply uniformly across sixty cities. The renderer already has a per-class table, switched off: motorway 24 m, trunk 20, primary 16, secondary 13, tertiary 10, residential and unclassified 8, living street and pedestrian 6. Would you use
widthandlaneswhere they exist and fall back to a table like this, and is there a per-class default the community already agrees on that we should use instead of inventing one? - Which cities next? We have a list of 120 more, chosen for fabric type rather than size: medinas, Japanese and Korean fine grain, Soviet microrayon, colonial grids, Gulf superblocks, North American sprawl, Latin American hill cities. Oceania is our weakest continent. Where is the mapping good enough for a uniform render to mean something, and which fabric types are we obviously missing?
- Attribution. The datasets are ODbL with © OpenStreetMap contributors, and the models are trained on them. We read the community guideline on produced works and concluded that the raster tiles are a derivative database and the trained weights a produced work, and licensed accordingly. If anyone who knows this ground better thinks we’ve got it wrong, I’d rather hear it now than later.
Video explaining the process: https://www.youtube.com/watch?v=gjkFAR7kbEY
Code and documentation: GitHub - differential-studio/urban-opengen · GitHub
Dataset: differential-studio/urban-opengen-tiles-v2 · Datasets at Hugging Face