Spaces:
Paused
Paused
Update app.py
Browse files
app.py
CHANGED
|
@@ -66,17 +66,21 @@ import gradio as gr
|
|
| 66 |
# room_validation, mask_to_polygon. Falls back to the inline port only if import
|
| 67 |
# fails. Force the port with USE_DEEPPLAN_WALLS=0.
|
| 68 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 69 |
-
#
|
| 70 |
-
#
|
|
|
|
|
|
|
| 71 |
try:
|
| 72 |
-
import
|
| 73 |
-
|
| 74 |
-
|
| 75 |
except Exception as _pexc:
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
print(f"[pdf]
|
|
|
|
|
|
|
| 80 |
|
| 81 |
_real_run_walls_only = None
|
| 82 |
_HAS_PIPELINE = False
|
|
@@ -2102,67 +2106,73 @@ def _export_files(overlay_rgb: np.ndarray, recs: List[Dict[str, Any]],
|
|
| 2102 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 2103 |
# Gradio callback
|
| 2104 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 2105 |
-
def
|
| 2106 |
-
|
| 2107 |
-
|
| 2108 |
-
|
| 2109 |
-
|
| 2110 |
-
|
| 2111 |
-
Returns None
|
| 2112 |
-
falls back to the raster pipeline instead of failing.
|
| 2113 |
"""
|
| 2114 |
if pdf_file is None:
|
| 2115 |
return None
|
| 2116 |
-
if not
|
| 2117 |
-
log.append(f"[pdf]
|
|
|
|
| 2118 |
return None
|
| 2119 |
path = getattr(pdf_file, "name", None) or str(pdf_file)
|
| 2120 |
try:
|
| 2121 |
-
|
| 2122 |
-
data = fh.read()
|
| 2123 |
-
doc = pdf_walls.open_document(data)
|
| 2124 |
except Exception as exc:
|
| 2125 |
-
log.append(f"[pdf] {exc}
|
| 2126 |
return None
|
| 2127 |
-
|
| 2128 |
-
idx = max(0, min(int(page_no) - 1, doc.page_count - 1))
|
| 2129 |
-
if idx != int(page_no) - 1:
|
| 2130 |
-
log.append(f"[pdf] page {page_no} out of range; using page {idx + 1} of {doc.page_count}")
|
| 2131 |
-
page = doc[idx]
|
| 2132 |
-
|
| 2133 |
try:
|
| 2134 |
-
|
| 2135 |
-
|
| 2136 |
-
min_thickness_mm=float(min_thk_mm), max_thickness_mm=float(max_thk_mm),
|
| 2137 |
-
min_wall_length_mm=float(min_wall_len_mm),
|
| 2138 |
-
use_strokes=bool(use_strokes), use_pairs=bool(use_pairs),
|
| 2139 |
-
use_fills=bool(use_fills))
|
| 2140 |
-
log += plog
|
| 2141 |
-
if not info.get("vector") or info.get("wall_px", 0) == 0:
|
| 2142 |
-
log.append("[pdf] falling back to the raster pipeline")
|
| 2143 |
return None
|
| 2144 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2145 |
except Exception as exc:
|
| 2146 |
-
log.append(f"[pdf]
|
| 2147 |
return None
|
| 2148 |
-
|
| 2149 |
-
|
| 2150 |
-
|
| 2151 |
-
|
| 2152 |
-
|
| 2153 |
-
mask = mask[:cropped.shape[0], :cropped.shape[1]]
|
| 2154 |
-
if mask.shape[:2] != cropped.shape[:2]:
|
| 2155 |
-
mask = cv2.resize(mask, (cropped.shape[1], cropped.shape[0]),
|
| 2156 |
-
interpolation=cv2.INTER_NEAREST)
|
| 2157 |
-
log.append("[pdf] source: VECTOR geometry β thresholds are physical (mm), "
|
| 2158 |
-
"not pixels, so render DPI does not affect the result")
|
| 2159 |
-
return cropped, mask, int(render_dpi)
|
| 2160 |
|
| 2161 |
|
| 2162 |
def segment(image: Optional[Image.Image],
|
| 2163 |
pdf_file: Any, pdf_page: int, pdf_dpi: int,
|
| 2164 |
-
pdf_min_thk: float, pdf_max_thk: float, pdf_min_len: float,
|
| 2165 |
-
pdf_strokes: bool, pdf_pairs: bool, pdf_fills: bool,
|
| 2166 |
maxdim: int, scale_denom: int, dpi: int,
|
| 2167 |
ocr_erase: bool, sam_min_stroke: int, seal_px: int, wall_thicken: int,
|
| 2168 |
n_pos: int, n_neg_wall: int, n_neg_neigh: int, neg_reach: int,
|
|
@@ -2175,43 +2185,35 @@ def segment(image: Optional[Image.Image],
|
|
| 2175 |
merge_iou: float, contain_max: float):
|
| 2176 |
if image is None and pdf_file is None:
|
| 2177 |
return (None, None, None, None, None, [], [], [],
|
| 2178 |
-
"Upload a floor plan image
|
| 2179 |
None, None, None)
|
| 2180 |
t0 = time.time()
|
| 2181 |
log: List[str] = []
|
| 2182 |
|
| 2183 |
-
#
|
| 2184 |
-
#
|
| 2185 |
-
#
|
| 2186 |
-
|
| 2187 |
-
|
| 2188 |
-
|
| 2189 |
-
|
| 2190 |
-
|
| 2191 |
-
|
| 2192 |
-
|
| 2193 |
-
|
| 2194 |
-
|
| 2195 |
-
box_pad, use_box, hard_clip,
|
| 2196 |
-
min_area_px, max_area_frac, enclosure_min,
|
| 2197 |
-
min_region_iou, max_leak, leak_penalty,
|
| 2198 |
-
fallback_region, min_solidity, min_extent,
|
| 2199 |
-
max_vertices, min_axis_frac, frag_max,
|
| 2200 |
-
merge_iou, contain_max)
|
| 2201 |
-
if image is None:
|
| 2202 |
-
return (None, None, None, None, None, [], [], [],
|
| 2203 |
-
"\n".join(log + ["No usable vector geometry and no image uploaded."]),
|
| 2204 |
-
None, None, None)
|
| 2205 |
|
| 2206 |
-
|
| 2207 |
-
|
| 2208 |
-
|
| 2209 |
-
|
| 2210 |
-
|
| 2211 |
-
|
| 2212 |
-
|
| 2213 |
-
|
| 2214 |
-
|
|
|
|
|
|
|
| 2215 |
eff_scale = int(scale_denom) if scale_denom else 100
|
| 2216 |
# Wall extraction is defined in pixels and ignores this; DPI only converts the
|
| 2217 |
# finished room areas to mΒ².
|
|
@@ -2352,11 +2354,12 @@ with gr.Blocks(title=f"DeepPlan β Automatic Room Segmentation v{SERVICE_VERSIO
|
|
| 2352 |
)
|
| 2353 |
with gr.Row():
|
| 2354 |
with gr.Column(scale=1):
|
| 2355 |
-
inp = gr.Image(type="pil", label="Floor plan (
|
| 2356 |
pdf_file = gr.File(
|
| 2357 |
-
label="PDF
|
| 2358 |
-
|
| 2359 |
-
|
|
|
|
| 2360 |
maxdim = gr.Slider(800, 6000, value=2000, step=100,
|
| 2361 |
label="Max SAM size (px) β walls always full-res; this downscales SAM only")
|
| 2362 |
with gr.Row():
|
|
@@ -2368,32 +2371,18 @@ with gr.Blocks(title=f"DeepPlan β Automatic Room Segmentation v{SERVICE_VERSIO
|
|
| 2368 |
"is in pixels")
|
| 2369 |
btn = gr.Button("Segment rooms", variant="primary")
|
| 2370 |
|
| 2371 |
-
with gr.Accordion("0 Β· PDF
|
| 2372 |
gr.Markdown(
|
| 2373 |
-
"A
|
| 2374 |
-
"
|
| 2375 |
-
"
|
| 2376 |
-
"
|
| 2377 |
-
"
|
| 2378 |
-
"
|
|
|
|
| 2379 |
pdf_page = gr.Number(value=1, precision=0, label="Page")
|
| 2380 |
-
pdf_dpi = gr.Slider(72, 600, value=
|
| 2381 |
-
label="Render DPI β
|
| 2382 |
-
"geometry is drawn, not what is detected")
|
| 2383 |
-
pdf_min_thk = gr.Slider(20, 400, value=75, step=5,
|
| 2384 |
-
label="Min wall thickness (mm)")
|
| 2385 |
-
pdf_max_thk = gr.Slider(100, 1200, value=500, step=10,
|
| 2386 |
-
label="Max wall thickness (mm)")
|
| 2387 |
-
pdf_min_len = gr.Slider(20, 3000, value=150, step=10,
|
| 2388 |
-
label="Min wall length (mm) β low, because CAD emits "
|
| 2389 |
-
"a wall face as many short segments which are "
|
| 2390 |
-
"welded back together by the tracer")
|
| 2391 |
-
pdf_strokes = gr.Checkbox(value=True,
|
| 2392 |
-
label="A Β· heavy single strokes")
|
| 2393 |
-
pdf_pairs = gr.Checkbox(value=True,
|
| 2394 |
-
label="B Β· double-line walls (two parallel faces)")
|
| 2395 |
-
pdf_fills = gr.Checkbox(value=True,
|
| 2396 |
-
label="C Β· pochΓ© fills (long thin filled regions)")
|
| 2397 |
|
| 2398 |
with gr.Accordion("1 Β· Structural walls", open=True):
|
| 2399 |
ocr_erase = gr.Checkbox(
|
|
@@ -2497,8 +2486,7 @@ with gr.Blocks(title=f"DeepPlan β Automatic Room Segmentation v{SERVICE_VERSIO
|
|
| 2497 |
btn.click(
|
| 2498 |
segment,
|
| 2499 |
[inp,
|
| 2500 |
-
pdf_file, pdf_page, pdf_dpi,
|
| 2501 |
-
pdf_strokes, pdf_pairs, pdf_fills,
|
| 2502 |
maxdim, scale_denom, dpi,
|
| 2503 |
ocr_erase, sam_min_stroke, seal_px, wall_thicken,
|
| 2504 |
n_pos, n_neg_wall, n_neg_neigh, neg_reach, box_pad, use_box, hard_clip,
|
|
|
|
| 66 |
# room_validation, mask_to_polygon. Falls back to the inline port only if import
|
| 67 |
# fails. Force the port with USE_DEEPPLAN_WALLS=0.
|
| 68 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 69 |
+
# PDF input. A PDF is RENDERED TO AN IMAGE and then goes through exactly the same
|
| 70 |
+
# wall extraction as an uploaded image β one code path, one set of thresholds, and
|
| 71 |
+
# no dependence on a sibling module, which is what makes it work on a Space where
|
| 72 |
+
# only this file is deployed. Needs pymupdf (`pip install pymupdf`); nothing else.
|
| 73 |
try:
|
| 74 |
+
import fitz # type: ignore # pymupdf
|
| 75 |
+
_HAS_FITZ = True
|
| 76 |
+
_FITZ_ERR = ""
|
| 77 |
except Exception as _pexc:
|
| 78 |
+
fitz = None # type: ignore
|
| 79 |
+
_HAS_FITZ = False
|
| 80 |
+
_FITZ_ERR = f"{type(_pexc).__name__}: {_pexc}"
|
| 81 |
+
print(f"[pdf] render-to-image: {'available' if _HAS_FITZ else 'OFF (' + _FITZ_ERR + ')'}")
|
| 82 |
+
|
| 83 |
+
PDF_MAX_DIM = int(os.environ.get("PDF_MAX_DIM", "12000")) # cap the rendered page's long edge
|
| 84 |
|
| 85 |
_real_run_walls_only = None
|
| 86 |
_HAS_PIPELINE = False
|
|
|
|
| 2106 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 2107 |
# Gradio callback
|
| 2108 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 2109 |
+
def _pdf_to_image(pdf_file: Any, page_no: int, render_dpi: int,
|
| 2110 |
+
log: List[str]) -> Optional[np.ndarray]:
|
| 2111 |
+
"""Render one PDF page to a BGR image.
|
| 2112 |
+
|
| 2113 |
+
The page becomes an ordinary raster and then goes through the identical wall
|
| 2114 |
+
extraction an uploaded image would β the PDF is an input format here, not a
|
| 2115 |
+
separate pipeline. Returns None (having logged why) if it cannot render.
|
|
|
|
| 2116 |
"""
|
| 2117 |
if pdf_file is None:
|
| 2118 |
return None
|
| 2119 |
+
if not _HAS_FITZ:
|
| 2120 |
+
log.append(f"[pdf] cannot render β pymupdf missing ({_FITZ_ERR}); "
|
| 2121 |
+
f"pip install pymupdf, or upload the sheet as an image")
|
| 2122 |
return None
|
| 2123 |
path = getattr(pdf_file, "name", None) or str(pdf_file)
|
| 2124 |
try:
|
| 2125 |
+
doc = fitz.open(path)
|
|
|
|
|
|
|
| 2126 |
except Exception as exc:
|
| 2127 |
+
log.append(f"[pdf] could not open ({type(exc).__name__}: {exc})")
|
| 2128 |
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2129 |
try:
|
| 2130 |
+
if doc.page_count < 1:
|
| 2131 |
+
log.append("[pdf] document has no pages")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2132 |
return None
|
| 2133 |
+
idx = max(0, min(int(page_no) - 1, doc.page_count - 1))
|
| 2134 |
+
if idx != int(page_no) - 1:
|
| 2135 |
+
log.append(f"[pdf] page {page_no} out of range; using page {idx + 1} "
|
| 2136 |
+
f"of {doc.page_count}")
|
| 2137 |
+
page = doc[idx]
|
| 2138 |
+
|
| 2139 |
+
# Cap the render so a big sheet at a high DPI cannot exhaust memory before
|
| 2140 |
+
# anything is extracted. The cap also honours EXTRACT_MAX_DIM: extraction
|
| 2141 |
+
# downscales to it regardless, so rendering larger only costs memory β
|
| 2142 |
+
# an A1 at 600 dpi is a 1.2 GB array that is then thrown away. Raise
|
| 2143 |
+
# EXTRACT_MAX_DIM if you want the finer render to actually be used.
|
| 2144 |
+
cap = min(PDF_MAX_DIM, EXTRACT_MAX_DIM) if EXTRACT_MAX_DIM else PDF_MAX_DIM
|
| 2145 |
+
dpi = max(36, int(render_dpi))
|
| 2146 |
+
rect = page.rect
|
| 2147 |
+
long_pt = max(rect.width, rect.height)
|
| 2148 |
+
if long_pt > 0 and long_pt * dpi / 72.0 > cap:
|
| 2149 |
+
dpi = max(36, int(cap * 72.0 / long_pt))
|
| 2150 |
+
log.append(f"[pdf] {int(render_dpi)} dpi would exceed the {cap}px working "
|
| 2151 |
+
f"size β rendering at {dpi} dpi instead")
|
| 2152 |
+
|
| 2153 |
+
pix = page.get_pixmap(matrix=fitz.Matrix(dpi / 72.0, dpi / 72.0), alpha=False)
|
| 2154 |
+
img = np.frombuffer(pix.samples, dtype=np.uint8).reshape(pix.height, pix.width, pix.n)
|
| 2155 |
+
if pix.n == 4:
|
| 2156 |
+
bgr = cv2.cvtColor(img, cv2.COLOR_RGBA2BGR)
|
| 2157 |
+
elif pix.n == 3:
|
| 2158 |
+
bgr = cv2.cvtColor(img, cv2.COLOR_RGB2BGR)
|
| 2159 |
+
else:
|
| 2160 |
+
bgr = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)
|
| 2161 |
+
log.append(f"[pdf] page {idx + 1}/{doc.page_count} rendered at {dpi} dpi β "
|
| 2162 |
+
f"{bgr.shape[1]}x{bgr.shape[0]} px; extracting walls from the image")
|
| 2163 |
+
return bgr
|
| 2164 |
except Exception as exc:
|
| 2165 |
+
log.append(f"[pdf] render failed ({type(exc).__name__}: {exc})")
|
| 2166 |
return None
|
| 2167 |
+
finally:
|
| 2168 |
+
try:
|
| 2169 |
+
doc.close()
|
| 2170 |
+
except Exception:
|
| 2171 |
+
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2172 |
|
| 2173 |
|
| 2174 |
def segment(image: Optional[Image.Image],
|
| 2175 |
pdf_file: Any, pdf_page: int, pdf_dpi: int,
|
|
|
|
|
|
|
| 2176 |
maxdim: int, scale_denom: int, dpi: int,
|
| 2177 |
ocr_erase: bool, sam_min_stroke: int, seal_px: int, wall_thicken: int,
|
| 2178 |
n_pos: int, n_neg_wall: int, n_neg_neigh: int, neg_reach: int,
|
|
|
|
| 2185 |
merge_iou: float, contain_max: float):
|
| 2186 |
if image is None and pdf_file is None:
|
| 2187 |
return (None, None, None, None, None, [], [], [],
|
| 2188 |
+
"Upload a floor plan image or a PDF.",
|
| 2189 |
None, None, None)
|
| 2190 |
t0 = time.time()
|
| 2191 |
log: List[str] = []
|
| 2192 |
|
| 2193 |
+
# A PDF is rendered to an image and then treated exactly like an uploaded
|
| 2194 |
+
# image β same crop, same wall extraction, same thresholds. The PDF is an
|
| 2195 |
+
# input format, not a second pipeline.
|
| 2196 |
+
bgr: Optional[np.ndarray] = None
|
| 2197 |
+
if pdf_file is not None:
|
| 2198 |
+
bgr = _pdf_to_image(pdf_file, pdf_page, pdf_dpi, log)
|
| 2199 |
+
if bgr is None and image is None:
|
| 2200 |
+
return (None, None, None, None, None, [], [], [],
|
| 2201 |
+
"\n".join(log + ["Could not render the PDF, and no image was uploaded."]),
|
| 2202 |
+
None, None, None)
|
| 2203 |
+
if bgr is not None and image is not None:
|
| 2204 |
+
log.append("[pdf] both a PDF and an image were supplied β using the PDF")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2205 |
|
| 2206 |
+
if bgr is None:
|
| 2207 |
+
src_pil = image
|
| 2208 |
+
if EXTRACT_MAX_DIM and max(image.size) > EXTRACT_MAX_DIM:
|
| 2209 |
+
src_pil = _downscale_pil(image.convert("RGB"), EXTRACT_MAX_DIM)
|
| 2210 |
+
log.append(f"[walls] image > {EXTRACT_MAX_DIM}px β downscaled for memory")
|
| 2211 |
+
bgr = cv2.cvtColor(np.array(src_pil.convert("RGB")), cv2.COLOR_RGB2BGR)
|
| 2212 |
+
elif EXTRACT_MAX_DIM and max(bgr.shape[:2]) > EXTRACT_MAX_DIM:
|
| 2213 |
+
s = EXTRACT_MAX_DIM / float(max(bgr.shape[:2]))
|
| 2214 |
+
bgr = cv2.resize(bgr, (max(1, int(bgr.shape[1] * s)), max(1, int(bgr.shape[0] * s))),
|
| 2215 |
+
interpolation=cv2.INTER_AREA)
|
| 2216 |
+
log.append(f"[walls] rendered page > {EXTRACT_MAX_DIM}px β downscaled for memory")
|
| 2217 |
eff_scale = int(scale_denom) if scale_denom else 100
|
| 2218 |
# Wall extraction is defined in pixels and ignores this; DPI only converts the
|
| 2219 |
# finished room areas to mΒ².
|
|
|
|
| 2354 |
)
|
| 2355 |
with gr.Row():
|
| 2356 |
with gr.Column(scale=1):
|
| 2357 |
+
inp = gr.Image(type="pil", label="Floor plan (image)", height=320)
|
| 2358 |
pdf_file = gr.File(
|
| 2359 |
+
label="PDF β rendered to an image, then walls extracted from it"
|
| 2360 |
+
if _HAS_FITZ
|
| 2361 |
+
else f"PDF unavailable ({_FITZ_ERR}) β pip install pymupdf",
|
| 2362 |
+
file_types=[".pdf"], interactive=_HAS_FITZ)
|
| 2363 |
maxdim = gr.Slider(800, 6000, value=2000, step=100,
|
| 2364 |
label="Max SAM size (px) β walls always full-res; this downscales SAM only")
|
| 2365 |
with gr.Row():
|
|
|
|
| 2371 |
"is in pixels")
|
| 2372 |
btn = gr.Button("Segment rooms", variant="primary")
|
| 2373 |
|
| 2374 |
+
with gr.Accordion("0 Β· PDF input", open=True):
|
| 2375 |
gr.Markdown(
|
| 2376 |
+
"A PDF page is **rendered to an image**, and the walls are then "
|
| 2377 |
+
"extracted from that image by exactly the same method as a "
|
| 2378 |
+
"direct image upload. Render DPI is the one setting that "
|
| 2379 |
+
"matters: it decides how many pixels the sheet gets, and a wall "
|
| 2380 |
+
"drawn as two thin lines needs enough of them to stay two lines. "
|
| 2381 |
+
"300 dpi on an A1 sheet is a good default; below ~200 the faces "
|
| 2382 |
+
"blur together.")
|
| 2383 |
pdf_page = gr.Number(value=1, precision=0, label="Page")
|
| 2384 |
+
pdf_dpi = gr.Slider(72, 600, value=300, step=8,
|
| 2385 |
+
label="Render DPI β how many pixels the page becomes")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2386 |
|
| 2387 |
with gr.Accordion("1 Β· Structural walls", open=True):
|
| 2388 |
ocr_erase = gr.Checkbox(
|
|
|
|
| 2486 |
btn.click(
|
| 2487 |
segment,
|
| 2488 |
[inp,
|
| 2489 |
+
pdf_file, pdf_page, pdf_dpi,
|
|
|
|
| 2490 |
maxdim, scale_denom, dpi,
|
| 2491 |
ocr_erase, sam_min_stroke, seal_px, wall_thicken,
|
| 2492 |
n_pos, n_neg_wall, n_neg_neigh, neg_reach, box_pad, use_box, hard_clip,
|