""" SAM3 Floor Plan Detection — Lightweight proxy Calls the SAM3 demo space for segmentation, converts masks to JSON coordinates. """ import gradio as gr import numpy as np from PIL import Image from gradio_client import Client, handle_file import cv2 import json import time import tempfile import os SAM3_DEMO = "prithivMLmods/SAM3-Demo" def mask_to_lines(mask_img: np.ndarray, min_length: int = 20) -> list: """Convert a segmentation mask image to line segments.""" # Convert to grayscale if needed if len(mask_img.shape) == 3: gray = cv2.cvtColor(mask_img, cv2.COLOR_RGB2GRAY) else: gray = mask_img # Threshold to binary — segmented regions are colored, background is not _, binary = cv2.threshold(gray, 10, 255, cv2.THRESH_BINARY) # Skeletonize to get thin lines try: skeleton = cv2.ximgproc.thinning(binary) except AttributeError: # Fallback: use Canny edge detection skeleton = cv2.Canny(binary, 50, 150) # Detect line segments lines = cv2.HoughLinesP( skeleton, rho=1, theta=np.pi / 180, threshold=25, minLineLength=min_length, maxLineGap=15, ) if lines is None: return [] result = [] for line in lines: x1, y1, x2, y2 = line[0] result.append({"position": [[int(x1), int(y1)], [int(x2), int(y2)]]}) return merge_close_lines(result) def mask_to_bboxes(mask_img: np.ndarray, min_area: int = 80) -> list: """Convert a segmentation mask image to bounding boxes.""" if len(mask_img.shape) == 3: gray = cv2.cvtColor(mask_img, cv2.COLOR_RGB2GRAY) else: gray = mask_img _, binary = cv2.threshold(gray, 10, 255, cv2.THRESH_BINARY) contours, _ = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) bboxes = [] for contour in contours: if cv2.contourArea(contour) < min_area: continue x, y, w, h = cv2.boundingRect(contour) bboxes.append({"bbox": [int(x), int(y), int(x + w), int(y + h)]}) return bboxes def merge_close_lines(lines: list, threshold: int = 10) -> list: """Merge line segments that are close and roughly parallel.""" if not lines: return lines merged = [] used = set() for i, la in enumerate(lines): if i in used: continue p = la["position"] x1, y1, x2, y2 = p[0][0], p[0][1], p[1][0], p[1][1] dx, dy = abs(x2 - x1), abs(y2 - y1) horiz = dx > dy if horiz and dy < 5: avg_y = (y1 + y2) // 2 y1 = y2 = avg_y elif not horiz and dx < 5: avg_x = (x1 + x2) // 2 x1 = x2 = avg_x for j, lb in enumerate(lines): if j <= i or j in used: continue q = lb["position"] bx1, by1, bx2, by2 = q[0][0], q[0][1], q[1][0], q[1][1] bdx, bdy = abs(bx2 - bx1), abs(by2 - by1) b_horiz = bdx > bdy if horiz != b_horiz: continue if horiz and abs(y1 - (by1 + by2) // 2) < threshold: x1, x2 = min(x1, bx1, bx2), max(x2, bx1, bx2) used.add(j) elif not horiz and abs(x1 - (bx1 + bx2) // 2) < threshold: y1, y2 = min(y1, by1, by2), max(y2, by1, by2) used.add(j) merged.append({"position": [[x1, y1], [x2, y2]]}) return merged def detect_floor_plan(image: Image.Image) -> dict: """Run SAM3 on a floor plan image via the demo space.""" if image is None: return {"error": "No image provided"} start = time.time() image = image.convert("RGB") w, h = image.size print(f"[SAM3] Processing {w}x{h} image...") # Save image to temp file for gradio_client tmp = tempfile.NamedTemporaryFile(suffix=".png", delete=False) image.save(tmp.name) tmp.close() results = {"walls": [], "doors": [], "windows": [], "rooms": [], "_imgWidth": w, "_imgHeight": h} try: client = Client(SAM3_DEMO) # Detect walls ("black line") print("[SAM3] Detecting walls...") try: wall_result = client.predict( source_img=handle_file(tmp.name), text_query="black line", api_name="/run_image_segmentation", ) # Result is a tuple: (output_image_path, ...) if wall_result and isinstance(wall_result, (tuple, list)): mask_path = wall_result[0] if isinstance(wall_result[0], str) else wall_result[0].get("path", "") if mask_path and os.path.exists(mask_path): mask_img = cv2.imread(mask_path) if mask_img is not None: results["walls"] = mask_to_lines(mask_img) print(f"[SAM3] Found {len(results['walls'])} walls") except Exception as e: print(f"[SAM3] Wall detection error: {e}") # Detect doors ("curved line") print("[SAM3] Detecting doors...") try: door_result = client.predict( source_img=handle_file(tmp.name), text_query="curved line", api_name="/run_image_segmentation", ) if door_result and isinstance(door_result, (tuple, list)): mask_path = door_result[0] if isinstance(door_result[0], str) else door_result[0].get("path", "") if mask_path and os.path.exists(mask_path): mask_img = cv2.imread(mask_path) if mask_img is not None: results["doors"] = mask_to_bboxes(mask_img) print(f"[SAM3] Found {len(results['doors'])} doors") except Exception as e: print(f"[SAM3] Door detection error: {e}") finally: os.unlink(tmp.name) elapsed = time.time() - start results["_elapsed"] = round(elapsed, 2) results["_source"] = "sam3" print(f"[SAM3] Done in {elapsed:.1f}s") return results demo = gr.Interface( fn=detect_floor_plan, inputs=gr.Image(type="pil", label="Floor Plan Image"), outputs=gr.JSON(label="Detected Elements"), title="SAM3 Floor Plan Detection", description="Detects walls and doors in floor plan images using Meta SAM3.", ) if __name__ == "__main__": demo.launch(server_name="0.0.0.0", server_port=7860)