RobertSpano commited on
Commit
f05f7be
·
verified ·
1 Parent(s): f943d69

Upload folder using huggingface_hub

Browse files
Files changed (3) hide show
  1. README.md +6 -7
  2. app.py +196 -0
  3. requirements.txt +4 -0
README.md CHANGED
@@ -1,12 +1,11 @@
1
  ---
2
- title: Sam3 Floorplan
3
- emoji:
4
- colorFrom: yellow
5
- colorTo: pink
6
  sdk: gradio
7
- sdk_version: 6.10.0
 
8
  app_file: app.py
9
  pinned: false
10
  ---
11
-
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
1
  ---
2
+ title: SAM3 Floor Plan Detection
3
+ emoji: 🏠
4
+ colorFrom: blue
5
+ colorTo: purple
6
  sdk: gradio
7
+ sdk_version: 4.44.0
8
+ python_version: "3.11"
9
  app_file: app.py
10
  pinned: false
11
  ---
 
 
app.py ADDED
@@ -0,0 +1,196 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ SAM3 Floor Plan Detection — Lightweight proxy
3
+ Calls the SAM3 demo space for segmentation, converts masks to JSON coordinates.
4
+ """
5
+
6
+ import gradio as gr
7
+ import numpy as np
8
+ from PIL import Image
9
+ from gradio_client import Client, handle_file
10
+ import cv2
11
+ import json
12
+ import time
13
+ import tempfile
14
+ import os
15
+
16
+ SAM3_DEMO = "prithivMLmods/SAM3-Demo"
17
+
18
+
19
+ def mask_to_lines(mask_img: np.ndarray, min_length: int = 20) -> list:
20
+ """Convert a segmentation mask image to line segments."""
21
+ # Convert to grayscale if needed
22
+ if len(mask_img.shape) == 3:
23
+ gray = cv2.cvtColor(mask_img, cv2.COLOR_RGB2GRAY)
24
+ else:
25
+ gray = mask_img
26
+
27
+ # Threshold to binary — segmented regions are colored, background is not
28
+ _, binary = cv2.threshold(gray, 10, 255, cv2.THRESH_BINARY)
29
+
30
+ # Skeletonize to get thin lines
31
+ try:
32
+ skeleton = cv2.ximgproc.thinning(binary)
33
+ except AttributeError:
34
+ # Fallback: use Canny edge detection
35
+ skeleton = cv2.Canny(binary, 50, 150)
36
+
37
+ # Detect line segments
38
+ lines = cv2.HoughLinesP(
39
+ skeleton, rho=1, theta=np.pi / 180,
40
+ threshold=25, minLineLength=min_length, maxLineGap=15,
41
+ )
42
+
43
+ if lines is None:
44
+ return []
45
+
46
+ result = []
47
+ for line in lines:
48
+ x1, y1, x2, y2 = line[0]
49
+ result.append({"position": [[int(x1), int(y1)], [int(x2), int(y2)]]})
50
+
51
+ return merge_close_lines(result)
52
+
53
+
54
+ def mask_to_bboxes(mask_img: np.ndarray, min_area: int = 80) -> list:
55
+ """Convert a segmentation mask image to bounding boxes."""
56
+ if len(mask_img.shape) == 3:
57
+ gray = cv2.cvtColor(mask_img, cv2.COLOR_RGB2GRAY)
58
+ else:
59
+ gray = mask_img
60
+
61
+ _, binary = cv2.threshold(gray, 10, 255, cv2.THRESH_BINARY)
62
+ contours, _ = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
63
+
64
+ bboxes = []
65
+ for contour in contours:
66
+ if cv2.contourArea(contour) < min_area:
67
+ continue
68
+ x, y, w, h = cv2.boundingRect(contour)
69
+ bboxes.append({"bbox": [int(x), int(y), int(x + w), int(y + h)]})
70
+
71
+ return bboxes
72
+
73
+
74
+ def merge_close_lines(lines: list, threshold: int = 10) -> list:
75
+ """Merge line segments that are close and roughly parallel."""
76
+ if not lines:
77
+ return lines
78
+
79
+ merged = []
80
+ used = set()
81
+
82
+ for i, la in enumerate(lines):
83
+ if i in used:
84
+ continue
85
+ p = la["position"]
86
+ x1, y1, x2, y2 = p[0][0], p[0][1], p[1][0], p[1][1]
87
+ dx, dy = abs(x2 - x1), abs(y2 - y1)
88
+ horiz = dx > dy
89
+
90
+ if horiz and dy < 5:
91
+ avg_y = (y1 + y2) // 2
92
+ y1 = y2 = avg_y
93
+ elif not horiz and dx < 5:
94
+ avg_x = (x1 + x2) // 2
95
+ x1 = x2 = avg_x
96
+
97
+ for j, lb in enumerate(lines):
98
+ if j <= i or j in used:
99
+ continue
100
+ q = lb["position"]
101
+ bx1, by1, bx2, by2 = q[0][0], q[0][1], q[1][0], q[1][1]
102
+ bdx, bdy = abs(bx2 - bx1), abs(by2 - by1)
103
+ b_horiz = bdx > bdy
104
+
105
+ if horiz != b_horiz:
106
+ continue
107
+ if horiz and abs(y1 - (by1 + by2) // 2) < threshold:
108
+ x1, x2 = min(x1, bx1, bx2), max(x2, bx1, bx2)
109
+ used.add(j)
110
+ elif not horiz and abs(x1 - (bx1 + bx2) // 2) < threshold:
111
+ y1, y2 = min(y1, by1, by2), max(y2, by1, by2)
112
+ used.add(j)
113
+
114
+ merged.append({"position": [[x1, y1], [x2, y2]]})
115
+ return merged
116
+
117
+
118
+ def detect_floor_plan(image: Image.Image) -> dict:
119
+ """Run SAM3 on a floor plan image via the demo space."""
120
+ if image is None:
121
+ return {"error": "No image provided"}
122
+
123
+ start = time.time()
124
+ image = image.convert("RGB")
125
+ w, h = image.size
126
+ print(f"[SAM3] Processing {w}x{h} image...")
127
+
128
+ # Save image to temp file for gradio_client
129
+ tmp = tempfile.NamedTemporaryFile(suffix=".png", delete=False)
130
+ image.save(tmp.name)
131
+ tmp.close()
132
+
133
+ results = {"walls": [], "doors": [], "windows": [], "rooms": [],
134
+ "_imgWidth": w, "_imgHeight": h}
135
+
136
+ try:
137
+ client = Client(SAM3_DEMO)
138
+
139
+ # Detect walls ("black line")
140
+ print("[SAM3] Detecting walls...")
141
+ try:
142
+ wall_result = client.predict(
143
+ source_img=handle_file(tmp.name),
144
+ text_query="black line",
145
+ api_name="/run_image_segmentation",
146
+ )
147
+ # Result is a tuple: (output_image_path, ...)
148
+ if wall_result and isinstance(wall_result, (tuple, list)):
149
+ mask_path = wall_result[0] if isinstance(wall_result[0], str) else wall_result[0].get("path", "")
150
+ if mask_path and os.path.exists(mask_path):
151
+ mask_img = cv2.imread(mask_path)
152
+ if mask_img is not None:
153
+ results["walls"] = mask_to_lines(mask_img)
154
+ print(f"[SAM3] Found {len(results['walls'])} walls")
155
+ except Exception as e:
156
+ print(f"[SAM3] Wall detection error: {e}")
157
+
158
+ # Detect doors ("curved line")
159
+ print("[SAM3] Detecting doors...")
160
+ try:
161
+ door_result = client.predict(
162
+ source_img=handle_file(tmp.name),
163
+ text_query="curved line",
164
+ api_name="/run_image_segmentation",
165
+ )
166
+ if door_result and isinstance(door_result, (tuple, list)):
167
+ mask_path = door_result[0] if isinstance(door_result[0], str) else door_result[0].get("path", "")
168
+ if mask_path and os.path.exists(mask_path):
169
+ mask_img = cv2.imread(mask_path)
170
+ if mask_img is not None:
171
+ results["doors"] = mask_to_bboxes(mask_img)
172
+ print(f"[SAM3] Found {len(results['doors'])} doors")
173
+ except Exception as e:
174
+ print(f"[SAM3] Door detection error: {e}")
175
+
176
+ finally:
177
+ os.unlink(tmp.name)
178
+
179
+ elapsed = time.time() - start
180
+ results["_elapsed"] = round(elapsed, 2)
181
+ results["_source"] = "sam3"
182
+ print(f"[SAM3] Done in {elapsed:.1f}s")
183
+
184
+ return results
185
+
186
+
187
+ demo = gr.Interface(
188
+ fn=detect_floor_plan,
189
+ inputs=gr.Image(type="pil", label="Floor Plan Image"),
190
+ outputs=gr.JSON(label="Detected Elements"),
191
+ title="SAM3 Floor Plan Detection",
192
+ description="Detects walls and doors in floor plan images using Meta SAM3.",
193
+ )
194
+
195
+ if __name__ == "__main__":
196
+ demo.launch(server_name="0.0.0.0", server_port=7860)
requirements.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ gradio_client
2
+ opencv-python-headless
3
+ numpy
4
+ Pillow