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:
111
src/data_vlm.py
111
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:
|
||||
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren