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",
|
"type": "string",
|
||||||
"description": "Geschaetzter Standort-Typ (z.B. Europa, Naher Osten, Nordamerika)"
|
"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": {
|
"landscape_clues": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -80,11 +98,12 @@ _VLM_SCHEMA = json.dumps({
|
|||||||
"signage_language": {"type": "string", "description": "Sprache auf Schildern falls erkennbar"},
|
"signage_language": {"type": "string", "description": "Sprache auf Schildern falls erkennbar"},
|
||||||
"vehicle_types": {"type": "string", "description": "Fahrzeugtypen und Fahrtrichtung (links/rechts)"},
|
"vehicle_types": {"type": "string", "description": "Fahrzeugtypen und Fahrtrichtung (links/rechts)"},
|
||||||
"climate_indicators": {"type": "string", "description": "Klimaindikatoren (Schnee, Wueste, tropisch, etc.)"},
|
"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 ---
|
# --- Objekt-Typ zu OverpassQL Mapping ---
|
||||||
@@ -141,6 +160,39 @@ _REGION_BBOX = {
|
|||||||
"central europe": (5, 45, 20, 56),
|
"central europe": (5, 45, 20, 56),
|
||||||
"deutschland": (5.8, 47.2, 15.1, 55.1),
|
"deutschland": (5.8, 47.2, 15.1, 55.1),
|
||||||
"germany": (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
|
||||||
"naher osten": (25, 12, 65, 42),
|
"naher osten": (25, 12, 65, 42),
|
||||||
"middle east": (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 = (
|
prompt = (
|
||||||
f"Lies die Bilddatei {image_path} mit dem Read-Tool. "
|
f"Lies die Bilddatei {image_path} mit dem Read-Tool. "
|
||||||
f"{context}"
|
f"{context}"
|
||||||
"Analysiere das Bild fuer GEOINT/OSINT-Zwecke. "
|
"PRIMAERZIEL: Bestimme den Aufnahmeort so praezise wie moeglich (Reverse Geolocation). "
|
||||||
"Identifiziere alle erkennbaren Objekte, Infrastruktur und militaerisch relevante Strukturen. "
|
"Schaetze in estimated_coordinates konkrete Lat/Lon-Werte und einen Unsicherheitsradius. "
|
||||||
"Gib fuer jedes Objekt passende OpenStreetMap-Tags an. "
|
"Nutze ALLE visuellen Hinweise: "
|
||||||
"Analysiere ausserdem Landschaftsmerkmale fuer Reverse Geolocation: "
|
"1) Gewaesser: Identifiziere den konkreten Fluss/See (Rhein, Donau, Elbe, etc.) anhand von Breite, Wasserfarbe, Uferform, Kiesbaenke, Stroemungsrichtung, Auenlandschaft. "
|
||||||
"Vegetation, Bodenfarbe, Strassenmarkierungen, Architekturstil, Schildersprache, "
|
"2) Vegetation: Baumart, Jahreszeit, Reifegrad. Schilf+Kies+Auwald am breiten Fluss = typisch Rhein/Niederrhein. "
|
||||||
"Fahrzeugtypen und Fahrtrichtung, Klimaindikatoren, Schattenrichtung. "
|
"3) Architektur, Schilder, Fahrzeuge, Infrastruktur im Hintergrund. "
|
||||||
"Fuelle das landscape_clues Objekt so detailliert wie moeglich. "
|
"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."
|
"Antworte ausschliesslich im vorgegebenen JSON-Format."
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -359,6 +414,8 @@ class QueryGenRequest(BaseModel):
|
|||||||
objects: list[dict]
|
objects: list[dict]
|
||||||
bbox: list[float] | None = None
|
bbox: list[float] | None = None
|
||||||
estimated_location_type: str | None = None
|
estimated_location_type: str | None = None
|
||||||
|
estimated_coordinates: dict | None = None
|
||||||
|
identified_features: dict | None = None
|
||||||
|
|
||||||
|
|
||||||
@router.post("/vlm/generate-queries")
|
@router.post("/vlm/generate-queries")
|
||||||
@@ -367,14 +424,46 @@ async def generate_queries(req: QueryGenRequest):
|
|||||||
if not req.objects:
|
if not req.objects:
|
||||||
raise HTTPException(400, "Keine Objekte angegeben")
|
raise HTTPException(400, "Keine Objekte angegeben")
|
||||||
|
|
||||||
# BBox bestimmen: explizit > Region > weltweit
|
# BBox bestimmen: explizite BBox > estimated_coordinates > identified_features > Region > weltweit
|
||||||
bbox = req.bbox
|
bbox = req.bbox
|
||||||
region_used = None
|
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:
|
if not bbox and req.estimated_location_type:
|
||||||
region_bbox = _resolve_region_bbox(req.estimated_location_type)
|
region_bbox = _resolve_region_bbox(req.estimated_location_type)
|
||||||
if region_bbox:
|
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
|
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 = ""
|
bbox_str = ""
|
||||||
if bbox and len(bbox) == 4:
|
if bbox and len(bbox) == 4:
|
||||||
|
|||||||
@@ -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 = '<div class="vlm-exif-card" style="border-color:rgba(0,255,136,0.3);background:rgba(0,255,136,0.06);margin-top:8px">' +
|
||||||
|
'<div style="font-size:9px;letter-spacing:1.5px;color:var(--accent);margin-bottom:4px">GESCHAETZTE POSITION</div>' +
|
||||||
|
'<div style="font-size:13px;color:var(--accent);font-weight:700">' + ec.latitude.toFixed(4) + ', ' + ec.longitude.toFixed(4) +
|
||||||
|
' <span style="font-size:11px;font-weight:400;color:var(--text-dim)">(±' + rKm + ' km)</span></div>' +
|
||||||
|
(ec.reasoning ? '<div style="font-size:10px;color:var(--text-dim);margin-top:4px">' + ec.reasoning + '</div>' : '') +
|
||||||
|
'</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Identifizierte Features
|
||||||
|
var featHtml = '';
|
||||||
|
if (data.identified_features) {
|
||||||
|
var if_ = data.identified_features;
|
||||||
|
var fp = [];
|
||||||
|
if (if_.water_body) fp.push('<b>Gewaesser:</b> ' + if_.water_body);
|
||||||
|
if (if_.specific_region) fp.push('<b>Region:</b> ' + if_.specific_region);
|
||||||
|
if (if_.nearby_landmarks) fp.push('<b>Landmarken:</b> ' + if_.nearby_landmarks);
|
||||||
|
if (fp.length > 0) {
|
||||||
|
featHtml = '<div style="font-size:9px;letter-spacing:1.5px;color:#00bcd4;margin:8px 0 4px">IDENTIFIZIERTE MERKMALE</div>' +
|
||||||
|
'<div style="font-size:11px;color:var(--text);line-height:1.8">' + fp.join('<br>') + '</div>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Szene
|
// Szene
|
||||||
var sceneEl = document.getElementById('vlm-scene');
|
var sceneEl = document.getElementById('vlm-scene');
|
||||||
if (sceneEl) {
|
if (sceneEl) {
|
||||||
sceneEl.innerHTML = exifHtml +
|
sceneEl.innerHTML = exifHtml + coordsHtml +
|
||||||
'<div style="margin-top:8px">' + (data.scene_description || 'Keine Beschreibung') +
|
'<div style="margin-top:8px">' + (data.scene_description || 'Keine Beschreibung') +
|
||||||
(data.terrain ? ' | Gelaende: ' + data.terrain : '') +
|
(data.terrain ? ' | Gelaende: ' + data.terrain : '') +
|
||||||
(data.estimated_location_type ? ' | Region: ' + data.estimated_location_type : '') +
|
(data.estimated_location_type ? ' | Region: ' + data.estimated_location_type : '') +
|
||||||
'</div>' + cluesHtml;
|
'</div>' + featHtml + cluesHtml;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Objekte
|
// 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 bbox = null;
|
||||||
|
var ec = analysis.estimated_coordinates;
|
||||||
|
var hasEstCoords = ec && ec.latitude && ec.longitude;
|
||||||
|
|
||||||
if (hasExifGps) {
|
if (hasExifGps) {
|
||||||
// 0.5 Grad um EXIF-Position
|
|
||||||
bbox = [
|
bbox = [
|
||||||
exif.latitude - 0.5,
|
exif.latitude - 0.5, exif.longitude - 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 {
|
} else {
|
||||||
var bboxCb = document.getElementById('vlm-use-bbox');
|
var bboxCb = document.getElementById('vlm-use-bbox');
|
||||||
if (bboxCb && bboxCb.checked && Globe.viewer) {
|
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');
|
var btn = document.getElementById('vlm-search-btn');
|
||||||
@@ -393,6 +432,8 @@ const VlmUI = {
|
|||||||
objects: selected,
|
objects: selected,
|
||||||
bbox: bbox,
|
bbox: bbox,
|
||||||
estimated_location_type: analysis.estimated_location_type || null,
|
estimated_location_type: analysis.estimated_location_type || null,
|
||||||
|
estimated_coordinates: analysis.estimated_coordinates || null,
|
||||||
|
identified_features: analysis.identified_features || null,
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
.then(function(r) {
|
.then(function(r) {
|
||||||
|
|||||||
In neuem Issue referenzieren
Einen Benutzer sperren