File size: 8,578 Bytes
a63cedf
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
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()