sam3-floorplan / app.py
RobertSpano's picture
Upload folder using huggingface_hub
f05f7be verified
Raw
History Blame Contribute Delete
6.44 kB
"""
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)