feat: Praezisere Geolocation durch Koordinatenschaetzung + Feature-Erkennung

VLM-Schema:
- estimated_coordinates: Claude schaetzt Lat/Lon + Radius direkt
- identified_features: Konkreter Gewaessername, engste Region, Landmarken
- landscape_clues um water_characteristics erweitert

VLM-Prompt: Komplett ueberarbeitet
- Primaerziel ist jetzt Reverse Geolocation statt nur Objekterkennung
- Spezifische Anleitungen: Fluss-ID (Rhein vs Donau), Kiesfarbe, Uferform
- Explizite Aufforderung fuer Koordinatenschaetzung

BBox-Kaskade (Prioritaet):
1. EXIF-GPS (exakt)
2. estimated_coordinates + Radius (VLM-Schaetzung)
3. identified_features (Gewaesser/Region aus Matching)
4. estimated_location_type (grobe Region)
5. Weltweit (Fallback)

Regions-Mapping: 30+ neue Eintraege
- Deutsche Bundeslaender (NRW, Bayern, Hessen, etc.)
- Flussgebiete (Niederrhein, Oberrhein, Mosel, Elbe, etc.)
- Staedte (Duesseldorf, Koeln, Bonn, Neuss)

Frontend:
- Gruen hervorgehobene Koordinaten-Box mit Radius
- Identifizierte Features (Gewaesser, Region, Landmarken)
- Fly-To zur geschaetzten Position
Dieser Commit ist enthalten in:
Claude Dev
2026-03-26 10:38:22 +01:00
Ursprung 9709536036
Commit 51e85b2572
2 geänderte Dateien mit 150 neuen und 20 gelöschten Zeilen

Datei anzeigen

