From 51e85b257241e6124f84ef614350b25ea1db50f7 Mon Sep 17 00:00:00 2001 From: Claude Dev Date: Thu, 26 Mar 2026 10:38:22 +0100 Subject: [PATCH] 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 --- src/data_vlm.py | 111 +++++++++++++++++++++++++++++++++++++++----- static/js/ui/vlm.js | 59 +++++++++++++++++++---- 2 files changed, 150 insertions(+), 20 deletions(-) diff --git a/src/data_vlm.py b/src/data_vlm.py index 286aa02..a23e859 100644 --- a/src/data_vlm.py +++ b/src/data_vlm.py @@ -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: diff --git a/static/js/ui/vlm.js b/static/js/ui/vlm.js index 3cbe910..005a26b 100644 --- a/static/js/ui/vlm.js +++ b/static/js/ui/vlm.js @@ -238,14 +238,41 @@ const VlmUI = { } } + // Geschaetzte Koordinaten + var coordsHtml = ''; + if (data.estimated_coordinates && data.estimated_coordinates.latitude) { + var ec = data.estimated_coordinates; + var rKm = ec.confidence_radius_km || '?'; + coordsHtml = '
' + + '
GESCHAETZTE POSITION
' + + '
' + ec.latitude.toFixed(4) + ', ' + ec.longitude.toFixed(4) + + ' (±' + rKm + ' km)
' + + (ec.reasoning ? '
' + ec.reasoning + '
' : '') + + '
'; + } + + // Identifizierte Features + var featHtml = ''; + if (data.identified_features) { + var if_ = data.identified_features; + var fp = []; + if (if_.water_body) fp.push('Gewaesser: ' + if_.water_body); + if (if_.specific_region) fp.push('Region: ' + if_.specific_region); + if (if_.nearby_landmarks) fp.push('Landmarken: ' + if_.nearby_landmarks); + if (fp.length > 0) { + featHtml = '
IDENTIFIZIERTE MERKMALE
' + + '
' + fp.join('
') + '
'; + } + } + // Szene var sceneEl = document.getElementById('vlm-scene'); if (sceneEl) { - sceneEl.innerHTML = exifHtml + + sceneEl.innerHTML = exifHtml + coordsHtml + '
' + (data.scene_description || 'Keine Beschreibung') + (data.terrain ? ' | Gelaende: ' + data.terrain : '') + (data.estimated_location_type ? ' | Region: ' + data.estimated_location_type : '') + - '
' + cluesHtml; + '' + featHtml + cluesHtml; } // Objekte @@ -350,16 +377,29 @@ const VlmUI = { }); } - // === BBox bestimmen: EXIF-GPS > Viewport-Checkbox > Region === + // === BBox bestimmen: EXIF-GPS > estimated_coordinates > Viewport > Region (Backend) === var bbox = null; + var ec = analysis.estimated_coordinates; + var hasEstCoords = ec && ec.latitude && ec.longitude; + if (hasExifGps) { - // 0.5 Grad um EXIF-Position bbox = [ - exif.latitude - 0.5, - exif.longitude - 0.5, - exif.latitude + 0.5, - exif.longitude + 0.5, + exif.latitude - 0.5, exif.longitude - 0.5, + exif.latitude + 0.5, exif.longitude + 0.5, ]; + } else if (hasEstCoords) { + // VLM-geschaetzte Koordinaten: Radius in Grad + var rKm = ec.confidence_radius_km || 50; + var rDeg = Math.max(0.1, Math.min(rKm / 111, 10)); + bbox = [ + ec.latitude - rDeg, ec.longitude - rDeg, + ec.latitude + rDeg, ec.longitude + rDeg, + ]; + // Zur geschaetzten Position fliegen + Globe.viewer.camera.flyTo({ + destination: Cesium.Cartesian3.fromDegrees(ec.longitude, ec.latitude, Math.max(50000, rKm * 2000)), + duration: 2, + }); } else { var bboxCb = document.getElementById('vlm-use-bbox'); if (bboxCb && bboxCb.checked && Globe.viewer) { @@ -373,7 +413,6 @@ const VlmUI = { ]; } } - // Kein explizites BBox? Region wird vom Backend aus estimated_location_type abgeleitet } var btn = document.getElementById('vlm-search-btn'); @@ -393,6 +432,8 @@ const VlmUI = { objects: selected, bbox: bbox, estimated_location_type: analysis.estimated_location_type || null, + estimated_coordinates: analysis.estimated_coordinates || null, + identified_features: analysis.identified_features || null, }), }) .then(function(r) {