| """ |
| 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))) |
|
|