Skip to main content
Glama

šŸŖ„ ImageSorcery MCP

test_fill.py•28.1 kB
import os import cv2 import numpy as np import pytest from fastmcp import Client, FastMCP from imagesorcery_mcp.server import mcp as image_sorcery_mcp_server @pytest.fixture def mcp_server(): # Use the existing server instance return image_sorcery_mcp_server @pytest.fixture def test_image_path(tmp_path): """Create a test image with a black and white background for filling.""" img_path = tmp_path / "test_image.png" # Create a white image img = np.ones((300, 400, 3), dtype=np.uint8) * 255 # Draw a black rectangle to check blending against cv2.rectangle(img, (100, 75), (300, 225), (0, 0, 0), -1) cv2.imwrite(str(img_path), img) return str(img_path) @pytest.fixture def test_jpeg_image_path(tmp_path): """Create a test JPEG image (no alpha channel) for testing transparency operations.""" img_path = tmp_path / "test_image.jpg" # Create a white image img = np.ones((300, 400, 3), dtype=np.uint8) * 255 # Draw a black rectangle to check blending against cv2.rectangle(img, (100, 75), (300, 225), (0, 0, 0), -1) # Draw a red circle cv2.circle(img, (200, 150), 50, (0, 0, 255), -1) cv2.imwrite(str(img_path), img) return str(img_path) class TestFillToolDefinition: """Tests for the fill tool definition and metadata.""" @pytest.mark.asyncio async def test_fill_in_tools_list(self, mcp_server: FastMCP): """Tests that fill tool is in the list of available tools.""" async with Client(mcp_server) as client: tools = await client.list_tools() assert tools, "Tools list should not be empty" tool_names = [tool.name for tool in tools] assert "fill" in tool_names, "fill tool should be in the list of available tools" @pytest.mark.asyncio async def test_fill_description(self, mcp_server: FastMCP): """Tests that fill tool has the correct description.""" async with Client(mcp_server) as client: tools = await client.list_tools() fill_tool = next((tool for tool in tools if tool.name == "fill"), None) assert fill_tool.description, "fill tool should have a description" assert "fill" in fill_tool.description.lower(), "Description should mention that it fills areas of an image" @pytest.mark.asyncio async def test_fill_parameters(self, mcp_server: FastMCP): """Tests that fill tool has the correct parameter structure.""" async with Client(mcp_server) as client: tools = await client.list_tools() fill_tool = next((tool for tool in tools if tool.name == "fill"), None) assert hasattr(fill_tool, "inputSchema"), "fill tool should have an inputSchema" assert "properties" in fill_tool.inputSchema, "inputSchema should have properties field" required_params = ["input_path", "areas"] for param in required_params: assert param in fill_tool.inputSchema["properties"], f"fill tool should have a '{param}' property in its inputSchema" assert "output_path" in fill_tool.inputSchema["properties"], "fill tool should have an 'output_path' property in its inputSchema" assert fill_tool.inputSchema["properties"]["input_path"].get("type") == "string", "input_path should be of type string" assert fill_tool.inputSchema["properties"]["areas"].get("type") == "array", "areas should be of type array" output_path_schema = fill_tool.inputSchema["properties"]["output_path"] assert "anyOf" in output_path_schema, "output_path should have anyOf field for optional types" string_type_present = any(type_option.get("type") == "string" for type_option in output_path_schema["anyOf"]) assert string_type_present, "output_path should allow string type" class TestFillToolExecution: """Tests for the fill tool execution and results.""" @pytest.mark.asyncio async def test_fill_tool_execution(self, mcp_server: FastMCP, test_image_path, tmp_path): """Tests the fill tool execution and return value.""" output_path = str(tmp_path / "output.png") fill_area = {"x1": 150, "y1": 100, "x2": 250, "y2": 200, "color": [0, 0, 255], "opacity": 0.5} async with Client(mcp_server) as client: result = await client.call_tool("fill", {"input_path": test_image_path, "areas": [fill_area], "output_path": output_path}) assert result.data == output_path assert os.path.exists(output_path) img = cv2.imread(output_path) filled_pixel = img[150, 200] assert np.allclose(filled_pixel, [0, 0, 128], atol=2) unfilled_pixel = img[150, 120] assert np.array_equal(unfilled_pixel, [0, 0, 0]) white_pixel = img[50, 50] assert np.array_equal(white_pixel, [255, 255, 255]) @pytest.mark.asyncio async def test_fill_polygon_area(self, mcp_server: FastMCP, test_image_path, tmp_path): """Tests the fill tool with a polygon area.""" output_path = str(tmp_path / "output_poly.png") polygon_area = {"polygon": [[160, 110], [240, 110], [200, 190]], "color": [0, 255, 0], "opacity": 0.8} async with Client(mcp_server) as client: result = await client.call_tool("fill", {"input_path": test_image_path, "areas": [polygon_area], "output_path": output_path}) assert result.data == output_path assert os.path.exists(output_path) img = cv2.imread(output_path) poly_center_pixel = img[130, 200] assert np.allclose(poly_center_pixel, [0, 204, 0], atol=2) @pytest.mark.asyncio async def test_fill_default_output_path(self, mcp_server: FastMCP, test_image_path): """Tests the fill tool with default output path.""" async with Client(mcp_server) as client: result = await client.call_tool("fill", {"input_path": test_image_path, "areas": [{"x1": 150, "y1": 100, "x2": 250, "y2": 200}]}) expected_output = test_image_path.replace(".png", "_filled.png") assert result.data == expected_output assert os.path.exists(expected_output) @pytest.mark.asyncio async def test_fill_multiple_areas(self, mcp_server: FastMCP, test_image_path, tmp_path): """Tests the fill tool with multiple overlapping areas.""" output_path = str(tmp_path / "multi_fill.png") async with Client(mcp_server) as client: await client.call_tool("fill", {"input_path": test_image_path, "areas": [{"x1": 110, "y1": 85, "x2": 160, "y2": 135, "color": [0, 0, 255], "opacity": 1.0}, {"x1": 150, "y1": 125, "x2": 200, "y2": 175, "color": [0, 255, 0], "opacity": 0.5}], "output_path": output_path}) img = cv2.imread(output_path) assert np.array_equal(img[100, 120], [0, 0, 255]) assert np.allclose(img[150, 160], [0, 128, 0], atol=2) assert np.allclose(img[130, 155], [0, 128, 128], atol=2) @pytest.mark.asyncio async def test_fill_transparent_rectangle(self, mcp_server: FastMCP, test_image_path, tmp_path): """Tests making a rectangular area transparent with all channels set to 0.""" output_path = str(tmp_path / "output_transparent.png") fill_area = {"x1": 150, "y1": 100, "x2": 250, "y2": 200, "color": None} async with Client(mcp_server) as client: result = await client.call_tool( "fill", { "input_path": test_image_path, "areas": [fill_area], "output_path": output_path, }, ) assert result.data == output_path assert os.path.exists(output_path) img = cv2.imread(output_path, cv2.IMREAD_UNCHANGED) assert img.shape[2] == 4 # Should have alpha channel # Check a pixel inside the transparent area - all channels should be 0 pixel_inside = img[150, 200] assert np.array_equal(pixel_inside, [0, 0, 0, 0]), "All BGRA channels should be 0 for transparent areas" # Check multiple pixels in the transparent area for y in range(100, 200, 20): for x in range(150, 250, 20): pixel = img[y, x] assert np.array_equal(pixel, [0, 0, 0, 0]), f"Pixel at ({y}, {x}) should have all channels set to 0" # Check a pixel outside the transparent area pixel_outside = img[50, 50] assert pixel_outside[3] == 255 # Alpha should be 255 assert np.array_equal(pixel_outside[:3], [255, 255, 255]) # Should be white @pytest.mark.asyncio async def test_fill_transparent_polygon(self, mcp_server: FastMCP, test_image_path, tmp_path): """Tests making a polygonal area transparent with all channels set to 0.""" output_path = str(tmp_path / "output_transparent_poly.png") fill_area = {"polygon": [[160, 110], [240, 110], [200, 190]], "color": None} async with Client(mcp_server) as client: result = await client.call_tool( "fill", { "input_path": test_image_path, "areas": [fill_area], "output_path": output_path, }, ) assert result.data == output_path assert os.path.exists(output_path) img = cv2.imread(output_path, cv2.IMREAD_UNCHANGED) assert img.shape[2] == 4 # Should have alpha channel # Check pixels inside the transparent polygon - all channels should be 0 test_points = [ (140, 200), # Center of polygon (170, 200), # Another point inside (150, 180), # Another point inside ] for y, x in test_points: pixel = img[y, x] assert np.array_equal(pixel, [0, 0, 0, 0]), f"Pixel at ({y}, {x}) inside polygon should have all channels set to 0" # Check a pixel outside the transparent area pixel_outside = img[50, 50] assert pixel_outside[3] == 255 # Alpha should be 255 assert np.array_equal(pixel_outside[:3], [255, 255, 255]) # Should be white @pytest.mark.asyncio async def test_fill_invert_rectangle(self, mcp_server: FastMCP, test_image_path, tmp_path): """Tests the fill tool with invert_areas for a rectangle.""" output_path = str(tmp_path / "output_inverted.png") # Define a rectangle in the center (where the black rectangle is) fill_area = {"x1": 150, "y1": 100, "x2": 250, "y2": 200, "color": [0, 255, 0], "opacity": 1.0} async with Client(mcp_server) as client: result = await client.call_tool( "fill", { "input_path": test_image_path, "areas": [fill_area], "invert_areas": True, "output_path": output_path } ) assert result.data == output_path assert os.path.exists(output_path) img = cv2.imread(output_path) # Center pixel (inside the specified area) should NOT be filled - remains original center_pixel = img[150, 200] assert np.array_equal(center_pixel, [0, 0, 0]) # Should remain black (original) # Pixels outside the area should be filled with green outside_pixel = img[50, 50] assert np.allclose(outside_pixel, [0, 255, 0], atol=2) # Should be green - allow tolerance for JPEG # Another outside pixel edge_pixel = img[250, 350] assert np.array_equal(edge_pixel, [0, 255, 0]) # Should be green @pytest.mark.asyncio async def test_fill_invert_polygon(self, mcp_server: FastMCP, test_image_path, tmp_path): """Tests the fill tool with invert_areas for a polygon.""" output_path = str(tmp_path / "output_inverted_poly.png") # Define a triangle polygon polygon_area = {"polygon": [[160, 110], [240, 110], [200, 190]], "color": [255, 0, 0], "opacity": 0.8} async with Client(mcp_server) as client: result = await client.call_tool( "fill", { "input_path": test_image_path, "areas": [polygon_area], "invert_areas": True, "output_path": output_path } ) assert result.data == output_path assert os.path.exists(output_path) img = cv2.imread(output_path) # Center of polygon (inside the specified area) should NOT be filled poly_center = img[150, 200] assert np.array_equal(poly_center, [0, 0, 0]) # Should remain black # Outside pixels should be filled with blue at 80% opacity # Since original is white [255,255,255], and we're applying blue [255,0,0] at 80% opacity: # Result = 0.8 * [255,0,0] + 0.2 * [255,255,255] = [255, 51, 51] (approximately) outside_pixel = img[50, 50] assert np.allclose(outside_pixel, [255, 51, 51], atol=2) # 80% blue over white @pytest.mark.asyncio async def test_fill_invert_transparent(self, mcp_server: FastMCP, test_image_path, tmp_path): """Tests making everything except a rectangle transparent (background removal) with all channels set to 0.""" output_path = str(tmp_path / "output_bg_removed.png") # Keep only the center rectangle, make everything else transparent keep_area = {"x1": 150, "y1": 100, "x2": 250, "y2": 200, "color": None} async with Client(mcp_server) as client: result = await client.call_tool( "fill", { "input_path": test_image_path, "areas": [keep_area], "invert_areas": True, "output_path": output_path } ) assert result.data == output_path assert os.path.exists(output_path) img = cv2.imread(output_path, cv2.IMREAD_UNCHANGED) assert img.shape[2] == 4 # Should have alpha channel # Inside the kept area - should be opaque (not modified) inside_pixel = img[150, 200] assert inside_pixel[3] == 255 # Alpha should be 255 (opaque) assert np.array_equal(inside_pixel[:3], [0, 0, 0]) # Color preserved # Outside the kept area - should be fully transparent (all channels 0) outside_pixels = [ (50, 50), # Top left (250, 350), # Bottom right (10, 10), # Corner ] for y, x in outside_pixels: pixel = img[y, x] assert np.array_equal(pixel, [0, 0, 0, 0]), f"Pixel at ({y}, {x}) outside kept area should have all channels set to 0" @pytest.mark.asyncio async def test_fill_invert_multiple_areas(self, mcp_server: FastMCP, test_image_path, tmp_path): """Tests invert_areas with multiple areas to keep.""" output_path = str(tmp_path / "output_multi_keep.png") # Keep two areas, fill everything else areas = [ {"x1": 50, "y1": 50, "x2": 100, "y2": 100, "color": [0, 0, 255], "opacity": 1.0}, {"x1": 200, "y1": 150, "x2": 250, "y2": 200} # Will use first area's color ] async with Client(mcp_server) as client: result = await client.call_tool( "fill", { "input_path": test_image_path, "areas": areas, "invert_areas": True, "output_path": output_path } ) assert result.data == output_path assert os.path.exists(output_path) img = cv2.imread(output_path) # First kept area should NOT be filled (remains original) kept_pixel1 = img[75, 75] assert np.array_equal(kept_pixel1, [255, 255, 255]) # Should remain white # Second kept area should NOT be filled (remains original) kept_pixel2 = img[175, 225] assert np.array_equal(kept_pixel2, [0, 0, 0]) # Should remain black # Area between them should be filled with blue between_pixel = img[125, 150] assert np.array_equal(between_pixel, [0, 0, 255]) # Should be blue (BGR format) @pytest.mark.asyncio async def test_fill_invert_complex_polygon(self, mcp_server: FastMCP, test_image_path, tmp_path): """Tests invert_areas with a complex polygon shape to keep.""" output_path = str(tmp_path / "output_complex_keep.png") # Create a star-like polygon star_polygon = { "polygon": [ [200, 50], [220, 100], [270, 100], [230, 130], [250, 180], [200, 150], [150, 180], [170, 130], [130, 100], [180, 100] ], "color": None # Make background transparent } async with Client(mcp_server) as client: result = await client.call_tool( "fill", { "input_path": test_image_path, "areas": [star_polygon], "invert_areas": True, "output_path": output_path } ) assert result.data == output_path assert os.path.exists(output_path) img = cv2.imread(output_path, cv2.IMREAD_UNCHANGED) assert img.shape[2] == 4 # Should have alpha channel # Center of star should be opaque (not modified) star_center = img[115, 200] assert star_center[3] == 255 # Should be opaque # Outside corners should be transparent corner_pixel = img[10, 10] assert corner_pixel[3] == 0 # Should be transparent @pytest.mark.asyncio async def test_fill_invert_single_area_transparent(self, mcp_server: FastMCP, test_image_path, tmp_path): """Tests a simple background removal use case - keep single object, remove background.""" output_path = str(tmp_path / "object_only.png") # Define object boundaries object_area = {"x1": 100, "y1": 80, "x2": 300, "y2": 220, "color": None} async with Client(mcp_server) as client: result = await client.call_tool( "fill", { "input_path": test_image_path, "areas": [object_area], "invert_areas": True, "output_path": output_path } ) assert result.data == output_path assert os.path.exists(output_path) img = cv2.imread(output_path, cv2.IMREAD_UNCHANGED) # Check that image has alpha channel assert img.shape[2] == 4 # Inside object area should be opaque object_pixel = img[150, 200] assert object_pixel[3] == 255 # Outside object area should be transparent bg_pixel = img[10, 10] assert bg_pixel[3] == 0 # Edge case - just outside the object edge_pixel = img[79, 150] # Just above the object assert edge_pixel[3] == 0 @pytest.mark.asyncio async def test_fill_with_mask_path(self, mcp_server: FastMCP, test_image_path, tmp_path): """Tests filling an area using a mask from a file.""" output_path = str(tmp_path / "output_mask_fill.png") mask_path = str(tmp_path / "test_mask.png") # Create a mask image (e.g., a circle) mask_img = np.zeros((300, 400), dtype=np.uint8) cv2.circle(mask_img, (150, 150), 50, 255, -1) cv2.imwrite(mask_path, mask_img) fill_area = {"mask_path": mask_path, "color": [0, 255, 255], "opacity": 1.0} async with Client(mcp_server) as client: result = await client.call_tool( "fill", { "input_path": test_image_path, "areas": [fill_area], "output_path": output_path, }, ) assert result.data == output_path assert os.path.exists(output_path) img = cv2.imread(output_path) # Check a pixel inside the masked area assert np.array_equal(img[150, 150], [0, 255, 255]) # Check a pixel outside the masked area assert np.array_equal(img[50, 50], [255, 255, 255]) class TestFillToolWithJPEG: """Tests for the fill tool with JPEG images (no alpha channel).""" @pytest.mark.asyncio async def test_fill_jpeg_to_transparent_rectangle(self, mcp_server: FastMCP, test_jpeg_image_path, tmp_path): """Tests making a rectangular area transparent in a JPEG image with all channels set to 0.""" output_path = str(tmp_path / "output_transparent.png") # Output as PNG to support transparency fill_area = {"x1": 150, "y1": 100, "x2": 250, "y2": 200, "color": None} async with Client(mcp_server) as client: result = await client.call_tool( "fill", { "input_path": test_jpeg_image_path, "areas": [fill_area], "output_path": output_path, }, ) assert result.data == output_path assert os.path.exists(output_path) img = cv2.imread(output_path, cv2.IMREAD_UNCHANGED) assert img.shape[2] == 4 # Should have alpha channel added # Check a pixel inside the transparent area - all channels should be 0 pixel_inside = img[150, 200] assert np.array_equal(pixel_inside, [0, 0, 0, 0]), "All BGRA channels should be 0 for transparent areas" # Check a pixel outside the transparent area pixel_outside = img[50, 50] assert pixel_outside[3] == 255 # Alpha should be 255 assert np.array_equal(pixel_outside[:3], [255, 255, 255]) # Should be white @pytest.mark.asyncio async def test_fill_jpeg_invert_transparent(self, mcp_server: FastMCP, test_jpeg_image_path, tmp_path): """Tests making everything except a rectangle transparent in a JPEG image (background removal) with all channels set to 0.""" output_path = str(tmp_path / "output_bg_removed.png") # Keep only the center rectangle, make everything else transparent keep_area = {"x1": 150, "y1": 100, "x2": 250, "y2": 200, "color": None} async with Client(mcp_server) as client: result = await client.call_tool( "fill", { "input_path": test_jpeg_image_path, "areas": [keep_area], "invert_areas": True, "output_path": output_path } ) assert result.data == output_path assert os.path.exists(output_path) img = cv2.imread(output_path, cv2.IMREAD_UNCHANGED) assert img.shape[2] == 4 # Should have alpha channel # Inside the kept area - should be opaque (not modified) inside_pixel = img[150, 200] assert inside_pixel[3] == 255 # Alpha should be 255 (opaque) # Check the red circle is preserved (use allclose due to JPEG compression) circle_center = img[150, 200] assert circle_center[3] == 255 # Should be opaque assert np.allclose(circle_center[:3], [0, 0, 255], atol=2) # Should be red (BGR) - allow tolerance for JPEG # Outside the kept area - should be fully transparent (all channels 0) outside_pixels = [ (50, 50), # Top left (10, 10), # Corner (250, 350), # Bottom right ] for y, x in outside_pixels: pixel = img[y, x] assert np.array_equal(pixel, [0, 0, 0, 0]), f"Pixel at ({y}, {x}) outside kept area should have all channels set to 0" @pytest.mark.asyncio async def test_fill_jpeg_invert_with_color(self, mcp_server: FastMCP, test_jpeg_image_path, tmp_path): """Tests invert_areas with color fill on a JPEG image.""" output_path = str(tmp_path / "output_inverted_color.jpg") # Keep as JPEG # Define a rectangle in the center fill_area = {"x1": 150, "y1": 100, "x2": 250, "y2": 200, "color": [0, 255, 0], "opacity": 1.0} async with Client(mcp_server) as client: result = await client.call_tool( "fill", { "input_path": test_jpeg_image_path, "areas": [fill_area], "invert_areas": True, "output_path": output_path } ) assert result.data == output_path assert os.path.exists(output_path) img = cv2.imread(output_path) # Center pixel (inside the specified area) should NOT be filled center_pixel = img[150, 200] assert np.allclose(center_pixel, [0, 0, 255], atol=2) # Should remain red - allow tolerance for JPEG # Pixels outside the area should be filled with green outside_pixel = img[50, 50] assert np.allclose(outside_pixel, [0, 255, 0], atol=2) # Should be green - allow tolerance for JPEG @pytest.mark.asyncio async def test_fill_jpeg_multiple_transparent_areas(self, mcp_server: FastMCP, test_jpeg_image_path, tmp_path): """Tests multiple transparent areas on a JPEG image with all channels set to 0.""" output_path = str(tmp_path / "output_multi_transparent.png") areas = [ {"x1": 50, "y1": 50, "x2": 100, "y2": 100, "color": None}, {"polygon": [[250, 150], [350, 150], [300, 250]], "color": None} ] async with Client(mcp_server) as client: result = await client.call_tool( "fill", { "input_path": test_jpeg_image_path, "areas": areas, "output_path": output_path } ) assert result.data == output_path assert os.path.exists(output_path) img = cv2.imread(output_path, cv2.IMREAD_UNCHANGED) assert img.shape[2] == 4 # Should have alpha channel # First transparent area - all channels should be 0 pixel_area1 = img[75, 75] assert np.array_equal(pixel_area1, [0, 0, 0, 0]), "First transparent area should have all channels set to 0" # Second transparent area (inside polygon) - all channels should be 0 pixel_area2 = img[180, 300] assert np.array_equal(pixel_area2, [0, 0, 0, 0]), "Second transparent area should have all channels set to 0" # Non-transparent area pixel_normal = img[150, 200] assert pixel_normal[3] == 255 # Should be opaque assert np.allclose(pixel_normal[:3], [0, 0, 255], atol=2) # Should be red (from the circle)

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/sunriseapps/imagesorcery-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server