Spaces:
Sleeping
Sleeping
| """ | |
| Retrieval engine combining feature extraction and FAISS search. | |
| Provides high-level API for image retrieval. | |
| """ | |
| import torch | |
| import time | |
| from PIL import Image | |
| from typing import List, Dict, Optional, Tuple | |
| from dataclasses import dataclass | |
| from ..features.extractor import FeatureExtractor | |
| from .index import FAISSIndex | |
| class RetrievalResult: | |
| """Result of a single retrieval query.""" | |
| indices: List[int] | |
| scores: List[float] | |
| query_time_ms: float | |
| modality: str | |
| class RetrievalEngine: | |
| """ | |
| High-level retrieval engine. | |
| Combines feature extraction with FAISS search for fast image retrieval. | |
| """ | |
| def __init__( | |
| self, | |
| feature_extractor: Optional[FeatureExtractor] = None, | |
| index: Optional[FAISSIndex] = None, | |
| device: Optional[str] = None | |
| ): | |
| """ | |
| Initialize retrieval engine. | |
| Args: | |
| feature_extractor: Feature extractor (creates new if None) | |
| index: FAISS index (creates new if None) | |
| device: Device to use | |
| """ | |
| self.feature_extractor = feature_extractor or FeatureExtractor(device=device) | |
| self.index = index or FAISSIndex(embed_dim=self.feature_extractor.embed_dim) | |
| # Timing statistics | |
| self._query_times: List[float] = [] | |
| def build_index( | |
| self, | |
| embeddings: torch.Tensor, | |
| save_path: Optional[str] = None | |
| ) -> None: | |
| """ | |
| Build index from pre-computed embeddings. | |
| Args: | |
| embeddings: Gallery embeddings, shape (N, embed_dim) | |
| save_path: Optional path to save index | |
| """ | |
| self.index.build(embeddings) | |
| if save_path: | |
| self.index.save(save_path) | |
| def query( | |
| self, | |
| image: Image.Image, | |
| modality: str = "optical", | |
| k: int = 5 | |
| ) -> RetrievalResult: | |
| """ | |
| Query with a single image. | |
| Args: | |
| image: Query image | |
| modality: Image modality | |
| k: Number of results | |
| Returns: | |
| RetrievalResult with indices, scores, and timing | |
| """ | |
| start_time = time.perf_counter() | |
| # Extract features | |
| query_embedding = self.feature_extractor.extract_features( | |
| image, modality=modality, normalize=True | |
| ) | |
| # Search | |
| scores, indices = self.index.search(query_embedding, k=k) | |
| elapsed_ms = (time.perf_counter() - start_time) * 1000 | |
| self._query_times.append(elapsed_ms) | |
| return RetrievalResult( | |
| indices=indices[0].tolist(), | |
| scores=scores[0].tolist(), | |
| query_time_ms=elapsed_ms, | |
| modality=modality | |
| ) | |
| def batch_query( | |
| self, | |
| images: List[Image.Image], | |
| modality: str = "optical", | |
| k: int = 5 | |
| ) -> List[RetrievalResult]: | |
| """ | |
| Query with multiple images. | |
| Args: | |
| images: List of query images | |
| modality: Image modality | |
| k: Number of results | |
| Returns: | |
| List of RetrievalResult | |
| """ | |
| results = [] | |
| for image in images: | |
| result = self.query(image, modality=modality, k=k) | |
| results.append(result) | |
| return results | |
| def get_timing_stats(self) -> Dict[str, float]: | |
| """ | |
| Get timing statistics. | |
| Returns: | |
| Dict with mean, median, p95, p99 query times | |
| """ | |
| if not self._query_times: | |
| return {"mean": 0, "median": 0, "p95": 0, "p99": 0} | |
| times = sorted(self._query_times) | |
| n = len(times) | |
| return { | |
| "mean": sum(times) / n, | |
| "median": times[n // 2], | |
| "p95": times[int(n * 0.95)] if n >= 20 else times[-1], | |
| "p99": times[int(n * 0.99)] if n >= 100 else times[-1], | |
| } | |
| def _query_times(self) -> List[float]: | |
| """Query times list (lazy init).""" | |
| if not hasattr(self, '_query_times_list'): | |
| self._query_times_list = [] | |
| return self._query_times_list | |
| # Self-check | |
| if __name__ == "__main__": | |
| print("Testing RetrievalEngine...") | |
| # Create dummy data | |
| n_gallery = 50 | |
| embed_dim = 768 | |
| # Build index | |
| embeddings = torch.randn(n_gallery, embed_dim) | |
| embeddings = torch.nn.functional.normalize(embeddings, dim=1) | |
| # Initialize engine (without model for testing) | |
| engine = RetrievalEngine.__new__(RetrievalEngine) | |
| engine.index = FAISSIndex(embed_dim) | |
| engine._query_times_list = [] | |
| # Build index | |
| engine.build_index(embeddings) | |
| print(f"Index built with {engine.index.size} embeddings") | |
| # Simulate query timing | |
| for _ in range(10): | |
| start = time.perf_counter() | |
| query = torch.randn(embed_dim) | |
| query = torch.nn.functional.normalize(query, dim=0) | |
| scores, indices = engine.index.search(query, k=5) | |
| elapsed = (time.perf_counter() - start) * 1000 | |
| engine._query_times_list.append(elapsed) | |
| # Get stats | |
| stats = engine.get_timing_stats() | |
| print(f"Timing stats: {stats}") | |
| print("\nRetrievalEngine test passed!") | |