How green is your city? It's a simple question, but surprisingly hard to answer objectively. Cities often claim to offer abundant green spaces, but official statistics typically only count public parks - missing private gardens, street trees, green roofs, and all the vegetation that doesn't appear on municipal registers.
In 2016, Berlin's Morgenpost published a groundbreaking investigation that changed how I thought about this problem. Using 185 Landsat satellite images, they analyzed all 79 German cities with over 100,000 inhabitants and discovered that Siegen - not Berlin or Hamburg - was Germany's greenest city with 86% vegetation coverage. The methodology was revolutionary: measure all vegetation from space, not just what's on paper.
I wanted to build something similar for the rest of Europe, but with modern satellite data and as an ongoing monitoring system rather than a one-time snapshot.
The core idea is straightforward: download satellite imagery, calculate vegetation indices, and compare cities using the same methodology. The implementation required solving several challenges.
Higher resolution data: Sentinel-2 provides 10-meter pixels compared to Landsat's 30-meter resolution. This means we can see individual trees and small gardens that were previously invisible. For urban analysis, this resolution difference is significant.
Consistent boundaries: Comparing cities requires consistent administrative boundaries. We use GADM (Global Administrative Areas) data, supplemented with Eurostat LAU boundaries for countries where GADM lacks municipality-level detail. Each city gets a unique identifier that traces back to its source data.
Vegetation measurement: We calculate NDVI (Normalized Difference Vegetation Index) from the red and near-infrared bands. Pixels with NDVI above 0.4 are classified as vegetation. The percentage of vegetated pixels gives us a comparable "green score" for each city.
# Simplified vegetation calculation
NDVI = (NIR - RED) / (NIR + RED)
vegetation_mask = NDVI > 0.4
green_percentage = vegetation_mask.sum() / total_pixels * 100
The platform currently covers 201 cities across all 27 EU member states (and growing), including all national capitals. Processing each city requires downloading satellite granules, clipping to city boundaries, generating cloud-free composites, and calculating statistics.
| Metric | Value |
|---|---|
| Cities analyzed | 201 |
| Countries covered | 27 (all EU) |
| Total area | ~46,000 km2 |
| Satellite resolution | 10 meters |
| Years available | 2018, 2025 |
One of the first analyses we ran was ranking all 27 EU capital cities. The results challenge some assumptions about which cities are "green":
| Rank | Capital | Vegetation % |
|---|---|---|
| 1 | Ljubljana, Slovenia | 75.7% |
| 2 | Zagreb, Croatia | 69.8% |
| 3 | Vilnius, Lithuania | 63.2% |
| 4 | Sofia, Bulgaria | 51.6% |
| 5 | Bratislava, Slovakia | 51.5% |
| ... | ... | ... |
| 25 | Athens, Greece | 3.5% |
| 26 | Nicosia, Cyprus | 2.3% |
| 27 | Valletta, Malta | 0.2% |
Ljubljana's dominance isn't surprising given Slovenia's geography - the city sits in a basin surrounded by forests. But the data reveals patterns: the top 5 capitals are all from Central and Eastern Europe. Nordic capitals like Stockholm and Copenhagen rank 14th and 20th respectively - city boundaries matter as much as urban planning choices.
Building a system that can process hundreds of cities across multiple years required a robust pipeline. The architecture follows a "clip-first" approach that dramatically reduces storage requirements:
# Pipeline stages (auto-discovery based on metadata status)
searched → downloaded → clipped → composited → tiled → stats → web
# Storage comparison (177 cities, 4 years)
Old approach (full tiles): ~3 TB
Clip-first approach: ~100 GB
Each Sentinel-2 granule is ~1 GB, but we only need the pixels within city boundaries. By clipping immediately after download, we can delete the raw data and keep just the city extracts. This makes the difference between needing a data center and running everything from a desktop with an external drive.
The pipeline is written in Python, using GDAL for raster processing and my raster-calc tool for optimized NDVI calculation. Web tiles are generated at zoom levels 8-14 and served from Cloudflare R2.
Urban vegetation is a topic that matters to citizens, not just researchers. The platform supports 25 languages (English, Spanish, Portuguese, German, French, Italian, Dutch, Polish) with a static site generator that builds localized versions from Jinja2 templates.
The frontend uses Leaflet.js for mapping, with city boundaries served as PMTiles (a single-file format for vector tiles). This means the entire boundary dataset for 201 cities fits in 7.5 MB, enabling fast initial load times.
All data outputs are licensed under CC BY 4.0. Journalists, researchers, and citizens can use, share, and build on the work with attribution.
The current release is a foundation. Planned improvements include:
The platform is live at green-city-index.org. You can explore the interactive map, compare cities, and read data stories about urban vegetation patterns across Europe.
This project was developed in collaboration with Julia Nogue, who contributed the editorial vision, contextual research, and multi-language content. The satellite data comes from the European Space Agency's Copernicus program, and administrative boundaries from GADM and Eurostat.