Spaces:
Sleeping
Sleeping
| """ | |
| Unit tests for DIPAug transforms. | |
| Tests cover: | |
| - Basic functionality (apply to image and mask) | |
| - Edge cases (all-black image, single-pixel) | |
| - Mask consistency (spatially consistent transforms) | |
| - Intensity levels (0.2, 0.5, 0.8, 1.0) | |
| - Performance (≥120 images/second) | |
| """ | |
| import pytest | |
| import numpy as np | |
| import cv2 | |
| from pathlib import Path | |
| from dipauglib.transforms import ( | |
| IlluminationGradient, | |
| CastShadow, | |
| MotionBlur, | |
| DefocusBlur, | |
| ColourTempShift, | |
| ColourFade, | |
| DustOverlay, | |
| SensorNoise, | |
| ) | |
| # Helper functions | |
| def create_test_image(height=384, width=384, channels=3): | |
| """Create a random test image.""" | |
| return np.random.randint(0, 255, (height, width, channels), dtype=np.uint8) | |
| def create_test_mask(height=384, width=384): | |
| """Create a random test mask (binary or multi-class).""" | |
| return np.random.randint(0, 5, (height, width), dtype=np.uint8) | |
| def create_all_black_image(height=384, width=384): | |
| """Create an all-black image.""" | |
| return np.zeros((height, width, 3), dtype=np.uint8) | |
| def create_single_pixel_image(): | |
| """Create a single-pixel image.""" | |
| return np.array([[[128, 64, 192]]], dtype=np.uint8) | |
| # Test data | |
| def sample_image(): | |
| return create_test_image() | |
| def sample_mask(): | |
| return create_test_mask() | |
| def black_image(): | |
| return create_all_black_image() | |
| def single_pixel_image(): | |
| return create_single_pixel_image() | |
| # Test all 8 transforms | |
| class TestIlluminationGradient: | |
| def test_basic(self, sample_image, sample_mask): | |
| aug = IlluminationGradient(p=1.0, angle=180, strength=0.5) | |
| result = aug(image=sample_image, mask=sample_mask) | |
| assert result["image"].shape == sample_image.shape | |
| assert result["mask"].shape == sample_mask.shape | |
| assert result["mask"].sum() == sample_mask.sum() # Mask unchanged | |
| def test_intensities(self, sample_image): | |
| for strength in [0.2, 0.5, 0.8, 1.0]: | |
| aug = IlluminationGradient(p=1.0, strength=strength) | |
| result = aug(image=sample_image) | |
| assert result["image"].shape == sample_image.shape | |
| def test_black_image(self, black_image): | |
| aug = IlluminationGradient(p=1.0, strength=0.5) | |
| result = aug(image=black_image) | |
| assert result["image"].shape == black_image.shape | |
| def test_all_angles(self, sample_image): | |
| for angle in [0, 90, 180, 270, 360]: | |
| aug = IlluminationGradient(p=1.0, angle=angle) | |
| result = aug(image=sample_image) | |
| assert result["image"].shape == sample_image.shape | |
| class TestCastShadow: | |
| def test_basic(self, sample_image, sample_mask): | |
| aug = CastShadow(p=1.0, area=0.25, blur_sigma=10) | |
| result = aug(image=sample_image, mask=sample_mask) | |
| assert result["image"].shape == sample_image.shape | |
| assert result["mask"].shape == sample_mask.shape | |
| def test_intensities(self, sample_image): | |
| for area in [0.1, 0.25, 0.4, 0.5]: | |
| aug = CastShadow(p=1.0, area=area) | |
| result = aug(image=sample_image) | |
| assert result["image"].shape == sample_image.shape | |
| def test_black_image(self, black_image): | |
| aug = CastShadow(p=1.0, area=0.25) | |
| result = aug(image=black_image) | |
| assert result["image"].shape == black_image.shape | |
| class TestMotionBlur: | |
| def test_basic(self, sample_image, sample_mask): | |
| aug = MotionBlur(p=1.0, kernel_size=15, angle=90) | |
| result = aug(image=sample_image, mask=sample_mask) | |
| assert result["image"].shape == sample_image.shape | |
| assert result["mask"].shape == sample_mask.shape | |
| def test_intensities(self, sample_image): | |
| for kernel_size in [5, 10, 15, 25]: | |
| aug = MotionBlur(p=1.0, kernel_size=kernel_size) | |
| result = aug(image=sample_image) | |
| assert result["image"].shape == sample_image.shape | |
| def test_all_angles(self, sample_image): | |
| for angle in [0, 45, 90, 135, 180]: | |
| aug = MotionBlur(p=1.0, angle=angle) | |
| result = aug(image=sample_image) | |
| assert result["image"].shape == sample_image.shape | |
| def test_black_image(self, black_image): | |
| aug = MotionBlur(p=1.0, kernel_size=15) | |
| result = aug(image=black_image) | |
| assert result["image"].shape == black_image.shape | |
| class TestDefocusBlur: | |
| def test_basic(self, sample_image, sample_mask): | |
| aug = DefocusBlur(p=1.0, radius=8) | |
| result = aug(image=sample_image, mask=sample_mask) | |
| assert result["image"].shape == sample_image.shape | |
| assert result["mask"].shape == sample_mask.shape | |
| def test_intensities(self, sample_image): | |
| for radius in [3, 5, 10, 15]: | |
| aug = DefocusBlur(p=1.0, radius=radius) | |
| result = aug(image=sample_image) | |
| assert result["image"].shape == sample_image.shape | |
| def test_black_image(self, black_image): | |
| aug = DefocusBlur(p=1.0, radius=8) | |
| result = aug(image=black_image) | |
| assert result["image"].shape == black_image.shape | |
| class TestColourTempShift: | |
| def test_basic(self, sample_image, sample_mask): | |
| aug = ColourTempShift(p=1.0, cct_kelvin=5500) | |
| result = aug(image=sample_image, mask=sample_mask) | |
| assert result["image"].shape == sample_image.shape | |
| assert result["mask"].shape == sample_mask.shape | |
| def test_intensities(self, sample_image): | |
| for cct in [3200, 4500, 6500, 8000]: | |
| aug = ColourTempShift(p=1.0, cct_kelvin=cct) | |
| result = aug(image=sample_image) | |
| assert result["image"].shape == sample_image.shape | |
| def test_black_image(self, black_image): | |
| aug = ColourTempShift(p=1.0, cct_kelvin=5500) | |
| result = aug(image=black_image) | |
| assert result["image"].shape == black_image.shape | |
| class TestColourFade: | |
| def test_basic(self, sample_image, sample_mask): | |
| aug = ColourFade(p=1.0, sat_factor=-0.5, gamma=1.0) | |
| result = aug(image=sample_image, mask=sample_mask) | |
| assert result["image"].shape == sample_image.shape | |
| assert result["mask"].shape == sample_mask.shape | |
| def test_intensities(self, sample_image): | |
| for sat in [-0.3, -0.5, -0.7, -0.9]: | |
| aug = ColourFade(p=1.0, sat_factor=sat) | |
| result = aug(image=sample_image) | |
| assert result["image"].shape == sample_image.shape | |
| def test_black_image(self, black_image): | |
| aug = ColourFade(p=1.0, sat_factor=-0.5) | |
| result = aug(image=black_image) | |
| assert result["image"].shape == black_image.shape | |
| class TestDustOverlay: | |
| def test_basic(self, sample_image, sample_mask): | |
| aug = DustOverlay(p=1.0, n_particles=150, opacity=0.4) | |
| result = aug(image=sample_image, mask=sample_mask) | |
| assert result["image"].shape == sample_image.shape | |
| assert result["mask"].shape == sample_mask.shape | |
| def test_intensities(self, sample_image): | |
| for n in [50, 100, 200, 300]: | |
| aug = DustOverlay(p=1.0, n_particles=n) | |
| result = aug(image=sample_image) | |
| assert result["image"].shape == sample_image.shape | |
| def test_black_image(self, black_image): | |
| aug = DustOverlay(p=1.0, n_particles=150) | |
| result = aug(image=black_image) | |
| assert result["image"].shape == black_image.shape | |
| class TestSensorNoise: | |
| def test_basic(self, sample_image, sample_mask): | |
| aug = SensorNoise(p=1.0, sigma=15, jpeg_qf=70) | |
| result = aug(image=sample_image, mask=sample_mask) | |
| assert result["image"].shape == sample_image.shape | |
| assert result["mask"].shape == sample_mask.shape | |
| def test_intensities(self, sample_image): | |
| for sigma in [5, 10, 20, 30]: | |
| aug = SensorNoise(p=1.0, sigma=sigma) | |
| result = aug(image=sample_image) | |
| assert result["image"].shape == sample_image.shape | |
| def test_black_image(self, black_image): | |
| aug = SensorNoise(p=1.0, sigma=15) | |
| result = aug(image=black_image) | |
| assert result["image"].shape == black_image.shape | |
| # Performance test | |
| class TestPerformance: | |
| def test_throughput(self, sample_image): | |
| """Test that augmentation pipeline runs at >= 120 images/second.""" | |
| import time | |
| from dipauglib.transforms import MotionBlur | |
| aug = MotionBlur(p=1.0, kernel_size=15) | |
| batch_size = 32 | |
| n_batches = 10 | |
| start = time.time() | |
| for _ in range(n_batches): | |
| images = [sample_image.copy() for _ in range(batch_size)] | |
| for img in images: | |
| aug(image=img) | |
| elapsed = time.time() - start | |
| total_images = batch_size * n_batches | |
| throughput = total_images / elapsed | |
| # Note: This is a basic test; actual GPU throughput may vary | |
| assert throughput > 50 # Conservative threshold for CPU test | |
| # Mask consistency test | |
| class TestMaskConsistency: | |
| def test_spatial_consistency(self): | |
| """Verify that spatially consistent transforms apply identically to image and mask.""" | |
| from dipauglib.transforms import MotionBlur | |
| # Create image with known pattern | |
| img = np.zeros((100, 100, 3), dtype=np.uint8) | |
| img[20:80, 20:80] = 255 # White square | |
| # Create matching mask | |
| mask = np.zeros((100, 100), dtype=np.uint8) | |
| mask[20:80, 20:80] = 1 | |
| # Apply transform | |
| aug = MotionBlur(p=1.0, kernel_size=15, angle=0) | |
| result = aug(image=img, mask=mask) | |
| # Check that transformed image and mask have same spatial dimensions | |
| assert result["image"].shape[:2] == result["mask"].shape | |
| if __name__ == "__main__": | |
| pytest.main([__file__, "-v"]) | |