|
|
""" |
|
|
Mapillary Finder — Minimal MVP |
|
|
============================== |
|
|
Goal: Start slow. Take an **address** string, geocode it, search Mapillary near that spot, |
|
|
and show the nearest images (thumbnails + metadata). |
|
|
|
|
|
What you get |
|
|
------------ |
|
|
- Text input: address (e.g., "College Station, TX, USA") |
|
|
- Button: "Find in Mapillary" |
|
|
- Output: |
|
|
- The resolved coordinates of the address |
|
|
- A small table of the **nearest Mapillary images** (id, distance, timestamp, bearing) |
|
|
- A **thumbnail gallery** of those images |
|
|
|
|
|
Requirements |
|
|
------------ |
|
|
- Environment variable: `MAPILLARY_TOKEN` (long‑lived token from your Mapillary account) |
|
|
- `pip install -r requirements.txt` where requirements.txt contains: |
|
|
gradio>=4.44.0 |
|
|
requests |
|
|
geopy |
|
|
numpy |
|
|
|
|
|
Run |
|
|
--- |
|
|
python app.py |
|
|
|
|
|
Notes |
|
|
----- |
|
|
- We query Graph API v4 `/images` using a **tiny bounding box** around the geocoded point. |
|
|
- You can increase the search radius in `pad_meters` if coverage is sparse. |
|
|
- We prefer 2048px thumbnails when available; fall back to 1024. |
|
|
""" |
|
|
|
|
|
import os |
|
|
import math |
|
|
import json |
|
|
from typing import List, Tuple |
|
|
from pathlib import Path |
|
|
|
|
|
import gradio as gr |
|
|
import requests |
|
|
import numpy as np |
|
|
from geopy.geocoders import Nominatim |
|
|
|
|
|
MAPILLARY_TOKEN = os.getenv("MAPILLARY_TOKEN", "") |
|
|
|
|
|
|
|
|
|
|
|
def geocode(address: str) -> Tuple[float, float]: |
|
|
geo = Nominatim(user_agent="mapillary-finder-mvp") |
|
|
res = geo.geocode(address) |
|
|
if not res: |
|
|
raise RuntimeError("Could not geocode that address. Try a more specific query.") |
|
|
return float(res.latitude), float(res.longitude) |
|
|
|
|
|
|
|
|
def pad_bbox(lat: float, lon: float, pad_meters: float = 80.0) -> Tuple[float, float, float, float]: |
|
|
|
|
|
dlat = pad_meters / 111_111.0 |
|
|
dlon = pad_meters / (111_111.0 * math.cos(math.radians(lat))) |
|
|
return (lon - dlon, lat - dlat, lon + dlon, lat + dlat) |
|
|
|
|
|
|
|
|
def haversine_km(lat1, lon1, lat2, lon2) -> float: |
|
|
R = 6371.0088 |
|
|
p1 = math.radians(lat1) |
|
|
p2 = math.radians(lat2) |
|
|
dp = p2 - p1 |
|
|
dl = math.radians(lon2 - lon1) |
|
|
a = math.sin(dp/2)**2 + math.cos(p1)*math.cos(p2)*math.sin(dl/2)**2 |
|
|
return 2*R*math.asin(math.sqrt(a)) |
|
|
|
|
|
|
|
|
def mapillary_search_bbox(bbox, limit=50): |
|
|
if not MAPILLARY_TOKEN: |
|
|
raise RuntimeError("MAPILLARY_TOKEN not set. Add it as an environment variable.") |
|
|
url = "https://graph.mapillary.com/images" |
|
|
fields = [ |
|
|
"id", |
|
|
"thumb_2048_url", |
|
|
"thumb_1024_url", |
|
|
"computed_geometry", |
|
|
"captured_at", |
|
|
"compass_angle", |
|
|
"camera_type", |
|
|
"sequence", |
|
|
] |
|
|
params = { |
|
|
"access_token": MAPILLARY_TOKEN, |
|
|
"fields": ",".join(fields), |
|
|
"limit": min(limit, 200), |
|
|
"bbox": f"{bbox[0]},{bbox[1]},{bbox[2]},{bbox[3]}", |
|
|
} |
|
|
r = requests.get(url, params=params, timeout=60) |
|
|
r.raise_for_status() |
|
|
return r.json().get("data", []) |
|
|
|
|
|
|
|
|
def find_nearest_images(address: str, pad_m: float = 80.0, topk: int = 12): |
|
|
lat, lon = geocode(address) |
|
|
bbox = pad_bbox(lat, lon, pad_m) |
|
|
items = mapillary_search_bbox(bbox, limit=100) |
|
|
|
|
|
if not items: |
|
|
return ( |
|
|
f"📍 {address}\nGeocoded to lat={lat:.6f}, lon={lon:.6f}.\n\nNo Mapillary images found in ~{int(pad_m)} m. Try increasing the radius.", |
|
|
None, |
|
|
None, |
|
|
) |
|
|
|
|
|
rows = [] |
|
|
thumbs = [] |
|
|
for it in items: |
|
|
geom = it.get("computed_geometry", {}) |
|
|
coords = geom.get("coordinates") if geom else None |
|
|
if not coords or len(coords) != 2: |
|
|
continue |
|
|
ilon, ilat = float(coords[0]), float(coords[1]) |
|
|
dist_km = haversine_km(lat, lon, ilat, ilon) |
|
|
thumb = it.get("thumb_2048_url") or it.get("thumb_1024_url") |
|
|
rows.append({ |
|
|
"id": it.get("id"), |
|
|
"lat": round(ilat, 6), |
|
|
"lon": round(ilon, 6), |
|
|
"distance_m": int(dist_km * 1000), |
|
|
"captured_at": it.get("captured_at"), |
|
|
"compass": it.get("compass_angle"), |
|
|
"camera_type": it.get("camera_type"), |
|
|
"thumb": thumb, |
|
|
}) |
|
|
|
|
|
|
|
|
rows.sort(key=lambda r: r["distance_m"]) |
|
|
top = rows[:topk] |
|
|
|
|
|
|
|
|
gallery = [r["thumb"] for r in top if r.get("thumb")] |
|
|
|
|
|
|
|
|
pretty = [ |
|
|
{k: v for k, v in r.items() if k != "thumb"} |
|
|
for r in top |
|
|
] |
|
|
info = ( |
|
|
f"📍 **{address}** → lat={lat:.6f}, lon={lon:.6f}\n" |
|
|
f"Found {len(rows)} images within ~{int(pad_m)} m. Showing {len(pretty)} nearest." |
|
|
) |
|
|
return info, json.dumps(pretty, indent=2), gallery |
|
|
|
|
|
|
|
|
with gr.Blocks(title="Mapillary Finder — Minimal", fill_height=True) as demo: |
|
|
gr.Markdown(""" |
|
|
# 🗺️ Mapillary Finder — Minimal |
|
|
Enter an **address**. I’ll geocode it and fetch the **nearest Mapillary images** in a small radius. |
|
|
Use this to verify coverage before attempting reconstruction. |
|
|
""") |
|
|
|
|
|
with gr.Row(): |
|
|
with gr.Column(scale=1): |
|
|
addr = gr.Textbox(label="Address", value="College Station, TX, USA") |
|
|
radius = gr.Slider(40, 300, step=10, value=80, label="Search radius (meters)") |
|
|
k = gr.Slider(4, 24, step=1, value=12, label="Max images to show") |
|
|
btn = gr.Button("Find in Mapillary", variant="primary") |
|
|
msg = gr.Markdown("Ready.") |
|
|
with gr.Column(scale=1): |
|
|
table = gr.Code(label="Nearest images (JSON)") |
|
|
gallery = gr.Gallery(label="Thumbnails", columns=[4], height="auto") |
|
|
|
|
|
|
|
|
|
|
|
def _run(a, r, topk): |
|
|
try: |
|
|
return find_nearest_images(a, r, int(topk)) |
|
|
except Exception as e: |
|
|
return f"Error: {e}", None, None |
|
|
|
|
|
btn.click(_run, inputs=[addr, radius, k], outputs=[msg, table, gallery]) |
|
|
|
|
|
if __name__ == "__main__": |
|
|
demo.launch(server_name="0.0.0.0", server_port=int(os.getenv("PORT", 7860))) |
|
|
|