""" Temporal Analysis Compare multiple observations of the same location over time """ import sys sys.path.insert(0, '/home/bernt/.openclaw/workspace/iom') from typing import Dict, List, Optional, Tuple from dataclasses import dataclass from datetime import datetime import numpy as np from visual_geolocation.evidence_extractor import ( EvidencePackage, ImageMetadata, VisualObject, TextDetection, GeometricFeature, EnvironmentalSignal ) @dataclass class TemporalComparison: """Comparison between two observations""" observation_id_1: str observation_id_2: str time_delta: float # hours # Changes detected new_objects: List[Dict] removed_objects: List[Dict] changed_objects: List[Dict] # Stability metrics structural_stability: float # 0-1 activity_change: float # 0-1 lighting_change: float # 0-1 # Reality gap evolution rgi_delta: float # Change in RGI class TemporalAnalyzer: """ Analyze temporal changes between observations Use cases: - Detect infrastructure degradation - Monitor construction progress - Track urban changes - Validate maintenance effectiveness """ def __init__(self): self.observations: Dict[str, EvidencePackage] = {} def add_observation(self, observation_id: str, evidence: EvidencePackage): """Add observation to temporal database""" self.observations[observation_id] = evidence def compare_observations( self, obs_id_1: str, obs_id_2: str ) -> TemporalComparison: """Compare two observations""" obs1 = self.observations[obs_id_1] obs2 = self.observations[obs_id_2] # Calculate time delta time_delta = self._calculate_time_delta(obs1, obs2) # Compare visual objects new_objects, removed_objects, changed_objects = self._compare_objects( obs1.visual_objects, obs2.visual_objects ) # Calculate stability metrics structural_stability = self._calculate_structural_stability( obs1, obs2, new_objects, removed_objects ) activity_change = self._calculate_activity_change(obs1, obs2) lighting_change = self._calculate_lighting_change(obs1, obs2) # Calculate RGI delta rgi_delta = self._calculate_rgi_delta(obs1, obs2) return TemporalComparison( observation_id_1=obs_id_1, observation_id_2=obs_id_2, time_delta=time_delta, new_objects=new_objects, removed_objects=removed_objects, changed_objects=changed_objects, structural_stability=structural_stability, activity_change=activity_change, lighting_change=lighting_change, rgi_delta=rgi_delta ) def _calculate_time_delta(self, obs1: EvidencePackage, obs2: EvidencePackage) -> float: """Calculate time difference in hours""" # Simplified: assume observations are close in time # In production, parse timestamps return 0.5 # 30 minutes def _compare_objects( self, objects1: List[VisualObject], objects2: List[VisualObject] ) -> Tuple[List[Dict], List[Dict], List[Dict]]: """Compare visual objects between observations""" new_objects = [] removed_objects = [] changed_objects = [] # Find new objects (in obs2 but not obs1) labels1 = {obj.label for obj in objects1} labels2 = {obj.label for obj in objects2} for obj in objects2: if obj.label not in labels1: new_objects.append({ "label": obj.label, "confidence": obj.confidence, "bbox": obj.bbox }) # Find removed objects (in obs1 but not obs2) for obj in objects1: if obj.label not in labels2: removed_objects.append({ "label": obj.label, "confidence": obj.confidence, "bbox": obj.bbox }) # Find changed objects (same label, different position/confidence) for obj1 in objects1: for obj2 in objects2: if obj1.label == obj2.label: confidence_change = abs(obj1.confidence - obj2.confidence) if confidence_change > 0.1: changed_objects.append({ "label": obj1.label, "confidence_change": confidence_change, "old_confidence": obj1.confidence, "new_confidence": obj2.confidence }) return new_objects, removed_objects, changed_objects def _calculate_structural_stability( self, obs1: EvidencePackage, obs2: EvidencePackage, new_objects: List[Dict], removed_objects: List[Dict] ) -> float: """Calculate structural stability (0-1)""" # Structural objects: buildings, street lights, roads, etc. structural_labels = {"building", "street_light", "road", "sidewalk", "bridge"} structural1 = {obj.label for obj in obs1.visual_objects if obj.label in structural_labels} structural2 = {obj.label for obj in obs2.visual_objects if obj.label in structural_labels} if not structural1: return 1.0 # Calculate intersection common = structural1 & structural2 stability = len(common) / len(structural1) return stability def _calculate_activity_change(self, obs1: EvidencePackage, obs2: EvidencePackage) -> float: """Calculate activity change (0-1)""" # Activity objects: cars, people, motorcycles, etc. activity_labels = {"car", "person", "motorcycle", "tuk-tuk", "bicycle"} activity1 = len([obj for obj in obs1.visual_objects if obj.label in activity_labels]) activity2 = len([obj for obj in obs2.visual_objects if obj.label in activity_labels]) if activity1 == 0 and activity2 == 0: return 0.0 max_activity = max(activity1, activity2) change = abs(activity1 - activity2) / max_activity return change def _calculate_lighting_change(self, obs1: EvidencePackage, obs2: EvidencePackage) -> float: """Calculate lighting change (0-1)""" # Compare environmental signals lighting1 = None lighting2 = None for signal in obs1.environmental_signals: if signal.signal_type == "lighting": lighting1 = signal.value for signal in obs2.environmental_signals: if signal.signal_type == "lighting": lighting2 = signal.value if not lighting1 or not lighting2: return 0.0 # Check if day/night changed is_night1 = lighting1.get("is_night", False) is_night2 = lighting2.get("is_night", False) if is_night1 != is_night2: return 1.0 # Day/night change is maximum change return 0.0 def _calculate_rgi_delta(self, obs1: EvidencePackage, obs2: EvidencePackage) -> float: """Calculate change in Reality Gap Index""" # Simplified: compare number of defects/anomalies # In production, calculate full RGI for both defects1 = len([obj for obj in obs1.visual_objects if obj.confidence < 0.5]) defects2 = len([obj for obj in obs2.visual_objects if obj.confidence < 0.5]) return defects2 - defects1 def analyze_location_history( self, location: Tuple[float, float], radius: float = 50.0 ) -> Dict: """Analyze all observations at a location""" # Find observations near location nearby_observations = [] for obs_id, evidence in self.observations.items(): if evidence.metadata.gps_lat and evidence.metadata.gps_lng: distance = self._haversine_distance( location[0], location[1], evidence.metadata.gps_lat, evidence.metadata.gps_lng ) if distance <= radius: nearby_observations.append((obs_id, evidence)) if len(nearby_observations) < 2: return { "status": "insufficient_data", "message": f"Only {len(nearby_observations)} observations at this location" } # Sort by timestamp nearby_observations.sort(key=lambda x: x[1].timestamp) # Compare consecutive observations comparisons = [] for i in range(len(nearby_observations) - 1): obs_id_1 = nearby_observations[i][0] obs_id_2 = nearby_observations[i + 1][0] comparison = self.compare_observations(obs_id_1, obs_id_2) comparisons.append(comparison) # Calculate trends avg_stability = np.mean([c.structural_stability for c in comparisons]) avg_activity_change = np.mean([c.activity_change for c in comparisons]) avg_rgi_delta = np.mean([c.rgi_delta for c in comparisons]) return { "status": "success", "observation_count": len(nearby_observations), "comparisons": len(comparisons), "trends": { "structural_stability": avg_stability, "activity_variability": avg_activity_change, "rgi_trend": avg_rgi_delta }, "latest_observation": nearby_observations[-1][0], "recommendations": self._generate_recommendations( avg_stability, avg_activity_change, avg_rgi_delta ) } def _generate_recommendations( self, stability: float, activity_change: float, rgi_delta: float ) -> List[str]: """Generate recommendations based on trends""" recommendations = [] if stability < 0.8: recommendations.append("Structural changes detected - inspect infrastructure") if activity_change > 0.5: recommendations.append("High activity variability - monitor during different times") if rgi_delta > 0: recommendations.append("Reality gap increasing - maintenance needed") elif rgi_delta < 0: recommendations.append("Reality gap decreasing - improvements working") return recommendations def _haversine_distance(self, lat1, lng1, lat2, lng2) -> float: """Calculate distance between coordinates""" import math R = 6371000 phi1 = math.radians(lat1) phi2 = math.radians(lat2) delta_phi = math.radians(lat2 - lat1) delta_lambda = math.radians(lng2 - lng1) a = math.sin(delta_phi / 2) ** 2 + \ math.cos(phi1) * math.cos(phi2) * math.sin(delta_lambda / 2) ** 2 c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a)) return R * c # Example usage with Bangkok images def analyze_bangkok_images(): """Analyze all three Bangkok images""" print("=" * 60) print("BANGKOK TEMPORAL ANALYSIS") print("=" * 60) analyzer = TemporalAnalyzer() # Create evidence for three Bangkok images # Image 1: Tuk-tuk, street view evidence1 = EvidencePackage( image_id="bangkok_001", timestamp=datetime(2026, 6, 26, 20, 0, 0), metadata=ImageMetadata(gps_lat=13.7563, gps_lng=100.5018), visual_objects=[ VisualObject("tuk-tuk", 0.85, [100, 300, 200, 400]), VisualObject("street_light", 0.80, [50, 50, 100, 400]), VisualObject("building", 0.90, [200, 100, 500, 400]) ], semantic_objects=[], text_detections=[], geometric_features=[], environmental_signals=[ EnvironmentalSignal("lighting", {"is_night": True}, 0.95) ], temporal_signals={} ) # Image 2: Truck, motorcycles evidence2 = EvidencePackage( image_id="bangkok_002", timestamp=datetime(2026, 6, 26, 20, 15, 0), metadata=ImageMetadata(gps_lat=13.7565, gps_lng=100.5020), visual_objects=[ VisualObject("truck", 0.88, [50, 250, 250, 450]), VisualObject("motorcycle", 0.82, [300, 350, 350, 400]), VisualObject("street_light", 0.85, [50, 50, 100, 400]), VisualObject("building", 0.92, [200, 100, 500, 400]) ], semantic_objects=[], text_detections=[], geometric_features=[], environmental_signals=[ EnvironmentalSignal("lighting", {"is_night": True}, 0.95) ], temporal_signals={} ) # Image 3: Car, restaurant evidence3 = EvidencePackage( image_id="bangkok_003", timestamp=datetime(2026, 6, 26, 20, 30, 0), metadata=ImageMetadata(gps_lat=13.7564, gps_lng=100.5019), visual_objects=[ VisualObject("car", 0.78, [100, 300, 200, 400]), VisualObject("street_light", 0.85, [50, 50, 100, 400]), VisualObject("building", 0.92, [200, 100, 500, 400]), VisualObject("sign", 0.88, [300, 50, 450, 150]) ], semantic_objects=[], text_detections=[ TextDetection("Sukhumvit Road", 0.92, [50, 50, 250, 100]), TextDetection("Sainokuni", 0.88, [300, 60, 450, 120]) ], geometric_features=[], environmental_signals=[ EnvironmentalSignal("lighting", {"is_night": True}, 0.95) ], temporal_signals={} ) # Add to analyzer analyzer.add_observation("bangkok_001", evidence1) analyzer.add_observation("bangkok_002", evidence2) analyzer.add_observation("bangkok_003", evidence3) # Compare images print("\nšŸ“Š COMPARISON: Image 1 vs Image 2") comp1 = analyzer.compare_observations("bangkok_001", "bangkok_002") print(f" Time delta: {comp1.time_delta}h") print(f" New objects: {len(comp1.new_objects)}") for obj in comp1.new_objects: print(f" - {obj['label']}") print(f" Removed objects: {len(comp1.removed_objects)}") for obj in comp1.removed_objects: print(f" - {obj['label']}") print(f" Structural stability: {comp1.structural_stability:.1%}") print(f" Activity change: {comp1.activity_change:.1%}") print("\nšŸ“Š COMPARISON: Image 2 vs Image 3") comp2 = analyzer.compare_observations("bangkok_002", "bangkok_003") print(f" Time delta: {comp2.time_delta}h") print(f" New objects: {len(comp2.new_objects)}") for obj in comp2.new_objects: print(f" - {obj['label']}") print(f" Removed objects: {len(comp2.removed_objects)}") for obj in comp2.removed_objects: print(f" - {obj['label']}") print(f" Structural stability: {comp2.structural_stability:.1%}") print(f" Activity change: {comp2.activity_change:.1%}") # Analyze location history print("\nšŸ“ˆ LOCATION HISTORY ANALYSIS") history = analyzer.analyze_location_history((13.7564, 100.5019), radius=100) print(f" Observations: {history['observation_count']}") print(f" Comparisons: {history['comparisons']}") print(f" Structural stability: {history['trends']['structural_stability']:.1%}") print(f" Activity variability: {history['trends']['activity_variability']:.1%}") print(f" RGI trend: {history['trends']['rgi_trend']:+.1f}") print("\nšŸŽÆ RECOMMENDATIONS") for rec in history['recommendations']: print(f" - {rec}") return history if __name__ == '__main__': analyze_bangkok_images()