La chaîne 100 % open data pour transformer des photos de rue libres en couches géo exploitables — sans contrat, sans quota, sans clause anti-IA.
Pour qui ? Data engineers, GIS analysts, développeurs OSM, collectivités et freelances géospatiaux qui veulent extraire de l'information terrain (commerces, panneaux, mobilier urbain, numéros de rue) à partir d'images de rue qu'ils ont légalement le droit de traiter par IA.
Prérequis : Python 3.10+, des bases en requêtes HTTP/JSON, une carte mentale du format GeoJSON. Pas besoin de GPU pour démarrer.
La tentation est naturelle : Google Street View couvre la planète, la qualité est excellente, et il existe une Static Street View API. Pourquoi se compliquer la vie avec une alternative ?
Parce que les conditions d'utilisation interdisent précisément ce que vous voulez faire. Les Google Maps Platform Terms prohibent le téléchargement en masse, la mise en cache des imageries au-delà du strict rendu, la création de jeux de données dérivés, et — point décisif depuis les clarifications de 2024-2025 — l'usage des contenus pour entraîner ou alimenter des modèles de machine learning. Faire tourner un détecteur d'objets sur des tuiles Street View pour en extraire des POI, c'est sortir du cadre contractuel.
S'ajoutent le coût (facturation au millier d'images, vite dissuasive sur une ville entière), l'absence de métadonnées exploitables (pas d'azimut fiable ni de paramètres caméra exposés), et la dépendance à un fournisseur qui peut couper l'accès du jour au lendemain.
💡 Insight #1 — La vraie barrière n'est pas technique
Détecter une devanture sur une image Street View est trivial. Le faire légalement, à l'échelle, et republier le résultat ne l'est pas. La licence est le mur, pas le modèle de vision.
Panoramax est le commun numérique de photos de rue géolocalisées, porté en France par l'IGN et OpenStreetMap France, et fondé sur le logiciel libre GeoVisio. C'est un réseau d'instances fédérées (l'instance IGN, l'instance OSM-FR, des instances de collectivités…) qui partagent une API commune.
Les points qui changent tout pour notre cas d'usage :
CC-BY-SA ou équivalent ouvert), qui autorisent la réutilisation et le traitement automatisé, attribution et partage à l'identique en contrepartie.💡 Insight #2 — Open data ≠ domaine public
CC-BY-SA vous donne le droit d'extraire et de republier, mais impose l'attribution et le partage à l'identique. Le dataset que vous produisez à partir de Panoramax hérite d'obligations. À anticiper avant la prod, pas après.
| Critère | Google Street View | Panoramax |
|---|---|---|
| Droit d'extraction par ML | Interdit par les ToS | Autorisé (licence ouverte) |
| Republication d'un dataset dérivé | Interdite | Autorisée (BY-SA → share-alike) |
| Coût d'accès aux images | Facturé au volume | Gratuit |
| Couverture mondiale | Quasi totale | Hétérogène, dense là où il y a des contributeurs |
| Métadonnées caméra / azimut | Limitées | Exposées via STAC |
| Floutage RGPD | Oui (rendu) | Oui (à la source, avant publication) |
| Fraîcheur | Passages épisodiques | Contributif, parfois très récent |
La conclusion est nette : sur la couverture brute, Google gagne. Sur le droit d'en faire quelque chose et de le republier, Panoramax est le seul terrain de jeu praticable. Et dans une logique de contribution OSM, les deux mondes se complètent naturellement.
On va dérouler chaque étage avec du code exécutable. Objectif fil rouge : cartographier automatiquement les commerces et panneaux d'une rue à partir des photos Panoramax.
# Vision + accès modèles Hugging Face
pip install transformers torch pillow requests
# Géospatial (sortie GeoParquet)
pip install geopandas pyarrow shapely
L'API suit la norme STAC. On cherche les images dans une bounding box via /api/search. Chaque feature renvoyée porte sa géométrie (un point), ses assets image et ses propriétés (azimut, date).
import requests
API = "https://api.panoramax.xyz/api" # instance centrale IGN
# bbox = lon_min, lat_min, lon_max, lat_max (ici un pâté de maisons)
bbox = [2.3500, 48.8560, 2.3540, 48.8585]
def search_pictures(bbox, limit=50):
params = {
"bbox": ",".join(map(str, bbox)),
"limit": limit,
}
r = requests.get(f"{API}/search", params=params, timeout=30)
r.raise_for_status()
return r.json()["features"]
features = search_pictures(bbox)
print(f"{len(features)} photos trouvées")
# Anatomie d'une feature STAC
f = features[0]
lon, lat = f["geometry"]["coordinates"]
azimuth = f["properties"].get("view:azimuth") # cap de la prise de vue, en degrés
img_url = f["assets"]["hd"]["href"] # aussi "sd", "thumb"
💡 Insight #3 — Pensez "séquences", pas "photos isolées"
Une même devanture apparaît sur plusieurs vues consécutives d'une collection (séquence). Traiter la séquence permet de dédupliquer et de trianguler la position d'un objet vu sous deux angles — bien plus robuste qu'une détection sur une image unique.
from PIL import Image
from io import BytesIO
def load_image(url):
r = requests.get(url, timeout=60)
r.raise_for_status()
return Image.open(BytesIO(r.content)).convert("RGB")
img = load_image(img_url)
Le pari gagnant en 2026, c'est le zero-shot object detection : on décrit les objets en langage naturel, sans réentraîner. OWLv2 (Google) accepte une liste de requêtes texte et renvoie des boîtes englobantes. Pas de dataset annoté à constituer.
from transformers import pipeline
detector = pipeline(
"zero-shot-object-detection",
model="google/owlv2-base-patch16-ensemble",
)
# On décrit ce qu'on cherche, en clair
labels = ["storefront sign", "traffic sign", "shop window", "street name plate"]
detections = detector(img, candidate_labels=labels)
# → liste de dicts : {score, label, box:{xmin,ymin,xmax,ymax}}
keep = [d for d in detections if d["score"] > 0.20]
for d in keep:
print(d["label"], round(d["score"], 2), d["box"])
💡 Insight #4 — Choisissez le bon modèle selon la tâche
Zero-shot pour découvrir sans annoter (OWLv2, Grounding DINO). Modèle fine-tuné pour la production à fort volume (plus rapide, plus précis sur un domaine étroit). Segmentation (SegFormer, Mask2Former) si vous voulez la surface et pas juste la boîte. Le pipeline ne change pas, seul le modèle change.
Une boîte « street name plate » ne sert à rien sans son contenu. On recadre la détection et on passe le crop à un modèle d'OCR. TrOCR de Microsoft est un bon défaut sur Hugging Face ; pour des plaques de rue multilignes, un VLM léger fait souvent mieux.
from transformers import pipeline
ocr = pipeline("image-to-text", model="microsoft/trocr-base-printed")
def read_box(img, box):
crop = img.crop((box["xmin"], box["ymin"], box["xmax"], box["ymax"]))
return ocr(crop)[0]["generated_text"]
for d in keep:
if d["label"] in ("street name plate", "storefront sign"):
d["text"] = read_box(img, d["box"])
print(d["label"], "→", d["text"])
C'est l'étape qui sépare un POC d'un livrable. Une détection est une boîte en pixels ; il faut une coordonnée géographique. Approximation pragmatique et robuste : la position de la caméra + l'azimut de la prise de vue + le décalage horizontal de l'objet dans l'image (qui donne un angle relatif) → on projette un point à une distance estimée le long de cet azimut corrigé.
import math
def project_detection(lon, lat, azimuth_deg, box, img_w,
hfov_deg=70, distance_m=8):
"""Estime la position au sol d'un objet détecté.
hfov_deg : champ horizontal de la caméra. distance_m : profondeur supposée."""
cx = (box["xmin"] + box["xmax"]) / 2
# angle relatif par rapport au centre de l'image (-hfov/2 .. +hfov/2)
rel_angle = (cx / img_w - 0.5) * hfov_deg
bearing = (azimuth_deg + rel_angle) % 360
R = 6378137.0 # rayon terrestre (m)
d = distance_m / R
b = math.radians(bearing)
lat1, lon1 = math.radians(lat), math.radians(lon)
lat2 = math.asin(math.sin(lat1)*math.cos(d) +
math.cos(lat1)*math.sin(d)*math.cos(b))
lon2 = lon1 + math.atan2(math.sin(b)*math.sin(d)*math.cos(lat1),
math.cos(d) - math.sin(lat1)*math.sin(lat2))
return math.degrees(lon2), math.degrees(lat2)
💡 Insight #5 — La distance est votre plus grande incertitude
Sans profondeur (depth map ou triangulation multi-vues),distance_mest une hypothèse. Pour de la donnée fiable, triangulez le même objet sur 2+ vues de la séquence : l'intersection des deux azimuts donne une position bien plus juste qu'une distance devinée.
import geopandas as gpd
from shapely.geometry import Point
rows = []
for f in features:
lon, lat = f["geometry"]["coordinates"]
az = f["properties"].get("view:azimuth", 0)
img = load_image(f["assets"]["sd"]["href"])
for d in detector(img, candidate_labels=labels):
if d["score"] < 0.20: continue
plon, plat = project_detection(lon, lat, az, d["box"], img.width)
rows.append({
"label": d["label"], "score": d["score"],
"src_picture": f["id"], "geometry": Point(plon, plat),
})
gdf = gpd.GeoDataFrame(rows, crs="EPSG:4326")
gdf.to_parquet("detections_rue.parquet") # GeoParquet, lisible par DuckDB/QGIS
Vous avez maintenant une couche géo de points typés, chacun traçable jusqu'à la photo source — prête pour QGIS, DuckDB Spatial, ou une revue avant import OSM.
src_picture.Contexte : Une intercommunalité doit recenser panneaux, bancs, abris-bus et bornes sur 40 km de voirie.
Chaîne : captation Panoramax par les agents → détection zero-shot multi-classes → triangulation → GeoParquet → contrôle visuel dans QGIS.
Gain : on remplace des semaines de relevé terrain par une captation roulante + une passe IA. Le relevé humain ne sert plus qu'à valider, pas à inventorier.
Contexte : Un mappeur veut compléter les shop=* d'un quartier.
Chaîne : Panoramax → détection « storefront sign » → OCR de l'enseigne → croisement avec les POI OSM existants pour ne garder que les absents.
Règle d'or ici : l'IA propose, le mappeur dispose. Jamais d'import automatique en masse dans OSM — chaque suggestion passe par une validation humaine, photo à l'appui.
Contexte : Une enseigne veut cartographier les concurrents d'une zone de chalandise.
Chaîne : Panoramax → détection enseignes → OCR + classification du nom → géolocalisation → tableau de bord.
Erreur à éviter : usage interne = pas de souci de share-alike, mais l'attribution Panoramax reste due. Et ne stockez pas d'images non floutées : vous récupérez déjà du contenu RGPD-safe, ne le dégradez pas.
| Couche | Outil | Pourquoi |
|---|---|---|
| Source images | API Panoramax (STAC) | Libre, géolocalisé, floutage RGPD à la source |
| Découverte / annotation | OWLv2, Grounding DINO (HF) | Zero-shot, aucun dataset à constituer |
| Production volume | Modèle fine-tuné + ONNX / HF Endpoints | Débit et précision sur domaine étroit |
| OCR | TrOCR / VLM léger (HF) | Lecture enseignes et plaques de rue |
| Géo-traitement | GeoPandas + Shapely | Rétro-projection, jointures spatiales |
| Stockage / requête | GeoParquet + DuckDB Spatial | Cloud-native, lisible par QGIS |
| Validation humaine | QGIS / éditeur OSM | L'IA propose, l'humain valide |
panoramax.fr — présentation, instances, application mobiledocs.panoramax.fr — endpoints search / collections / itemshuggingface.co/models (OWLv2, Grounding DINO, TrOCR, SegFormer)geoparquet.org