bae705aa97
- Add NFC ePassport roadmap (ICAO 9303, eIDAS) - Add TensorFlow.js edge face detection (BlazeFace) - Add structured audit logger (GDPR-compliant) - Risk scoring support Part of KYC Apple Native UX v1.1.0
443 lines
16 KiB
Python
443 lines
16 KiB
Python
"""
|
|
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()
|