@@ -70,6 +70,24 @@ _VLM_SCHEMA = json.dumps({
"type": "string",
"description": "Geschaetzter Standort-Typ (z.B. Europa, Naher Osten, Nordamerika)"
},
"estimated_coordinates": {
"type": "object",
"properties": {
"latitude": {"type": "number", "description": "Geschaetzte Breite in Dezimalgrad (z.B. 51.2 fuer Duesseldorf-Region)"},
"longitude": {"type": "number", "description": "Geschaetzte Laenge in Dezimalgrad (z.B. 6.8 fuer Duesseldorf-Region)"},
"confidence_radius_km": {"type": "number", "description": "Unsicherheitsradius in km (z.B. 5 = sehr sicher, 50 = Region, 500 = nur Kontinent)"},
"reasoning": {"type": "string", "description": "Begruendung fuer die Koordinatenschaetzung"}
},
"description": "Beste Schaetzung der Aufnahmeposition basierend auf ALLEN visuellen Hinweisen. Versuche so praezise wie moeglich zu sein."
},
"identified_features": {
"type": "object",
"properties": {
"water_body": {"type": "string", "description": "Konkreter Name des Gewaessers falls bestimmbar (z.B. Rhein, Donau, Themse, Nil). Aus Breite, Uferform, Wasserfarbe, Umgebung ableiten."},
"specific_region": {"type": "string", "description": "Engstmoegliche Regionsbestimmung (z.B. Niederrhein, Oberrheingraben, Ruhrgebiet, Muenchner Schotterebene)"},
"nearby_landmarks": {"type": "string", "description": "Erkennbare Landmarken, Berge, Gebaeude, Infrastruktur in der Naehe"}
}
},
"landscape_clues": {
"type": "object",
"properties": {
@@ -80,11 +98,12 @@ _VLM_SCHEMA = json.dumps({
"signage_language": {"type": "string", "description": "Sprache auf Schildern falls erkennbar"},
"vehicle_types": {"type": "string", "description": "Fahrzeugtypen und Fahrtrichtung (links/rechts)"},
"climate_indicators": {"type": "string", "description": "Klimaindikatoren (Schnee, Wueste, tropisch, etc.)"},
"sun_shadow_direction": {"type": "string", "description": "Schattenrichtung falls erkennbar (z.B. N, NW)"}
"sun_shadow_direction": {"type": "string", "description": "Schattenrichtung falls erkennbar (z.B. N, NW)"},
"water_characteristics": {"type": "string", "description": "Gewaesser-Details: Breite, Farbe, Stroemung, Ufertyp, Kiesbank, Bewuchs"}
}
}
},
"required": ["scene_description", "objects"]
"required": ["scene_description", "objects", "estimated_coordinates"]
})
# --- Objekt-Typ zu OverpassQL Mapping ---
@@ -141,6 +160,39 @@ _REGION_BBOX = {
"central europe": (5, 45, 20, 56),
"deutschland": (5.8, 47.2, 15.1, 55.1),
"germany": (5.8, 47.2, 15.1, 55.1),
# Deutsche Regionen
"nordrhein-westfalen": (5.8, 50.3, 9.5, 52.6),
"nrw": (5.8, 50.3, 9.5, 52.6),
"niederrhein": (6.0, 51.0, 7.0, 52.0),
"rheinland": (6.0, 50.3, 7.5, 51.5),
"ruhrgebiet": (6.5, 51.2, 7.8, 51.7),
"bayern": (9.0, 47.2, 13.9, 50.6),
"bavaria": (9.0, 47.2, 13.9, 50.6),
"baden-wuerttemberg": (7.5, 47.5, 10.5, 49.8),
"hessen": (7.7, 49.4, 10.3, 51.7),
"niedersachsen": (6.5, 51.3, 11.6, 53.9),
"sachsen": (11.8, 50.2, 15.1, 51.7),
"berlin": (13.0, 52.3, 13.8, 52.7),
"hamburg": (9.7, 53.4, 10.3, 53.7),
"schleswig-holstein": (8.3, 53.3, 11.4, 55.1),
"rheinland-pfalz": (6.1, 48.9, 8.5, 50.9),
# Flussgebiete
"oberrhein": (7.0, 47.5, 8.5, 49.0),
"oberrheingraben": (7.0, 47.5, 8.5, 49.0),
"mittelrhein": (6.8, 49.8, 7.8, 50.5),
"rhein": (6.0, 47.5, 8.5, 52.0),
"rhine": (6.0, 47.5, 8.5, 52.0),
"donau": (8.0, 47.5, 17.0, 49.0),
"danube": (8.0, 47.5, 29.0, 48.5),
"elbe": (9.5, 50.5, 14.0, 54.0),
"mosel": (6.0, 49.0, 7.6, 50.4),
"main": (8.0, 49.5, 11.5, 50.3),
"weser": (8.5, 51.0, 9.8, 53.5),
"neuss": (6.6, 51.1, 6.9, 51.3),
"duesseldorf": (6.6, 51.1, 6.9, 51.3),
"koeln": (6.8, 50.8, 7.1, 51.1),
"cologne": (6.8, 50.8, 7.1, 51.1),
"bonn": (7.0, 50.6, 7.2, 50.8),
# Naher Osten
"naher osten": (25, 12, 65, 42),
"middle east": (25, 12, 65, 42),
@@ -229,13 +281,16 @@ async def _run_claude(image_path: str, viewport_info: str = "") -> dict:
prompt = (
f"Lies die Bilddatei {image_path} mit dem Read-Tool. "
f"{context}"
"Analysiere das Bild fuer GEOINT/OSINT-Zwecke. "
"Identifiziere alle erkennbaren Objekte, Infrastruktur und militaerisch relevante Strukturen. "
"Gib fuer jedes Objekt passende OpenStreetMap-Tags an. "
"Analysiere ausserdem Landschaftsmerkmale fuer Reverse Geolocation: "
"Vegetation, Bodenfarbe, Strassenmarkierungen, Architekturstil, Schildersprache, "
"Fahrzeugtypen und Fahrtrichtung, Klimaindikatoren, Schattenrichtung. "
"Fuelle das landscape_clues Objekt so detailliert wie moeglich. "
"PRIMAERZIEL: Bestimme den Aufnahmeort so praezise wie moeglich (Reverse Geolocation). "
"Schaetze in estimated_coordinates konkrete Lat/Lon-Werte und einen Unsicherheitsradius. "
"Nutze ALLE visuellen Hinweise: "
"1) Gewaesser: Identifiziere den konkreten Fluss/See (Rhein, Donau, Elbe, etc.) anhand von Breite, Wasserfarbe, Uferform, Kiesbaenke, Stroemungsrichtung, Auenlandschaft. "
"2) Vegetation: Baumart, Jahreszeit, Reifegrad. Schilf+Kies+Auwald am breiten Fluss = typisch Rhein/Niederrhein. "
"3) Architektur, Schilder, Fahrzeuge, Infrastruktur im Hintergrund. "
"4) Bodenfarbe, Gestein, Gelaendeform. Rheinischer Kies ist hell, Donauschlick dunkel. "
"5) Sonne/Schatten: Richtung und Laenge fuer Breitengrad-Schaetzung. "
"Identifiziere ausserdem Objekte und militaerisch relevante Strukturen mit OSM-Tags. "
"Fuelle identified_features (Gewaessername, Region, Landmarken) und landscape_clues komplett. "
"Antworte ausschliesslich im vorgegebenen JSON-Format."
)
@@ -359,6 +414,8 @@ class QueryGenRequest(BaseModel):
objects: list[dict]
bbox: list[float] | None = None
estimated_location_type: str | None = None
estimated_coordinates: dict | None = None
identified_features: dict | None = None
@router.post("/vlm/generate-queries")
@@ -367,14 +424,46 @@ async def generate_queries(req: QueryGenRequest):
if not req.objects:
raise HTTPException(400, "Keine Objekte angegeben")
# BBox bestimmen: explizit > Region > weltweit
# BBox bestimmen: explizite BBox > estimated_coordinates > identified_features > Region > weltweit
bbox = req.bbox
region_used = None
bbox_source = "explicit" if bbox else None
# 1. Geschaetzte Koordinaten mit Radius (praeziseste Quelle)
if not bbox and req.estimated_coordinates:
ec = req.estimated_coordinates
lat = ec.get("latitude")
lon = ec.get("longitude")
radius_km = ec.get("confidence_radius_km", 50)
if lat is not None and lon is not None:
# Radius in Grad umrechnen (1 Grad ~ 111 km)
radius_deg = max(0.1, min(radius_km / 111, 10))
bbox = [lat - radius_deg, lon - radius_deg, lat + radius_deg, lon + radius_deg]
bbox_source = f"coordinates ({lat:.2f}, {lon:.2f}, r={radius_km}km)"
logger.info(f"BBox aus estimated_coordinates: {lat}, {lon}, radius {radius_km}km -> {bbox}")
# 2. Identifizierte Features (Gewaesser, Region)
if not bbox and req.identified_features:
for key in ["specific_region", "water_body"]:
text = req.identified_features.get(key, "")
if text:
region_bbox = _resolve_region_bbox(text)
if region_bbox:
bbox = [region_bbox[1], region_bbox[0], region_bbox[3], region_bbox[2]]
region_used = text
bbox_source = f"feature: {key}={text}"
break
# 3. Fallback: estimated_location_type
if not bbox and req.estimated_location_type:
region_bbox = _resolve_region_bbox(req.estimated_location_type)
if region_bbox:
bbox = [region_bbox[1], region_bbox[0], region_bbox[3], region_bbox[2]] # S,W,N,E
bbox = [region_bbox[1], region_bbox[0], region_bbox[3], region_bbox[2]]
region_used = req.estimated_location_type
bbox_source = f"region: {req.estimated_location_type}"
if bbox_source:
logger.info(f"BBox-Quelle: {bbox_source}")
bbox_str = ""
if bbox and len(bbox) == 4: