Skip to content

Commit 12ec27d

Browse files
authored
adding unit test for ibvs featurematching
1 parent b6f7a4f commit 12ec27d

File tree

4 files changed

+859
-3
lines changed

4 files changed

+859
-3
lines changed

metro-ai-suite/image-based-video-search/src/feature-matching/server.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,14 @@
1919
from PIL import Image
2020
from pymilvus import Collection, CollectionSchema, DataType, FieldSchema
2121

22-
from encoder import Base64ImageProcessor
23-
from milvus_utils import (
22+
from .encoder import Base64ImageProcessor
23+
from .milvus_utils import (
2424
CollectionExists,
2525
create_collection,
2626
get_milvus_client,
2727
get_search_results,
2828
)
29-
from schemas import PayloadSchema, TensorSchema
29+
from .schemas import PayloadSchema, TensorSchema
3030

3131
load_dotenv()
3232

Lines changed: 339 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,339 @@
1+
# tests/test_encoder.py
2+
3+
import os
4+
import sys
5+
from pathlib import Path
6+
import pytest
7+
import unittest.mock
8+
import base64
9+
import io
10+
from PIL import Image
11+
import tempfile
12+
13+
# Add the src directory to Python path
14+
src_path = Path(__file__).parent.parent / "src"
15+
sys.path.insert(0, str(src_path))
16+
17+
# Set up environment variables to avoid connection issues
18+
os.environ.setdefault('MILVUS_ENDPOINT', 'http://localhost:19530')
19+
os.environ.setdefault('MILVUS_TOKEN', 'test_token')
20+
os.environ.setdefault('COLLECTION_NAME', 'test_collection')
21+
os.environ.setdefault('MODEL_DIM', '512')
22+
os.environ.setdefault('MQTT_BROKER', 'localhost')
23+
os.environ.setdefault('MQTT_PORT', '1883')
24+
os.environ.setdefault('MQTT_TOPIC', 'test_topic')
25+
os.environ.setdefault('CONFIDENCE_THRESHOLD', '0.4')
26+
27+
# Import the encoder module directly to avoid loading the server module
28+
import importlib.util
29+
encoder_spec = importlib.util.spec_from_file_location("encoder", src_path / "feature_matching" / "encoder.py")
30+
encoder = importlib.util.module_from_spec(encoder_spec)
31+
encoder_spec.loader.exec_module(encoder)
32+
33+
34+
class TestBase64ImageProcessor:
35+
"""Test cases for Base64ImageProcessor"""
36+
37+
def setup_method(self):
38+
"""Set up test fixtures"""
39+
self.processor = encoder.Base64ImageProcessor()
40+
self.custom_processor = encoder.Base64ImageProcessor(size=(128, 128))
41+
42+
def create_test_image(self, size=(100, 100), mode="RGB", color=(255, 0, 0)):
43+
"""Helper method to create a test image"""
44+
return Image.new(mode, size, color)
45+
46+
def test_processor_initialization_default_size(self):
47+
"""Test Base64ImageProcessor initialization with default size"""
48+
processor = encoder.Base64ImageProcessor()
49+
assert processor.size == (224, 224)
50+
51+
def test_processor_initialization_custom_size(self):
52+
"""Test Base64ImageProcessor initialization with custom size"""
53+
custom_size = (128, 256)
54+
processor = encoder.Base64ImageProcessor(size=custom_size)
55+
assert processor.size == custom_size
56+
57+
def test_process_image_to_base64_basic(self):
58+
"""Test basic image processing to base64"""
59+
# Create a simple test image
60+
test_image = self.create_test_image(size=(100, 100), color=(255, 0, 0))
61+
62+
# Process the image
63+
result = self.processor.process_image_to_base64(test_image)
64+
65+
# Verify the result is a string
66+
assert isinstance(result, str)
67+
68+
# Verify it's valid base64
69+
try:
70+
decoded = base64.b64decode(result)
71+
assert len(decoded) > 0
72+
except Exception:
73+
pytest.fail("Result is not valid base64")
74+
75+
def test_process_image_to_base64_with_custom_size(self):
76+
"""Test image processing with custom processor size"""
77+
test_image = self.create_test_image(size=(200, 200), color=(0, 255, 0))
78+
79+
# Process with custom processor
80+
result = self.custom_processor.process_image_to_base64(test_image)
81+
82+
# Verify the result is a string
83+
assert isinstance(result, str)
84+
85+
# Decode the base64 and verify it's a valid image
86+
decoded = base64.b64decode(result)
87+
processed_image = Image.open(io.BytesIO(decoded))
88+
89+
# Verify the image has been resized to the custom size
90+
assert processed_image.size == (128, 128)
91+
92+
def test_process_image_rgb_conversion(self):
93+
"""Test that images are converted to RGB mode"""
94+
# Create a grayscale image
95+
grayscale_image = self.create_test_image(size=(50, 50), mode="L", color=128)
96+
97+
# Process the image
98+
result = self.processor.process_image_to_base64(grayscale_image)
99+
100+
# Decode and verify the result is RGB
101+
decoded = base64.b64decode(result)
102+
processed_image = Image.open(io.BytesIO(decoded))
103+
104+
assert processed_image.mode == "RGB"
105+
assert processed_image.size == (224, 224)
106+
107+
def test_process_image_rgba_conversion(self):
108+
"""Test that RGBA images are converted to RGB"""
109+
# Create an RGBA image with transparency
110+
rgba_image = self.create_test_image(size=(75, 75), mode="RGBA", color=(255, 0, 0, 128))
111+
112+
# Process the image
113+
result = self.processor.process_image_to_base64(rgba_image)
114+
115+
# Decode and verify the result is RGB
116+
decoded = base64.b64decode(result)
117+
processed_image = Image.open(io.BytesIO(decoded))
118+
119+
assert processed_image.mode == "RGB"
120+
assert processed_image.size == (224, 224)
121+
122+
def test_process_image_resize_functionality(self):
123+
"""Test that images are properly resized"""
124+
# Test with various input sizes
125+
test_sizes = [(50, 50), (100, 200), (300, 100), (500, 500)]
126+
127+
for input_size in test_sizes:
128+
test_image = self.create_test_image(size=input_size, color=(0, 0, 255))
129+
result = self.processor.process_image_to_base64(test_image)
130+
131+
# Decode and verify size
132+
decoded = base64.b64decode(result)
133+
processed_image = Image.open(io.BytesIO(decoded))
134+
135+
assert processed_image.size == (224, 224), f"Failed for input size {input_size}"
136+
137+
def test_process_image_format_output(self):
138+
"""Test that output format is JPEG"""
139+
test_image = self.create_test_image(size=(100, 100), color=(255, 255, 0))
140+
result = self.processor.process_image_to_base64(test_image)
141+
142+
# Decode and verify format
143+
decoded = base64.b64decode(result)
144+
processed_image = Image.open(io.BytesIO(decoded))
145+
146+
assert processed_image.format == "JPEG"
147+
148+
def test_process_image_deterministic_output(self):
149+
"""Test that the same image produces the same base64 output"""
150+
test_image = self.create_test_image(size=(100, 100), color=(128, 128, 128))
151+
152+
# Process the same image multiple times
153+
result1 = self.processor.process_image_to_base64(test_image)
154+
result2 = self.processor.process_image_to_base64(test_image)
155+
156+
# Results should be identical for the same input
157+
assert result1 == result2
158+
159+
def test_process_image_different_inputs_different_outputs(self):
160+
"""Test that different images produce different base64 outputs"""
161+
image1 = self.create_test_image(size=(100, 100), color=(255, 0, 0))
162+
image2 = self.create_test_image(size=(100, 100), color=(0, 255, 0))
163+
164+
result1 = self.processor.process_image_to_base64(image1)
165+
result2 = self.processor.process_image_to_base64(image2)
166+
167+
# Different images should produce different outputs
168+
assert result1 != result2
169+
170+
def test_process_image_with_real_image_file(self):
171+
"""Test processing with a real image file"""
172+
# Create a temporary image file
173+
with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as temp_file:
174+
temp_image = self.create_test_image(size=(150, 150), color=(100, 150, 200))
175+
temp_image.save(temp_file.name, "PNG")
176+
temp_file_path = temp_file.name
177+
178+
try:
179+
# Load the image from file
180+
loaded_image = Image.open(temp_file_path)
181+
182+
# Process it
183+
result = self.processor.process_image_to_base64(loaded_image)
184+
185+
# Verify the result
186+
assert isinstance(result, str)
187+
assert len(result) > 0
188+
189+
# Verify it can be decoded back to an image
190+
decoded = base64.b64decode(result)
191+
processed_image = Image.open(io.BytesIO(decoded))
192+
assert processed_image.size == (224, 224)
193+
assert processed_image.mode == "RGB"
194+
195+
finally:
196+
# Clean up the temporary file
197+
os.unlink(temp_file_path)
198+
199+
def test_process_image_lanczos_resampling(self):
200+
"""Test that LANCZOS resampling is used (this is more of a behavior test)"""
201+
# Create a detailed test image with patterns
202+
test_image = Image.new("RGB", (100, 100))
203+
# Create a checkerboard pattern
204+
for x in range(100):
205+
for y in range(100):
206+
if (x // 10 + y // 10) % 2:
207+
test_image.putpixel((x, y), (255, 255, 255))
208+
else:
209+
test_image.putpixel((x, y), (0, 0, 0))
210+
211+
# Process the image
212+
result = self.processor.process_image_to_base64(test_image)
213+
214+
# Verify the result is valid
215+
assert isinstance(result, str)
216+
decoded = base64.b64decode(result)
217+
processed_image = Image.open(io.BytesIO(decoded))
218+
assert processed_image.size == (224, 224)
219+
220+
def test_process_image_preserves_aspect_ratio_behavior(self):
221+
"""Test the behavior of resize (note: PIL resize doesn't preserve aspect ratio by default)"""
222+
# Create a rectangular image
223+
rectangular_image = self.create_test_image(size=(100, 200), color=(255, 128, 0))
224+
225+
# Process it
226+
result = self.processor.process_image_to_base64(rectangular_image)
227+
228+
# Decode and verify
229+
decoded = base64.b64decode(result)
230+
processed_image = Image.open(io.BytesIO(decoded))
231+
232+
# The image should be exactly the target size (aspect ratio not preserved)
233+
assert processed_image.size == (224, 224)
234+
235+
def test_process_image_edge_cases(self):
236+
"""Test edge cases and boundary conditions"""
237+
# Test with very small image
238+
tiny_image = self.create_test_image(size=(1, 1), color=(255, 255, 255))
239+
result = self.processor.process_image_to_base64(tiny_image)
240+
assert isinstance(result, str)
241+
242+
# Test with square image matching target size
243+
exact_size_image = self.create_test_image(size=(224, 224), color=(128, 64, 192))
244+
result = self.processor.process_image_to_base64(exact_size_image)
245+
assert isinstance(result, str)
246+
247+
# Verify the exact size image
248+
decoded = base64.b64decode(result)
249+
processed_image = Image.open(io.BytesIO(decoded))
250+
assert processed_image.size == (224, 224)
251+
252+
def test_process_image_invalid_input(self):
253+
"""Test error handling for invalid inputs"""
254+
# Test with None input
255+
with pytest.raises(AttributeError):
256+
self.processor.process_image_to_base64(None)
257+
258+
# Test with non-PIL Image object
259+
with pytest.raises(AttributeError):
260+
self.processor.process_image_to_base64("not_an_image")
261+
262+
def test_multiple_processors_independence(self):
263+
"""Test that multiple processor instances work independently"""
264+
processor1 = encoder.Base64ImageProcessor(size=(64, 64))
265+
processor2 = encoder.Base64ImageProcessor(size=(128, 128))
266+
267+
test_image = self.create_test_image(size=(100, 100), color=(200, 100, 50))
268+
269+
result1 = processor1.process_image_to_base64(test_image)
270+
result2 = processor2.process_image_to_base64(test_image)
271+
272+
# Results should be different due to different target sizes
273+
assert result1 != result2
274+
275+
# Verify the actual sizes
276+
decoded1 = base64.b64decode(result1)
277+
decoded2 = base64.b64decode(result2)
278+
279+
image1 = Image.open(io.BytesIO(decoded1))
280+
image2 = Image.open(io.BytesIO(decoded2))
281+
282+
assert image1.size == (64, 64)
283+
assert image2.size == (128, 128)
284+
285+
286+
class TestBase64ImageProcessorIntegration:
287+
"""Integration tests for Base64ImageProcessor"""
288+
289+
def test_base64_roundtrip_consistency(self):
290+
"""Test that the base64 encoding/decoding roundtrip works correctly"""
291+
processor = encoder.Base64ImageProcessor(size=(100, 100))
292+
293+
# Create a test image with known properties
294+
original_image = Image.new("RGB", (50, 50), (123, 234, 45))
295+
296+
# Process to base64
297+
base64_string = processor.process_image_to_base64(original_image)
298+
299+
# Decode back to image
300+
decoded_bytes = base64.b64decode(base64_string)
301+
decoded_image = Image.open(io.BytesIO(decoded_bytes))
302+
303+
# Verify properties
304+
assert decoded_image.mode == "RGB"
305+
assert decoded_image.size == (100, 100)
306+
assert decoded_image.format == "JPEG"
307+
308+
def test_processor_with_various_color_modes(self):
309+
"""Test processor with different PIL image color modes"""
310+
processor = encoder.Base64ImageProcessor()
311+
312+
# Test different color modes
313+
modes_and_colors = [
314+
("RGB", (255, 128, 64)),
315+
("L", 128), # Grayscale
316+
("RGBA", (255, 128, 64, 200)),
317+
("P", 5), # Palette mode
318+
]
319+
320+
for mode, color in modes_and_colors:
321+
if mode == "P":
322+
# Palette mode requires special handling
323+
test_image = Image.new("P", (50, 50))
324+
test_image.putpalette([i % 256 for i in range(768)]) # Simple palette
325+
test_image.putpixel((25, 25), color)
326+
else:
327+
test_image = Image.new(mode, (50, 50), color)
328+
329+
# Process the image
330+
result = processor.process_image_to_base64(test_image)
331+
332+
# Verify the result
333+
assert isinstance(result, str)
334+
335+
# Decode and verify it's RGB
336+
decoded = base64.b64decode(result)
337+
processed_image = Image.open(io.BytesIO(decoded))
338+
assert processed_image.mode == "RGB"
339+
assert processed_image.size == (224, 224)

0 commit comments

Comments
 (0)