Files
boc/iom/visual_geolocation/temporal_analysis.py
T
Bernt bae705aa97 ARCHITECTURE: NFC roadmap, edge AI, audit logging
- 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
2026-06-29 16:24:48 +00:00

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()