Spaces:
Sleeping
Sleeping
| import unittest | |
| from unittest.mock import MagicMock, patch | |
| import os | |
| import io | |
| from PIL import Image | |
| import numpy as np | |
| from app.services.image_preprocess_service import image_preprocess_service, PreprocessStrategy | |
| class TestImagePreprocessService(unittest.TestCase): | |
| def setUp(self): | |
| self.image_path = "tests/data/Melanoma1280x891.jpg" | |
| self.small_image_path = "tests/data/Melanoma400x278.jpg" | |
| self.melanoma_path = "tests/data/melanoma.jpg" | |
| self.mole_path = "tests/data/mole.jpg" | |
| # Verify all files exist | |
| for p in [self.image_path, self.small_image_path, self.melanoma_path, self.mole_path]: | |
| if not os.path.exists(p): | |
| print(f"DEBUG: Missing test file {p}") | |
| # We skip instead of failing to avoid breaking CI if files are partially missing | |
| # though they should be in the repo. | |
| self.skipTest(f"Missing required test data: {p}") | |
| image_preprocess_service.recommend_prep_strategy.cache_clear() | |
| def test_melanoma_crop_vs_pad_logic(self): | |
| """ | |
| Test the logic that decides between cropping and padding based on detection. | |
| Using Melanoma1280×891.jpg (Landscape: 1280x891). | |
| Center crop window would be [194.5, 0, 1085.5, 891]. | |
| """ | |
| with open(self.image_path, "rb") as f: | |
| content = f.read() | |
| # Verify image dimensions first | |
| img = Image.open(io.BytesIO(content)) | |
| self.assertEqual(img.size, (1280, 891)) | |
| # Case 1: Lesion is centered -> Strategy: CROP | |
| with patch.object(image_preprocess_service, 'get_lesion_bbox', return_value=(500, 300, 700, 500)): | |
| image_preprocess_service.recommend_prep_strategy.cache_clear() | |
| res = image_preprocess_service.recommend_prep_strategy(content) | |
| self.assertEqual(res["strategy"], PreprocessStrategy.CROP) | |
| self.assertIn("fully contained", res["reason"]) | |
| # Case 2: Lesion is at the far left edge (x=50) -> Strategy: PAD | |
| with patch.object(image_preprocess_service, 'get_lesion_bbox', return_value=(50, 300, 200, 500)): | |
| image_preprocess_service.recommend_prep_strategy.cache_clear() | |
| res = image_preprocess_service.recommend_prep_strategy(content) | |
| self.assertEqual(res["strategy"], PreprocessStrategy.PAD) | |
| self.assertIn("extends beyond", res["reason"]) | |
| # Case 3: Lesion is at the far right edge (x=1200) -> Strategy: PAD | |
| with patch.object(image_preprocess_service, 'get_lesion_bbox', return_value=(1100, 300, 1250, 500)): | |
| image_preprocess_service.recommend_prep_strategy.cache_clear() | |
| res = image_preprocess_service.recommend_prep_strategy(content) | |
| self.assertEqual(res["strategy"], PreprocessStrategy.PAD) | |
| self.assertIn("extends beyond", res["reason"]) | |
| def test_melanoma_real_image_strategy(self): | |
| """ | |
| Test that the real Melanoma1280×891.jpg results in PAD strategy. | |
| This image has the melanoma near the edge, so cropping would cut it. | |
| We mock the detection bbox to represent this edge-positioning. | |
| """ | |
| with open(self.image_path, "rb") as f: | |
| content = f.read() | |
| # Mocking the detection result for this specific file: | |
| # For a 1280 wide image, center crop starts at 194.5. | |
| # We mock a lesion at the far left edge (x=50) to verify PAD logic. | |
| with patch.object(image_preprocess_service, 'get_lesion_bbox', return_value=(50, 400, 250, 600)): | |
| image_preprocess_service.recommend_prep_strategy.cache_clear() | |
| res = image_preprocess_service.recommend_prep_strategy(content) | |
| self.assertEqual(res["strategy"], PreprocessStrategy.PAD) | |
| self.assertIn("extends beyond", res["reason"]) | |
| def test_mole_crop_vs_pad_strategy(self): | |
| """ | |
| Test logic for mole.jpg (670x442). | |
| Center crop x-range is [114, 556]. | |
| """ | |
| path = "tests/data/mole.jpg" | |
| if not os.path.exists(path): | |
| self.skipTest("mole.jpg not found") | |
| with open(path, "rb") as f: | |
| content = f.read() | |
| # Case 1: Centered mole -> CROP | |
| with patch.object(image_preprocess_service, 'get_lesion_bbox', return_value=(200, 100, 400, 300)): | |
| image_preprocess_service.recommend_prep_strategy.cache_clear() | |
| res = image_preprocess_service.recommend_prep_strategy(content) | |
| self.assertEqual(res["strategy"], PreprocessStrategy.CROP) | |
| # Case 2: Mole at left edge (x=50) -> PAD (Cutoff is at x=114) | |
| with patch.object(image_preprocess_service, 'get_lesion_bbox', return_value=(50, 100, 150, 300)): | |
| image_preprocess_service.recommend_prep_strategy.cache_clear() | |
| res = image_preprocess_service.recommend_prep_strategy(content) | |
| self.assertEqual(res["strategy"], PreprocessStrategy.PAD) | |
| def test_prepare_image_basic(self): | |
| """Test basic crop/resize via prepare_image.""" | |
| # Force a crop strategy by mocking get_lesion_bbox to return centered result | |
| # Use existing melanoma.jpg (224x224) | |
| with Image.open(self.melanoma_path) as img: | |
| with patch.object(image_preprocess_service, 'get_lesion_bbox', return_value=(90, 90, 130, 130)): | |
| image_preprocess_service.recommend_prep_strategy.cache_clear() | |
| prepared = image_preprocess_service.prepare_image(img, (50, 50)) | |
| self.assertEqual(prepared.size, (50, 50)) | |
| def test_prepare_image_pad(self): | |
| """Test padding via prepare_image.""" | |
| # Force a pad strategy by using a LARGE image that exceeds 448x448 | |
| # Use existing Melanoma1280x891.jpg | |
| with Image.open(self.image_path) as img: | |
| # Mock lesion at the very left (x=50) so it's outside center crop | |
| with patch.object(image_preprocess_service, 'get_lesion_bbox', return_value=(50, 200, 150, 300)): | |
| image_preprocess_service.recommend_prep_strategy.cache_clear() | |
| prepared = image_preprocess_service.prepare_image(img, (50, 50)) | |
| self.assertEqual(prepared.size, (50, 50)) | |
| # Resize to 50x50 -> should have black bars if PAD was used | |
| pixels = list(prepared.getdata()) | |
| top_pixel = pixels[0] | |
| self.assertEqual(top_pixel, (0, 0, 0)) # Should be black padding | |
| def test_small_image_bypass_logic(self): | |
| """Test that images <= 448x448 return PAD strategy to avoid cropping/scaling down.""" | |
| # Use Melanoma400x278.jpg as the small rectangular image | |
| with open(self.small_image_path, "rb") as f: | |
| content = f.read() | |
| image_preprocess_service.recommend_prep_strategy.cache_clear() | |
| res = image_preprocess_service.recommend_prep_strategy(content) | |
| self.assertEqual(res["strategy"], PreprocessStrategy.PAD) | |
| self.assertIn("padding to square to avoid any data loss", res["reason"]) | |
| def test_melanoma_small_padding_real_flow(self): | |
| """ | |
| Test that Melanoma400x278.jpg is padded to 400x400. | |
| It should not be scaled down (kept at 400 max dim). | |
| """ | |
| with open(self.small_image_path, "rb") as f: | |
| content = f.read() | |
| # 1. Check recommendation | |
| res = image_preprocess_service.recommend_prep_strategy(content) | |
| self.assertEqual(res["strategy"], PreprocessStrategy.PAD) | |
| # 2. Check preparation result | |
| # We specify target_size=(400, 400) to verify it stays at that size | |
| img = Image.open(io.BytesIO(content)) | |
| prepared = image_preprocess_service.prepare_image(img, target_size=(400, 400)) | |
| self.assertEqual(prepared.size, (400, 400)) | |
| # Verify symmetric padding (top and bottom should be black) | |
| # 400x278 -> 400x400 square. Padding = (400-278)/2 = 61 pixels top and bottom | |
| pixels = list(prepared.getdata()) | |
| # Top-left pixel should be black padding (0,0,0) | |
| self.assertEqual(pixels[0], (0, 0, 0)) | |
| # Top-middle pixel should be black padding | |
| self.assertEqual(pixels[200], (0, 0, 0)) | |
| # Center pixel (200, 200) should be the original image content (not black) | |
| center_pixel = pixels[200 * 400 + 200] | |
| self.assertNotEqual(center_pixel, (0, 0, 0)) | |
| if __name__ == '__main__': | |
| unittest.main() | |