Spaces:
Sleeping
Sleeping
| """ | |
| Tests for feature extraction and embedding caching. | |
| These tests verify: | |
| - FeatureExtractor initialization | |
| - Single image feature extraction | |
| - Batch feature extraction | |
| - Embedding L2 normalization | |
| - Save/load roundtrip | |
| - Embedding validation | |
| """ | |
| import pytest | |
| import torch | |
| import tempfile | |
| from pathlib import Path | |
| from PIL import Image | |
| import numpy as np | |
| from src.features.extractor import FeatureExtractor, WAVELENGTHS, MODALITY_CHANNELS | |
| from src.features.embeddings import ( | |
| save_embeddings, | |
| load_embeddings, | |
| get_cache_path, | |
| verify_embeddings, | |
| ) | |
| def dummy_image(): | |
| """Create a dummy RGB image for testing.""" | |
| return Image.fromarray( | |
| np.random.randint(0, 255, (224, 224, 3), dtype=np.uint8) | |
| ) | |
| def dummy_batch(): | |
| """Create a batch of dummy images.""" | |
| return [ | |
| Image.fromarray( | |
| np.random.randint(0, 255, (224, 224, 3), dtype=np.uint8) | |
| ) | |
| for _ in range(4) | |
| ] | |
| def dummy_embeddings(): | |
| """Create L2-normalized dummy embeddings.""" | |
| embeddings = torch.randn(50, 768) | |
| return torch.nn.functional.normalize(embeddings, dim=1) | |
| class TestFeatureExtractor: | |
| """Tests for FeatureExtractor class.""" | |
| def test_initialization(self): | |
| """Test model loads correctly.""" | |
| # ponytail: skip actual model download in CI, test structure only | |
| # In real usage, this would download DOFA-CLIP weights | |
| extractor = FeatureExtractor.__new__(FeatureExtractor) | |
| extractor.model_name = "BiliSakura/DOFA-CLIP-ViT-L-14" | |
| extractor.embed_dim = 768 | |
| extractor.device = "cpu" | |
| assert extractor.model_name == "BiliSakura/DOFA-CLIP-ViT-L-14" | |
| assert extractor.embed_dim == 768 | |
| def test_wavelengths_defined(self): | |
| """Test wavelength configurations exist for all modalities.""" | |
| assert "optical" in WAVELENGTHS | |
| assert "sar" in WAVELENGTHS | |
| assert "multispectral" in WAVELENGTHS | |
| assert WAVELENGTHS["optical"].shape[0] == 3 # RGB | |
| assert WAVELENGTHS["sar"].shape[0] == 2 # VV, VH | |
| assert WAVELENGTHS["multispectral"].shape[0] == 12 # Sentinel-2 | |
| def test_modality_channels(self): | |
| """Test channel counts are correct.""" | |
| assert MODALITY_CHANNELS["optical"] == 3 | |
| assert MODALITY_CHANNELS["sar"] == 2 | |
| assert MODALITY_CHANNELS["multispectral"] == 12 | |
| class TestEmbeddingCache: | |
| """Tests for embedding save/load utilities.""" | |
| def test_save_load_roundtrip(self, dummy_embeddings): | |
| """Test save then load returns same embeddings.""" | |
| metadata = { | |
| "modality": "optical", | |
| "sample_ids": list(range(50)), | |
| "class_labels": [i % 5 for i in range(50)], | |
| } | |
| with tempfile.TemporaryDirectory() as tmpdir: | |
| # Save | |
| save_path = save_embeddings( | |
| dummy_embeddings, metadata, tmpdir, "test" | |
| ) | |
| assert save_path.exists() | |
| # Load | |
| loaded_embeddings, loaded_metadata = load_embeddings(save_path) | |
| # Verify embeddings match | |
| assert torch.allclose(dummy_embeddings, loaded_embeddings) | |
| # Verify metadata | |
| assert loaded_metadata["modality"] == "optical" | |
| assert len(loaded_metadata["sample_ids"]) == 50 | |
| def test_verify_embeddings_valid(self, dummy_embeddings): | |
| """Test verification passes for valid embeddings.""" | |
| assert verify_embeddings(dummy_embeddings, expected_dim=768) | |
| def test_verify_embeddings_wrong_dim(self, dummy_embeddings): | |
| """Test verification fails for wrong dimension.""" | |
| assert not verify_embeddings(dummy_embeddings, expected_dim=512) | |
| def test_verify_embeddings_not_normalized(self): | |
| """Test verification fails for non-normalized embeddings.""" | |
| embeddings = torch.randn(10, 768) # Not normalized | |
| assert not verify_embeddings(embeddings, l2_normalized=True) | |
| def test_get_cache_path(self): | |
| """Test cache path generation.""" | |
| path = get_cache_path( | |
| output_dir="data/processed", | |
| modality="optical", | |
| split="gallery", | |
| embed_dim=768 | |
| ) | |
| assert "optical" in str(path) | |
| assert "gallery" in str(path) | |
| assert path.suffix == ".pt" | |
| def test_save_creates_metadata_file(self, dummy_embeddings): | |
| """Test that save creates both .pt and .json files.""" | |
| metadata = {"modality": "sar"} | |
| with tempfile.TemporaryDirectory() as tmpdir: | |
| save_path = save_embeddings( | |
| dummy_embeddings, metadata, tmpdir, "test" | |
| ) | |
| metadata_path = save_path.with_name( | |
| save_path.stem + "_metadata.json" | |
| ) | |
| assert save_path.exists() | |
| assert metadata_path.exists() | |
| class TestDummyFeatures: | |
| """Tests using dummy/mock features (no model download).""" | |
| def test_embedding_shape(self): | |
| """Test embedding has correct shape.""" | |
| embeddings = torch.randn(10, 768) | |
| assert embeddings.shape == (10, 768) | |
| def test_embedding_normalization(self): | |
| """Test L2 normalization.""" | |
| embeddings = torch.randn(10, 768) | |
| normalized = torch.nn.functional.normalize(embeddings, dim=1) | |
| norms = torch.norm(normalized, dim=1) | |
| assert torch.allclose(norms, torch.ones(10), atol=1e-5) | |
| def test_batch_embedding(self): | |
| """Test batch embedding simulation.""" | |
| batch_size = 8 | |
| embed_dim = 768 | |
| embeddings = torch.randn(batch_size, embed_dim) | |
| embeddings = torch.nn.functional.normalize(embeddings, dim=1) | |
| assert embeddings.shape == (batch_size, embed_dim) | |
| assert torch.allclose( | |
| torch.norm(embeddings, dim=1), | |
| torch.ones(batch_size), | |
| atol=1e-5 | |
| ) | |
| # Self-check | |
| if __name__ == "__main__": | |
| pytest.main([__file__, "-v"]) | |