Skip to main content
Glama

mcp_3d_relief

by Bigchx
relief.py10.6 kB
import asyncio import io import logging import os import random import string from traceback import print_exc import cv2 import numpy as np from aiohttp import ClientSession from PIL import Image logger = logging.getLogger(__name__) logging.basicConfig( level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s" ) file_handler = logging.FileHandler("relief.log") file_handler.setLevel(logging.INFO) formatter = logging.Formatter("%(asctime)s - %(levelname)s - %(message)s") file_handler.setFormatter(formatter) logger.addHandler(file_handler) async def generate_depth_map(img, detail_level=1.0, invert_depth=False): base_size = 320 * detail_level width, height = img.size ratio = min(base_size / width, base_size / height) new_width, new_height = int(width * ratio), int(height * ratio) img = img.resize((new_width, new_height), Image.BICUBIC) img_array = np.array(img) if len(img_array.shape) == 3: r, g, b = img_array[:, :, 0], img_array[:, :, 1], img_array[:, :, 2] gray = 0.299 * r + 0.587 * g + 0.114 * b else: gray = img_array gray = np.power(gray / 255.0, 1.5) * 255 if invert_depth: gray = 255 - gray depth_map = gray.astype(np.uint8) depth_map = cv2.GaussianBlur(depth_map, (5, 5), 1.5) return depth_map async def generate_stl( depth_map, output_path, model_width=50, model_thickness=5, base_thickness=2.0 ): height, width = depth_map.shape pixel_size = model_width / width with open(output_path, "w") as f: f.write("solid relief_model\n") vertices = np.zeros((height, width)) for y in range(height): for x in range(width): vertices[y, x] = (depth_map[y, x] / 255.0) * model_thickness for y in range(height - 1): for x in range(width - 1): y0 = (height - y - 1) * pixel_size y1 = (height - (y + 1) - 1) * pixel_size x0 = x * pixel_size x1 = (x + 1) * pixel_size write_facet( f, [x0, y0, -base_thickness], [x1, y1, -base_thickness], [x0, y1, -base_thickness], ) write_facet( f, [x0, y0, -base_thickness], [x1, y0, -base_thickness], [x1, y1, -base_thickness], ) for y in range(height - 1): for x in range(width - 1): y0 = (height - y - 1) * pixel_size y1 = (height - (y + 1) - 1) * pixel_size x0 = x * pixel_size x1 = (x + 1) * pixel_size z00 = vertices[y, x] z01 = vertices[y + 1, x] z10 = vertices[y, x + 1] z11 = vertices[y + 1, x + 1] write_facet(f, [x0, y0, z00], [x1, y0, z10], [x0, y1, z01]) write_facet(f, [x1, y0, z10], [x1, y1, z11], [x0, y1, z01]) for x in range(width - 1): x0 = x * pixel_size x1 = (x + 1) * pixel_size y0 = 0 z0 = -base_thickness z1 = vertices[height - 1, x] z2 = vertices[height - 1, x + 1] write_facet(f, [x0, y0, z0], [x1, y0, z0], [x0, y0, z1]) write_facet(f, [x1, y0, z0], [x1, y0, z2], [x0, y0, z1]) y0 = (height - 1) * pixel_size for x in range(width - 1): x0 = x * pixel_size x1 = (x + 1) * pixel_size z0 = -base_thickness z1 = vertices[0, x] z2 = vertices[0, x + 1] write_facet(f, [x0, y0, z0], [x0, y0, z1], [x1, y0, z0]) write_facet(f, [x1, y0, z0], [x1, y0, z2], [x0, y0, z1]) x0 = 0 for y in range(height - 1): y0 = (height - y - 1) * pixel_size y1 = (height - (y + 1) - 1) * pixel_size z0 = -base_thickness z1 = vertices[y, 0] z2 = vertices[y + 1, 0] write_facet(f, [x0, y0, z0], [x0, y0, z1], [x0, y1, z0]) write_facet(f, [x0, y1, z0], [x0, y0, z1], [x0, y1, z2]) x0 = (width - 1) * pixel_size for y in range(height - 1): y0 = (height - y - 1) * pixel_size y1 = (height - (y + 1) - 1) * pixel_size z0 = -base_thickness z1 = vertices[y, width - 1] z2 = vertices[y + 1, width - 1] write_facet(f, [x0, y0, z0], [x0, y1, z0], [x0, y0, z1]) write_facet(f, [x0, y1, z0], [x0, y1, z2], [x0, y0, z1]) f.write("endsolid relief_model\n") return output_path def write_facet(file, v1, v2, v3): a = np.array(v2) - np.array(v1) b = np.array(v3) - np.array(v1) normal = np.cross(a, b) if np.linalg.norm(normal) > 0: normal = normal / np.linalg.norm(normal) file.write(f" facet normal {normal[0]} {normal[1]} {normal[2]}\n") file.write(" outer loop\n") file.write(f" vertex {v1[0]} {v1[1]} {v1[2]}\n") file.write(f" vertex {v2[0]} {v2[1]} {v2[2]}\n") file.write(f" vertex {v3[0]} {v3[1]} {v3[2]}\n") file.write(" endloop\n") file.write(" endfacet\n") async def relief( input_image_path: str = None, input_image: Image.Image = None, detail_level: float = 1.0, model_width: float = 50.0, model_thickness: float = 5.0, base_thickness: float = 2.0, output_dir: str = "./output", skip_depth: bool = False, invert_depth: bool = False, ): logger.info("Processing image...") if input_image_path is not None: if input_image_path.startswith("http://") or input_image_path.startswith( "https://" ): async with ClientSession() as session: async with session.get(input_image_path) as response: if response.status != 200: return { "error": f"Failed to download image from URL: {input_image_path}", "status": "failed", } img_data = await response.read() input_image = Image.open(io.BytesIO(img_data)) elif os.path.isfile(input_image_path): if not os.path.exists(input_image_path): return { "error": f"Input image not found: {input_image_path}", "status": "failed", } input_image = Image.open(input_image_path) else: return { "error": f"Invalid input image path: {input_image_path}", "status": "failed", } elif input_image is None: return {"error": "No input image specified", "status": "failed"} if not os.path.exists(output_dir): os.makedirs(output_dir) try: original_filename = "".join( random.choices(string.ascii_letters + string.digits, k=10) ) logger.info("Preparing image for 3D conversion...") if not skip_depth: img = np.array(input_image.convert("L")) base_size = 320 * detail_level height, width = img.shape ratio = min(base_size / width, base_size / height) new_width, new_height = int(width * ratio), int(height * ratio) depth_map = cv2.resize( img, (new_width, new_height), interpolation=cv2.INTER_CUBIC ) depth_map = cv2.GaussianBlur(depth_map, (3, 3), 0.8) if invert_depth: depth_map = 255 - depth_map logger.info("Using original image directly (skipping depth conversion)...") else: logger.info("Generating depth map...") depth_map = await generate_depth_map( input_image, detail_level, invert_depth ) depth_map_name = f"{original_filename}_depth_map.png" depth_map_path = os.path.join(output_dir, depth_map_name) cv2.imwrite(depth_map_path, depth_map) logger.info("Generating STL file...") stl_name = f"{original_filename}.stl" stl_path = os.path.join(output_dir, stl_name) await generate_stl( depth_map, stl_path, model_width, model_thickness, base_thickness ) ret = { "depth_map_path": os.path.abspath(depth_map_path), "stl_path": os.path.abspath(stl_path), "status": "success", } logger.info(f"result is: {ret}") return ret except Exception as e: error_message = repr(e) print_exc() logger.warning(f"Error processing image: {error_message}") return {"error": error_message, "status": "failed"} if __name__ == "__main__": import argparse parser = argparse.ArgumentParser(description="Generate STL from image") parser.add_argument( "input_image_path", type=str, help="Path to the input image file or URL", ) parser.add_argument( "--detail_level", type=float, default=1.0, help="Detail level for depth map generation", ) parser.add_argument( "--model_width", type=float, default=50.0, help="Width of the 3D model in mm", ) parser.add_argument( "--model_thickness", type=float, default=5.0, help="Thickness of the 3D model in mm", ) parser.add_argument( "--base_thickness", type=float, default=2.0, help="Base thickness of the 3D model in mm", ) parser.add_argument( "--output_dir", type=str, default="output", help="Directory to save the output STL file", ) parser.add_argument( "--skip_depth", action="store_true", help="Skip depth map generation and use the original image directly", ) parser.add_argument( "--invert_depth", action="store_true", help="Invert the depth map", ) args = parser.parse_args() asyncio.run( relief( input_image_path=args.input_image_path, detail_level=args.detail_level, model_width=args.model_width, model_thickness=args.model_thickness, base_thickness=args.base_thickness, output_dir=args.output_dir, skip_depth=args.skip_depth, invert_depth=args.invert_depth, ) )

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/Bigchx/mcp_3d_relief'

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