Files
boc/iom/visual_geolocation/similarity_search.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

298 lines
9.0 KiB
Python

"""
Similarity Search
Find similar images using embeddings for temporal geolocation
"""
from typing import Dict, List, Optional, Tuple
from dataclasses import dataclass
import numpy as np
@dataclass
class SimilarityMatch:
"""Similarity match result"""
image_id: str
similarity: float # 0-1
location: Optional[Tuple[float, float]]
timestamp: Optional[str]
metadata: Dict
class SimilaritySearch:
"""
Find similar images using embeddings
Uses:
- Visual embeddings (from CLIP)
- Scene embeddings
- Object embeddings
- Combined embeddings
"""
def __init__(self, embedding_dim: int = 512):
self.embedding_dim = embedding_dim
self.embeddings: Dict[str, np.ndarray] = {}
self.metadata: Dict[str, Dict] = {}
# In production, use FAISS or similar for efficient search
self.use_faiss = False
try:
import faiss
self.faiss_index = faiss.IndexFlatIP(embedding_dim) # Inner product for cosine similarity
self.use_faiss = True
except ImportError:
pass
def add_image(
self,
image_id: str,
embedding: np.ndarray,
location: Optional[Tuple[float, float]] = None,
timestamp: Optional[str] = None,
metadata: Dict = None
):
"""Add image to similarity index"""
# Normalize embedding for cosine similarity
embedding = embedding / np.linalg.norm(embedding)
self.embeddings[image_id] = embedding
self.metadata[image_id] = {
"location": location,
"timestamp": timestamp,
"metadata": metadata or {}
}
# Add to FAISS if available
if self.use_faiss:
self.faiss_index.add(embedding.reshape(1, -1))
def search(
self,
query_embedding: np.ndarray,
k: int = 5,
min_similarity: float = 0.7
) -> List[SimilarityMatch]:
"""
Search for similar images
Args:
query_embedding: Embedding of query image
k: Number of results to return
min_similarity: Minimum similarity threshold
Returns:
List of similarity matches
"""
# Normalize query embedding
query_embedding = query_embedding / np.linalg.norm(query_embedding)
if self.use_faiss and len(self.embeddings) > 0:
# Use FAISS for fast search
similarities, indices = self.faiss_index.search(
query_embedding.reshape(1, -1), k
)
results = []
image_ids = list(self.embeddings.keys())
for i, (sim, idx) in enumerate(zip(similarities[0], indices[0])):
if sim >= min_similarity and idx < len(image_ids):
image_id = image_ids[idx]
meta = self.metadata[image_id]
results.append(SimilarityMatch(
image_id=image_id,
similarity=float(sim),
location=meta["location"],
timestamp=meta["timestamp"],
metadata=meta["metadata"]
))
return results
else:
# Brute force search
return self._brute_force_search(query_embedding, k, min_similarity)
def _brute_force_search(
self,
query_embedding: np.ndarray,
k: int,
min_similarity: float
) -> List[SimilarityMatch]:
"""Brute force similarity search"""
results = []
for image_id, embedding in self.embeddings.items():
# Cosine similarity
similarity = np.dot(query_embedding, embedding)
if similarity >= min_similarity:
meta = self.metadata[image_id]
results.append(SimilarityMatch(
image_id=image_id,
similarity=float(similarity),
location=meta["location"],
timestamp=meta["timestamp"],
metadata=meta["metadata"]
))
# Sort by similarity
results.sort(key=lambda x: x.similarity, reverse=True)
return results[:k]
def find_temporal_matches(
self,
query_embedding: np.ndarray,
location: Tuple[float, float],
radius: float = 100.0, # meters
k: int = 5
) -> List[SimilarityMatch]:
"""
Find temporal matches at same location
Useful for detecting changes over time
"""
# First find all matches
all_matches = self.search(query_embedding, k=k * 2)
# Filter by location
temporal_matches = []
for match in all_matches:
if match.location:
# Calculate distance
distance = self._haversine_distance(
location[0], location[1],
match.location[0], match.location[1]
)
if distance <= radius:
temporal_matches.append(match)
return temporal_matches[:k]
def detect_changes(
self,
current_embedding: np.ndarray,
location: Tuple[float, float],
radius: float = 50.0
) -> Dict:
"""
Detect changes between current image and historical matches
Returns:
Dict with change analysis
"""
# Find historical matches
historical = self.find_temporal_matches(
current_embedding, location, radius, k=10
)
if not historical:
return {
"status": "no_history",
"message": "No historical observations at this location"
}
# Find best match
best_match = max(historical, key=lambda x: x.similarity)
# Calculate change score
# Lower similarity = more change
change_score = 1 - best_match.similarity
return {
"status": "change_detected",
"change_score": change_score,
"best_match": {
"image_id": best_match.image_id,
"similarity": best_match.similarity,
"timestamp": best_match.timestamp
},
"historical_count": len(historical),
"severity": "high" if change_score > 0.5 else "medium" if change_score > 0.3 else "low"
}
def _haversine_distance(self, lat1, lng1, lat2, lng2) -> float:
"""Calculate distance between two coordinates in meters"""
import math
R = 6371000 # Earth radius in meters
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
def test_similarity_search():
"""Test similarity search"""
print("=== Testing Similarity Search ===\n")
search = SimilaritySearch(embedding_dim=512)
# Add some historical images
np.random.seed(42)
for i in range(10):
embedding = np.random.randn(512)
search.add_image(
image_id=f"hist_{i:03d}",
embedding=embedding,
location=(13.7563 + np.random.randn() * 0.001,
100.5018 + np.random.randn() * 0.001),
timestamp=f"2026-03-{i+1:02d}T10:00:00Z",
metadata={"scene_type": "street_view"}
)
# Query with similar embedding (very close to hist_000)
query_embedding = search.embeddings["hist_000"] + np.random.randn(512) * 0.01
# Search
results = search.search(query_embedding, k=5)
print(f"Found {len(results)} similar images:")
for match in results:
print(f" - {match.image_id}: {match.similarity:.3f} similarity")
if match.location:
print(f" Location: {match.location}")
if match.timestamp:
print(f" Timestamp: {match.timestamp}")
print()
# Test temporal matching
temporal = search.find_temporal_matches(
query_embedding,
location=(13.7563, 100.5018),
radius=100
)
print(f"Found {len(temporal)} temporal matches at location")
# Test change detection
changes = search.detect_changes(
query_embedding,
location=(13.7563, 100.5018)
)
print(f"\nChange detection:")
print(f" Status: {changes['status']}")
if 'change_score' in changes:
print(f" Change score: {changes['change_score']:.3f}")
print(f" Severity: {changes['severity']}")
return results
if __name__ == '__main__':
test_similarity_search()