""" Benchmark cross-modal retrieval approaches. Tests: 1. Single-index with modality filtering 2. Multi-index search 3. Hybrid search 4. Cross-modal alignment with projection heads """ import torch import numpy as np import time from pathlib import Path from typing import Dict, List, Tuple # Add src to path import sys sys.path.insert(0, str(Path(__file__).parent)) from src.features.cross_modal import CrossModalAligner, CrossModalConfig from src.retrieval.cross_modal_retrieval import CrossModalRetrieval def load_gallery_data(): """Load real gallery embeddings and metadata.""" import json data_dir = Path("data/processed") # Load embeddings embeddings = torch.load(data_dir / "gallery_embeddings.pt", weights_only=True) # Load metadata with open(data_dir / "gallery_metadata.json") as f: metadata = json.load(f) return embeddings.numpy().astype(np.float32), metadata def split_by_modality(embeddings: np.ndarray, metadata: List[dict]) -> Dict[str, np.ndarray]: """Split embeddings by modality.""" modalities = {} for i, entry in enumerate(metadata): mod = entry["modality"] if mod not in modalities: modalities[mod] = [] modalities[mod].append(embeddings[i]) return {mod: np.array(embs) for mod, embs in modalities.items()} def compute_recall_at_k(retrieved_indices: List[int], ground_truth_idx: int, k: int) -> float: """Compute Recall@K.""" if ground_truth_idx in retrieved_indices[:k]: return 1.0 return 0.0 def benchmark_single_index( embeddings: np.ndarray, metadata: List[dict], n_queries: int = 50, k: int = 5 ) -> Dict: """Benchmark single-index approach.""" print("\n=== Single-Index Benchmark ===") retrieval = CrossModalRetrieval(embed_dim=embeddings.shape[1]) retrieval.build_single_index(embeddings, [m["modality"] for m in metadata], metadata) # Generate queries (use gallery images as queries) query_indices = np.random.choice(len(embeddings), n_queries, replace=False) results = { "same_modal": [], "cross_modal": [], } for idx in query_indices: query = embeddings[idx:idx+1] query_mod = metadata[idx]["modality"] query_class = metadata[idx]["class"] # Same-modal search same_result = retrieval.search(query, query_mod, target_modality=query_mod, k=k) same_recall = compute_recall_at_k( [metadata[i]["class"] for i in same_result.indices], query_class, k ) results["same_modal"].append(same_recall) # Cross-modal search (find different modality with same class) cross_targets = [m for m in ["optical", "sar", "multispectral"] if m != query_mod] for target_mod in cross_targets: cross_result = retrieval.search(query, query_mod, target_mod, k=k) cross_recall = compute_recall_at_k( [metadata[i]["class"] for i in cross_result.indices], query_class, k ) results["cross_modal"].append(cross_recall) return { "same_modal_recall@k": np.mean(results["same_modal"]), "cross_modal_recall@k": np.mean(results["cross_modal"]), } def benchmark_multi_index( embeddings_by_mod: Dict[str, np.ndarray], metadata: List[dict], n_queries: int = 50, k: int = 5 ) -> Dict: """Benchmark multi-index approach.""" print("\n=== Multi-Index Benchmark ===") # Build metadata by modality metadata_by_mod = {} for entry in metadata: mod = entry["modality"] if mod not in metadata_by_mod: metadata_by_mod[mod] = [] metadata_by_mod[mod].append(entry) retrieval = CrossModalRetrieval(embed_dim=768) retrieval.build_multi_index(embeddings_by_mod, metadata_by_mod) results = { "same_modal": [], "cross_modal": [], } # Generate queries all_embeddings = np.concatenate(list(embeddings_by_mod.values())) query_indices = np.random.choice(len(all_embeddings), n_queries, replace=False) for idx in query_indices: query = all_embeddings[idx:idx+1] # Determine query modality offset = 0 query_mod = None for mod, embs in embeddings_by_mod.items(): if idx < offset + len(embs): query_mod = mod break offset += len(embs) if query_mod is None: continue # Same-modal search same_result = retrieval.search(query, query_mod, target_modality=query_mod, k=k) same_recall = 1.0 if any(True for _ in same_result.indices) else 0.0 results["same_modal"].append(same_recall) # Cross-modal search cross_targets = [m for m in embeddings_by_mod.keys() if m != query_mod] cross_result = retrieval.search(query, query_mod, k=k) cross_recall = 1.0 if any(True for _ in cross_result.indices) else 0.0 results["cross_modal"].append(cross_recall) return { "same_modal_recall@k": np.mean(results["same_modal"]), "cross_modal_recall@k": np.mean(results["cross_modal"]), } def benchmark_hybrid( embeddings_by_mod: Dict[str, np.ndarray], metadata: List[dict], n_queries: int = 50, k: int = 5 ) -> Dict: """Benchmark hybrid search approach.""" print("\n=== Hybrid Search Benchmark ===") metadata_by_mod = {} for entry in metadata: mod = entry["modality"] if mod not in metadata_by_mod: metadata_by_mod[mod] = [] metadata_by_mod[mod].append(entry) retrieval = CrossModalRetrieval(embed_dim=768) retrieval.build_multi_index(embeddings_by_mod, metadata_by_mod) results = [] all_embeddings = np.concatenate(list(embeddings_by_mod.values())) query_indices = np.random.choice(len(all_embeddings), n_queries, replace=False) for idx in query_indices: query = all_embeddings[idx:idx+1] offset = 0 query_mod = None for mod, embs in embeddings_by_mod.items(): if idx < offset + len(embs): query_mod = mod break offset += len(embs) if query_mod is None: continue result = retrieval.search_hybrid(query, query_mod, k=k) results.append(1.0 if len(result.indices) > 0 else 0.0) return { "hybrid_recall@k": np.mean(results), } def benchmark_cross_modal_alignment( embeddings: np.ndarray, metadata: List[dict], n_queries: int = 50, k: int = 5 ) -> Dict: """Benchmark cross-modal alignment with projection heads.""" print("\n=== Cross-Modal Alignment Benchmark ===") config = CrossModalConfig( embed_dim=embeddings.shape[1], projection_dim=256, use_wavelength_encoding=True, use_domain_adaptation=True, ) aligner = CrossModalAligner(config) # Project all embeddings projected = {} for mod in ["optical", "sar", "multispectral"]: mask = [m["modality"] == mod for m in metadata] mod_embeddings = embeddings[mask] projected[mod] = aligner.project( torch.tensor(mod_embeddings), mod ).detach().numpy() results = { "same_modal": [], "cross_modal": [], } query_indices = np.random.choice(len(embeddings), n_queries, replace=False) for idx in query_indices: query = embeddings[idx:idx+1] query_mod = metadata[idx]["modality"] query_class = metadata[idx]["class"] # Project query query_proj = aligner.project( torch.tensor(query), query_mod ).detach().numpy() # Same-modal search same_proj = projected[query_mod] similarities = query_proj @ same_proj.T topk_idx = np.argsort(similarities[0])[::-1][:k] same_recall = compute_recall_at_k( [metadata[i]["class"] for i in topk_idx], query_class, k ) results["same_modal"].append(same_recall) # Cross-modal search for target_mod in ["optical", "sar", "multispectral"]: if target_mod == query_mod: continue target_proj = projected[target_mod] similarities = query_proj @ target_proj.T topk_idx = np.argsort(similarities[0])[::-1][:k] cross_recall = compute_recall_at_k( [metadata[i]["class"] for i in topk_idx], query_class, k ) results["cross_modal"].append(cross_recall) return { "same_modal_recall@k": np.mean(results["same_modal"]), "cross_modal_recall@k": np.mean(results["cross_modal"]), } def main(): """Run all benchmarks.""" print("=" * 60) print("Cross-Modal Retrieval Benchmark") print("=" * 60) # Load data print("\nLoading gallery data...") embeddings, metadata = load_gallery_data() print(f"Loaded {len(metadata)} embeddings of dimension {embeddings.shape[1]}") # Split by modality embeddings_by_mod = split_by_modality(embeddings, metadata) print(f"Modalities: {list(embeddings_by_mod.keys())}") for mod, embs in embeddings_by_mod.items(): print(f" {mod}: {len(embs)} images") # Run benchmarks n_queries = min(50, len(metadata)) k = 5 results = {} # 1. Single-index t0 = time.time() results["single"] = benchmark_single_index(embeddings, metadata, n_queries, k) results["single"]["time"] = time.time() - t0 # 2. Multi-index t0 = time.time() results["multi"] = benchmark_multi_index(embeddings_by_mod, metadata, n_queries, k) results["multi"]["time"] = time.time() - t0 # 3. Hybrid t0 = time.time() results["hybrid"] = benchmark_hybrid(embeddings_by_mod, metadata, n_queries, k) results["hybrid"]["time"] = time.time() - t0 # 4. Cross-modal alignment t0 = time.time() results["alignment"] = benchmark_cross_modal_alignment(embeddings, metadata, n_queries, k) results["alignment"]["time"] = time.time() - t0 # Print results print("\n" + "=" * 60) print("Results Summary") print("=" * 60) print(f"\n{'Method':<20} {'Same-Modal R@5':<18} {'Cross-Modal R@5':<18} {'Time (s)':<10}") print("-" * 66) for method, res in results.items(): same = res.get("same_modal_recall@k", 0) cross = res.get("cross_modal_recall@k", 0) or res.get("hybrid_recall@k", 0) t = res.get("time", 0) print(f"{method:<20} {same:<18.4f} {cross:<18.4f} {t:<10.3f}") print("\n" + "=" * 60) print("Recommendation: Use the method with highest cross-modal recall") print("=" * 60) if __name__ == "__main__": main()