Spaces:
Runtime error
Runtime error
| """ | |
| Floor Plan Segmentation API β Hugging Face Space | |
| Segments rooms, walls, doors, windows from floor plan images. | |
| Uses Mask2Former for instance segmentation with fallback to | |
| color-based contour detection for robustness. | |
| API endpoint: POST /api/predict | |
| """ | |
| import gradio as gr | |
| import numpy as np | |
| import cv2 | |
| import json | |
| from PIL import Image | |
| import io | |
| import base64 | |
| # βββ Model loading βββββββββββββββββββββββββββββββββββββββββββ | |
| MODEL = None | |
| PROCESSOR = None | |
| USE_MASK2FORMER = False | |
| def load_model(): | |
| """Try loading Mask2Former; fall back to OpenCV contour detection.""" | |
| global MODEL, PROCESSOR, USE_MASK2FORMER | |
| try: | |
| from transformers import AutoImageProcessor, Mask2FormerForInstanceSegmentation | |
| PROCESSOR = AutoImageProcessor.from_pretrained( | |
| "Hyunwoo1605/mask2former-floorplan-instance-segmentation" | |
| ) | |
| MODEL = Mask2FormerForInstanceSegmentation.from_pretrained( | |
| "Hyunwoo1605/mask2former-floorplan-instance-segmentation" | |
| ) | |
| MODEL.eval() | |
| USE_MASK2FORMER = True | |
| print("[INFO] Mask2Former model loaded successfully") | |
| except Exception as e: | |
| print(f"[WARN] Could not load Mask2Former: {e}") | |
| print("[INFO] Using OpenCV contour-based fallback") | |
| USE_MASK2FORMER = False | |
| load_model() | |
| # βββ Mask2Former inference βββββββββββββββββββββββββββββββββββ | |
| def segment_mask2former(image: np.ndarray) -> dict: | |
| """Run Mask2Former instance segmentation on floor plan image.""" | |
| import torch | |
| pil_image = Image.fromarray(cv2.cvtColor(image, cv2.COLOR_BGR2RGB)) | |
| inputs = PROCESSOR(images=pil_image, return_tensors="pt") | |
| with torch.no_grad(): | |
| outputs = MODEL(**inputs) | |
| # Post-process: get instance masks and labels | |
| result = PROCESSOR.post_process_instance_segmentation( | |
| outputs, target_sizes=[pil_image.size[::-1]] | |
| )[0] | |
| rooms = [] | |
| walls = [] | |
| h, w = image.shape[:2] | |
| for seg_info in result["segments_info"]: | |
| mask = (result["segmentation"] == seg_info["id"]).numpy().astype(np.uint8) | |
| label_id = seg_info["label_id"] | |
| score = float(seg_info["score"]) | |
| # Extract contour from mask | |
| contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) | |
| if not contours: | |
| continue | |
| # Largest contour = room boundary | |
| contour = max(contours, key=cv2.contourArea) | |
| area_px = cv2.contourArea(contour) | |
| if area_px < 100: # skip tiny segments | |
| continue | |
| # Simplify contour to polygon | |
| epsilon = 0.02 * cv2.arcLength(contour, True) | |
| approx = cv2.approxPolyDP(contour, epsilon, True) | |
| boundary = [{"x": float(p[0][0]) / w, "y": float(p[0][1]) / h} for p in approx] | |
| # Get label name from model config | |
| label_name = MODEL.config.id2label.get(label_id, f"class_{label_id}") | |
| # Map to room type | |
| room_type = classify_label(label_name) | |
| if room_type == "wall": | |
| # Extract wall segments from contour | |
| for i in range(len(approx)): | |
| j = (i + 1) % len(approx) | |
| walls.append({ | |
| "start": {"x": float(approx[i][0][0]) / w, "y": float(approx[i][0][1]) / h}, | |
| "end": {"x": float(approx[j][0][0]) / w, "y": float(approx[j][0][1]) / h}, | |
| "is_exterior": False, | |
| }) | |
| else: | |
| rooms.append({ | |
| "name": label_name, | |
| "type": room_type, | |
| "boundary": boundary, | |
| "area_estimate_m2": 0, # needs scale info | |
| "has_door": False, | |
| "has_window": False, | |
| "confidence": score, | |
| "floor_type": "parkett", | |
| }) | |
| return {"rooms": rooms, "walls": walls, "doors": [], "windows": [], "method": "mask2former"} | |
| # βββ OpenCV contour-based fallback ββββββββββββββββββββββββββ | |
| def segment_opencv(image: np.ndarray) -> dict: | |
| """ | |
| OpenCV-based floor plan segmentation using adaptive thresholding | |
| and contour detection. Works without GPU or ML models. | |
| """ | |
| h, w = image.shape[:2] | |
| gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) | |
| # Adaptive threshold to detect walls (dark lines on light/white background) | |
| thresh = cv2.adaptiveThreshold( | |
| gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY_INV, 11, 5 | |
| ) | |
| # Morphological operations to clean up wall detection | |
| kernel_close = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3)) | |
| walls_mask = cv2.morphologyEx(thresh, cv2.MORPH_CLOSE, kernel_close, iterations=2) | |
| # Dilate walls slightly to close small gaps | |
| kernel_dilate = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5)) | |
| walls_dilated = cv2.dilate(walls_mask, kernel_dilate, iterations=1) | |
| # Invert to get room regions (white = room interior) | |
| rooms_mask = cv2.bitwise_not(walls_dilated) | |
| # Find room contours | |
| contours, _ = cv2.findContours(rooms_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) | |
| rooms = [] | |
| min_room_area = (w * h) * 0.005 # min 0.5% of image area | |
| max_room_area = (w * h) * 0.5 # max 50% of image area | |
| for i, contour in enumerate(contours): | |
| area = cv2.contourArea(contour) | |
| if area < min_room_area or area > max_room_area: | |
| continue | |
| # Simplify contour | |
| epsilon = 0.015 * cv2.arcLength(contour, True) | |
| approx = cv2.approxPolyDP(contour, epsilon, True) | |
| if len(approx) < 3: | |
| continue | |
| boundary = [{"x": float(p[0][0]) / w, "y": float(p[0][1]) / h} for p in approx] | |
| # Compute bounding rect for aspect ratio | |
| x_r, y_r, w_r, h_r = cv2.boundingRect(approx) | |
| aspect = max(w_r, h_r) / max(min(w_r, h_r), 1) | |
| # Simple room classification by size and shape | |
| relative_area = area / (w * h) | |
| if aspect > 5: | |
| room_type = "hallway" | |
| name = f"Flur {i+1}" | |
| elif relative_area > 0.08: | |
| room_type = "living_room" | |
| name = f"Raum {i+1}" | |
| elif relative_area < 0.02: | |
| room_type = "wc" | |
| name = f"WC/Bad {i+1}" | |
| else: | |
| room_type = "custom" | |
| name = f"Raum {i+1}" | |
| rooms.append({ | |
| "name": name, | |
| "type": room_type, | |
| "boundary": boundary, | |
| "area_estimate_m2": 0, | |
| "has_door": False, | |
| "has_window": False, | |
| "confidence": 0.6, | |
| "floor_type": "parkett", | |
| }) | |
| # Extract wall segments using Hough Line Transform | |
| walls = [] | |
| lines = cv2.HoughLinesP(walls_mask, 1, np.pi / 180, 80, minLineLength=30, maxLineGap=10) | |
| if lines is not None: | |
| for line in lines[:200]: # cap at 200 wall segments | |
| x1, y1, x2, y2 = line[0] | |
| walls.append({ | |
| "start": {"x": float(x1) / w, "y": float(y1) / h}, | |
| "end": {"x": float(x2) / w, "y": float(y2) / h}, | |
| "is_exterior": False, | |
| }) | |
| # Detect doors (arcs / small circular segments) | |
| doors = [] | |
| circles = cv2.HoughCircles( | |
| gray, cv2.HOUGH_GRADIENT, 1, 50, | |
| param1=100, param2=30, minRadius=15, maxRadius=80 | |
| ) | |
| if circles is not None: | |
| for circle in circles[0][:20]: | |
| cx, cy, r = circle | |
| doors.append({ | |
| "position": {"x": float(cx) / w, "y": float(cy) / h}, | |
| "width_mm": int(r * 2 * 10), # rough estimate | |
| "type": "standard", | |
| }) | |
| return {"rooms": rooms, "walls": walls, "doors": doors, "windows": [], "method": "opencv"} | |
| # βββ Label mapping ββββββββββββββββββββββββββββββββββββββββββ | |
| LABEL_MAP = { | |
| "wall": "wall", | |
| "room": "custom", | |
| "living": "living_room", | |
| "living_room": "living_room", | |
| "bedroom": "bedroom", | |
| "bathroom": "bathroom", | |
| "kitchen": "kitchen", | |
| "hallway": "hallway", | |
| "corridor": "hallway", | |
| "closet": "storage", | |
| "storage": "storage", | |
| "balcony": "terrace", | |
| "door": "door", | |
| "window": "window", | |
| "dining": "dining_room", | |
| "office": "study", | |
| "garage": "garage", | |
| "stairs": "staircase", | |
| "toilet": "wc", | |
| "wc": "wc", | |
| "utility": "utility_room", | |
| "laundry": "utility_room", | |
| "entrance": "entrance", | |
| } | |
| def classify_label(label: str) -> str: | |
| """Map model output label to standard room type.""" | |
| label_lower = label.lower().strip() | |
| for key, value in LABEL_MAP.items(): | |
| if key in label_lower: | |
| return value | |
| return "custom" | |
| # βββ Main API function ββββββββββββββββββββββββββββββββββββββ | |
| def analyze_floor_plan(image: np.ndarray) -> dict: | |
| """Analyze a floor plan image and return segmentation results.""" | |
| if image is None: | |
| return {"error": "No image provided", "rooms": [], "walls": [], "doors": [], "windows": []} | |
| # Ensure BGR format | |
| if len(image.shape) == 2: | |
| image = cv2.cvtColor(image, cv2.COLOR_GRAY2BGR) | |
| elif image.shape[2] == 4: | |
| image = cv2.cvtColor(image, cv2.COLOR_RGBA2BGR) | |
| # Run segmentation | |
| if USE_MASK2FORMER: | |
| result = segment_mask2former(image) | |
| else: | |
| result = segment_opencv(image) | |
| result["image_width"] = image.shape[1] | |
| result["image_height"] = image.shape[0] | |
| result["_version"] = "hf-space-v1" | |
| result["_coordSystem"] = "normalized" # all coords 0..1 | |
| result["notes"] = f"Analyzed using {result.get('method', 'unknown')} method. {len(result.get('rooms', []))} rooms detected." | |
| return result | |
| # βββ Gradio Interface ββββββββββββββββββββββββββββββββββββββββ | |
| def gradio_predict(image): | |
| """Gradio wrapper that returns JSON string + annotated image.""" | |
| result = analyze_floor_plan(image) | |
| # Draw annotations on image for visualization | |
| annotated = image.copy() | |
| h, w = annotated.shape[:2] | |
| colors = [ | |
| (66, 133, 244), (234, 67, 53), (251, 188, 4), (52, 168, 83), | |
| (171, 71, 188), (255, 112, 67), (0, 172, 193), (124, 179, 66), | |
| ] | |
| for i, room in enumerate(result.get("rooms", [])): | |
| color = colors[i % len(colors)] | |
| boundary = room.get("boundary", []) | |
| if len(boundary) < 3: | |
| continue | |
| pts = np.array([[int(p["x"] * w), int(p["y"] * h)] for p in boundary], dtype=np.int32) | |
| # Semi-transparent fill | |
| overlay = annotated.copy() | |
| cv2.fillPoly(overlay, [pts], color) | |
| cv2.addWeighted(overlay, 0.3, annotated, 0.7, 0, annotated) | |
| # Boundary outline | |
| cv2.polylines(annotated, [pts], True, color, 2) | |
| # Label | |
| M = cv2.moments(pts) | |
| if M["m00"] > 0: | |
| cx = int(M["m10"] / M["m00"]) | |
| cy = int(M["m01"] / M["m00"]) | |
| label = room.get("name", f"Room {i+1}") | |
| cv2.putText(annotated, label, (cx - 30, cy), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 2) | |
| cv2.putText(annotated, label, (cx - 30, cy), cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, 1) | |
| return annotated, json.dumps(result, indent=2, ensure_ascii=False) | |
| with gr.Blocks(title="Floor Plan Segmentation API") as demo: | |
| gr.Markdown(""" | |
| # Floor Plan Segmentation | |
| Upload a floor plan image to detect rooms, walls, doors, and windows. | |
| **API Usage:** `POST /api/predict` with `{"data": [<base64_image>]}` | |
| """) | |
| with gr.Row(): | |
| with gr.Column(): | |
| input_image = gr.Image(label="Floor Plan", type="numpy") | |
| analyze_btn = gr.Button("Analyze", variant="primary") | |
| with gr.Column(): | |
| output_image = gr.Image(label="Segmentation Result") | |
| output_json = gr.Textbox(label="JSON Result", lines=15, max_lines=30) | |
| analyze_btn.click( | |
| fn=gradio_predict, | |
| inputs=[input_image], | |
| outputs=[output_image, output_json], | |
| api_name="predict", | |
| ) | |
| demo.launch(show_api=True) | |