Skip to main content
Glama
DeepPVMapper

DeepPVMapper

README.md
# DeepPVMapper

[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
[![HF Models](https://img.shields.io/badge/πŸ€—%20Models-bdappv--models-orange)](https://huggingface.co/gabrielkasmi/bdappv-models)
[![HF Dataset](https://img.shields.io/badge/πŸ€—%20Dataset-bdappv-orange)](https://huggingface.co/datasets/gabrielkasmi/bdappv)
[![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.21534856.svg)](https://doi.org/10.5281/zenodo.21534856)
[![DeepPVMapper MCP connector – tool definition quality and endpoint health on Glama](https://glama.ai/mcp/connectors/io.github.gabrielkasmi/deeppvmapper/badges/score.svg)](https://glama.ai/mcp/connectors/io.github.gabrielkasmi/deeppvmapper)

Large-scale rooftop PV detection pipeline for France. Classifies IGN aerial tiles with InceptionV3, segments positive patches with FCN/DeepLab, and extracts panel characteristics (surface, tilt, azimuth, installed capacity) via [pypvroof](https://pypi.org/project/pypvroof/).

**[πŸ—ΊοΈ Official project page](https://deeppvmapper.fr)**  
Work carried out by [Gabriel Kasmi](https://gabrielkasmi.github.io) as part of his PhD at Mines Paris-PSL (2020–2024).

---

## MCP Server

[![DeepPVMapper MCP server](https://glama.ai/mcp/servers/gabrielkasmi/deeppvmapper/badges/card.svg)](https://glama.ai/mcp/servers/gabrielkasmi/deeppvmapper)

DeepPVMapper's detection registry is also exposed as a public **MCP (Model Context Protocol) server** over Streamable HTTP, so any MCP-compatible client (Claude, etc.) can query the 1.14M+ rooftop-solar detections directly via natural language: search detections, aggregate installed capacity by department, explore an area, track yearly deployment, and check data-quality signals.

- **Live endpoint:** `https://zelhliylrlktnasircwp.supabase.co/functions/v1/mcp/mcp`
- **Server source:** [`supabase/functions/mcp`](https://github.com/gabrielkasmi/deeppvmapper/tree/main/supabase/functions/mcp)
- **Registry listing:** [registry.modelcontextprotocol.io](https://registry.modelcontextprotocol.io/v0.1/servers?search=io.github.gabrielkasmi/deeppvmapper) β€” `io.github.gabrielkasmi/deeppvmapper`

No install required β€” add the endpoint as a custom connector in any MCP client.

---

## Mapping data

Pre-computed detection results for French departments are available on Zenodo:

[![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.19188878.svg)](https://doi.org/10.5281/zenodo.19188878)

---

## Installation

GDAL must be installed system-wide first, then via pip to match the running Python version:

```bash
# Ubuntu/Debian
apt-get install -y gdal-bin libgdal-dev libopenjp2-7

pip install "GDAL==$(gdal-config --version)"
pip install -r requirements.txt
```

GPU required. CUDA 11.8+, 8 GB VRAM minimum.

---

## Data and model weights

Download model weights and runtime data from Zenodo:

[![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.7576814.svg)](https://doi.org/10.5281/zenodo.7576814)

Model weights are also available on Hugging Face:

[![HF Models](https://img.shields.io/badge/πŸ€—%20Models-bdappv--models-orange)](https://huggingface.co/gabrielkasmi/bdappv-models)

The training dataset (BDAPPV):

[![HF Dataset](https://img.shields.io/badge/πŸ€—%20Dataset-bdappv-orange)](https://huggingface.co/datasets/gabrielkasmi/bdappv)

Fill in the source paths in `config.yml` before running:

| config key | what goes there |
|---|---|
| `source_images_dir` | IGN JP2 tiles + `dalles.shp` index shapefile |
| `source_topo_dir` | BDTOPO folder (BATIMENT.shp, ZONE_D_ACTIVITE_OU_D_INTERET.shp) |
| `source_commune_dir` | folder containing `communes-20210101.shp` |
| `model_dir` | folder containing `model_bdappv_cls.pth` and `model_bdappv_seg.pth` |

---

## Usage

```bash
python main.py --dpt 06
```

`--count` sets tiles per classification batch (default 16 β€” reduce if OOM):

```bash
python main.py --dpt 06 --count 8
```

`--config` points to an alternative config file (useful for RunPod deployments):

```bash
python main.py --dpt 06 --config /workspace/config_runpod.yml
```

To process a subset of tiles (local testing), set `tiles_list` in `config.yml`:

```yaml
tiles_list:
  - 01-2024-0850-6565-LA93-0M20-E080
  - 01-2024-0850-6570-LA93-0M20-E080
```

Force a full rerun (wipe prior progress):

```bash
python main.py --dpt 06 --clean
```

---

## Pipeline

Four steps run sequentially inside `main.py`:

| step | what happens |
|---|---|
| **Init** | Builds per-department auxiliary files (buildings, plants, communes) into `temp/`. Skipped if already present β€” safe to rerun after a crash. |
| **Classification** | Tiles loaded fully in memory. InceptionV3 classifies 299Γ—299 patches; positives saved as GeoTIFFs to `temp/segmentation/`. |
| **Segmentation** | FCN/DeepLab segments each positive patch. LAMB93 polygons extracted, sorted by tile, merged into pseudo-arrays. |
| **Aggregation** | pypvroof extracts tilt/azimuth/kWp per polygon. Building filter applied. Results written to `outputs_dir`. |

**On success**: `temp/` is deleted automatically.  
**On crash**: `temp/` is kept. Rerun the same command to resume from where it stopped.

---

## Outputs

Written to `outputs_dir` (default: `data/`):

| file | description |
|---|---|
| `arrays_{dpt}.geojson` | Detected PV polygons in WGS84 |
| `characteristics_{dpt}.csv` | Per-installation registry: surface (mΒ²), tilt (Β°), azimuth (Β°), kWp, city code, lat, lon |
| `aggregated_characteristics_{dpt}.csv` | City-level aggregation: count, total kWp, avg surface, avg kWp |
| `arrays_characteristics_{dpt}.geojson` | Polygons enriched with all characteristics |

Only residential-scale installations (1.7–36.1 kWp) located on buildings are retained.

---

## Configuration reference

| parameter | default | description |
|---|---|---|
| `temp_dir` | `temp` | Working directory. Deleted on success, kept on crash. |
| `outputs_dir` | `data` | Final outputs directory. |
| `cls_threshold` | 0.4 | Classification confidence threshold |
| `cls_batch_size` | 512 | Patches per GPU batch (classification) |
| `decode_workers` | 3 | Concurrent JP2 decode processes feeding the GPU β€” tune to your real CPU quota, not host core count |
| `decode_stagger_s` | 35 | Gap between initial decode submissions, to avoid lockstep bursty waits β€” rule of thumb: decode_time / decode_workers |
| `seg_threshold` | 0.46 | Segmentation binarization threshold |
| `seg_batch_size` | 64 | Images per GPU batch (segmentation) |
| `filter_building` | True | Discard detections not on a building |
| `tilt_method` | `lut` | pypvroof tilt method (`lut` or `constant`) |
| `azimuth_method` | `bounding-box` | pypvroof azimuth method |
| `ic_method` | `clustered` | pypvroof installed-capacity regression type |
| `tiles_list` | *(empty)* | Optional tile subset for partial runs |

---

## Contributing

Contributions are welcome β€” both **code** (performance, new imagery sources, models, building filters) and **registry corrections** via the [interactive map](https://gabrielkasmi.github.io/deeppvmapper/content/map.html), no coding required.

See [CONTRIBUTING.md](CONTRIBUTING.md) for the contribution areas, setup instructions and workflow. Issues labelled [`good first issue`](https://github.com/gabrielkasmi/deeppvmapper/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) are the best entry points.

---

## Known issues

**GDAL is fragile to install, for two distinct reasons β€” and the fix below handles both.**

1. The pip `GDAL` binding must match the system `libgdal` version exactly. `pip install GDAL` fails to build, or segfaults at import, if its version differs from the system library.
2. On images that ship a pre-installed `python3-gdal` apt package (common on RunPod/cloud GPU images), that package bundles its own `osgeo/`, which takes priority over the pip-installed one in `sys.path` β€” and its `.so` is often broken, regardless of what pip installs.

Run this in place of a plain `pip install`, e.g. right when deploying the pipeline, around the `pip install -r requirements.txt` step:

```bash
#!/usr/bin/env bash
set -e

# --- native GDAL lib (gdal-config must exist before pip can build the python binding) ---
apt-get update
apt-get install -y --no-install-recommends gdal-bin libgdal-dev

# --- purge python3-gdal if present: this apt package ships its own osgeo/,
#     which takes priority over the pip-installed one in sys.path, and its
#     .so is often broken ---
dpkg -l | grep -q python3-gdal && apt-get remove --purge -y python3-gdal || true
rm -rf /usr/lib/python3/dist-packages/osgeo

# --- python deps ---
pip install -r requirements.txt

# --- repin the pip GDAL binding to exactly match the native lib version
#     (requirements.txt only pins GDAL>=3.0, so pip can grab a newer
#     version than the one apt just installed -> ABI mismatch at import) ---
GDAL_VERSION=$(gdal-config --version)
pip install --no-cache-dir --force-reinstall "GDAL==${GDAL_VERSION}"

# --- check ---
python -c "
import torch
from osgeo import gdal
print('torch', torch.__version__, '| cuda', torch.cuda.is_available())
print('gdal', gdal.__version__, '| GTiff driver:', gdal.GetDriverByName('GTiff') is not None)
"
```

---

## Citation

```bibtex
@phdthesis{kasmi2024enhancing,
  title={Enhancing the Reliability of Deep Learning Models to Improve the Observability of French Rooftop Photovoltaic Installations},
  author={Kasmi, Gabriel},
  year={2024},
  school={Universit{\'e} Paris sciences et lettres}
}
```

---

## License

[MIT](LICENSE)

TDQS

A4.2/5.0

Scored across 17 tools

Disambiguation4/5

Most tools have clearly distinct query targets, and descriptions explicitly cross-reference alternatives ('use aggregate_detection_capacity instead if...', 'use get_detections_in_bbox for spatial queries'). The main risk is the cluster of reference tools (get_data_quality_reference, get_field_reference, get_source_reference) and the department-level tools (get_department_capacity_stats, get_department_yearly_stats, get_department_profile), which overlap somewhat but are adequately differentiated in text.

Naming Consistency5/5

Every tool follows a consistent snake_case verb_noun pattern (get_*, search_*, aggregate_*, count_*), with the domain noun consistently indicating the resource or scope (detection, department, field, source, community). No mixed conventions or casing inconsistencies.

Tool Count4/5

17 tools is on the heavier end but each earns its place across distinct axes: entry/help, references, single-record lookup, filtered search, spatial search, counting, aggregation, distributions, coverage, and community activity. A couple of tools (e.g. get_department_profile) could arguably be folded into others, keeping it just short of ideal.

Completeness4/5

The read-only query surface is comprehensive: introduction/help, quality metadata, field and source references, single-record detail, filtered counts and searches, spatial bbox queries, department aggregations and trends, field distributions and coverage, plus unmoderated community activity. The only apparent gap is the absence of any write/contribution tool (e.g. submitting or moderating a correction), though this may be intentionally out of scope.

Maintenance

ActivityNo data
ResponsivenessSyncing