Spaces:
Sleeping
Sleeping
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()
|