Pream912 commited on
Commit
02bb7b3
Β·
verified Β·
1 Parent(s): 75a63cc

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +104 -116
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
- # Vector wall extraction from PDF. Optional (needs pymupdf); absent β†’ the PDF tab
70
- # reports why and the raster path is used.
 
 
71
  try:
72
- import pdf_walls # type: ignore
73
- _HAS_PDF = pdf_walls.HAS_FITZ
74
- _PDF_ERR = "" if _HAS_PDF else pdf_walls.FITZ_ERROR
75
  except Exception as _pexc:
76
- pdf_walls = None # type: ignore
77
- _HAS_PDF = False
78
- _PDF_ERR = f"{type(_pexc).__name__}: {_pexc}"
79
- print(f"[pdf] vector wall extraction: {'available' if _HAS_PDF else 'OFF (' + _PDF_ERR + ')'}")
 
 
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 _walls_from_pdf(pdf_file: Any, page_no: int, render_dpi: int, scale_denom: int,
2106
- min_thk_mm: float, max_thk_mm: float, min_wall_len_mm: float,
2107
- use_strokes: bool, use_pairs: bool, use_fills: bool,
2108
- log: List[str]) -> Optional[Tuple[np.ndarray, np.ndarray, int]]:
2109
- """Vector path: PDF page β†’ (cropped BGR, wall mask, effective dpi).
2110
-
2111
- Returns None when it cannot apply, having said why in the log, so the caller
2112
- falls back to the raster pipeline instead of failing.
2113
  """
2114
  if pdf_file is None:
2115
  return None
2116
- if not _HAS_PDF:
2117
- log.append(f"[pdf] vector extraction unavailable ({_PDF_ERR}) β€” using raster path")
 
2118
  return None
2119
  path = getattr(pdf_file, "name", None) or str(pdf_file)
2120
  try:
2121
- with open(path, "rb") as fh:
2122
- data = fh.read()
2123
- doc = pdf_walls.open_document(data)
2124
  except Exception as exc:
2125
- log.append(f"[pdf] {exc} β€” using raster path")
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
- mask, info, plog = pdf_walls.extract_wall_mask(
2135
- page, dpi=int(render_dpi), scale_denominator=int(scale_denom),
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
- page_bgr = pdf_walls.render_page(page, int(render_dpi))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2145
  except Exception as exc:
2146
- log.append(f"[pdf] vector extraction failed ({type(exc).__name__}: {exc}) β€” using raster path")
2147
  return None
2148
-
2149
- # Same title-block crop the raster path applies, derived from the rendered
2150
- # page so the mask and the image stay in one coordinate space.
2151
- cropped, m2 = stage2_crop_drawing(page_bgr)
2152
- log.append(m2)
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, or a PDF for the vector path.",
2179
  None, None, None)
2180
  t0 = time.time()
2181
  log: List[str] = []
2182
 
2183
- # Vector first when a PDF is supplied. A vector PDF carries the wall geometry
2184
- # exactly β€” coordinates, stroke widths, fills β€” so there is nothing to recover
2185
- # by thresholding pixels, and no render DPI for the thresholds to depend on.
2186
- vec = _walls_from_pdf(pdf_file, pdf_page, pdf_dpi, scale_denom,
2187
- pdf_min_thk, pdf_max_thk, pdf_min_len,
2188
- pdf_strokes, pdf_pairs, pdf_fills, log)
2189
- if vec is not None:
2190
- cropped, wall_mask, eff_dpi = vec
2191
- eff_scale = int(scale_denom) if scale_denom else 100
2192
- return _segment_from_walls(cropped, wall_mask, eff_dpi, eff_scale, t0, log,
2193
- maxdim, sam_min_stroke, seal_px, wall_thicken,
2194
- n_pos, n_neg_wall, n_neg_neigh, neg_reach,
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
- # Wall extraction in this app is VECTOR β€” NOT the heuristic Stages 2-4. The
2207
- # blueprint is cropped to drop the title block, then traced to vector walls
2208
- # locally by vtracer; the downstream thick-stroke filter keeps structural walls
2209
- # only. Downscale only as a memory guard for oversized sheets.
2210
- src_pil = image
2211
- if EXTRACT_MAX_DIM and max(image.size) > EXTRACT_MAX_DIM:
2212
- src_pil = _downscale_pil(image.convert("RGB"), EXTRACT_MAX_DIM)
2213
- log.append(f"[walls] image > {EXTRACT_MAX_DIM}px β€” downscaled for memory")
2214
- bgr = cv2.cvtColor(np.array(src_pil.convert("RGB")), cv2.COLOR_RGB2BGR)
 
 
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 (raster path)", height=320)
2356
  pdf_file = gr.File(
2357
- label="PDF (vector path β€” preferred)" if _HAS_PDF
2358
- else f"PDF vector path unavailable ({_PDF_ERR}) β€” pip install pymupdf",
2359
- file_types=[".pdf"], interactive=_HAS_PDF)
 
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 vector walls", open=True):
2372
  gr.Markdown(
2373
- "A vector PDF stores each wall as geometry β€” coordinates, stroke "
2374
- "width, fills β€” so there is nothing to recover by thresholding "
2375
- "pixels and **no resolution to be robust to**. Thresholds below "
2376
- "are millimetres of real building, converted through *Scale 1:N*, "
2377
- "so render DPI does not change the result. Falls back to the "
2378
- "raster path automatically if the page turns out to be a scan.")
 
2379
  pdf_page = gr.Number(value=1, precision=0, label="Page")
2380
- pdf_dpi = gr.Slider(72, 600, value=200, step=8,
2381
- label="Render DPI β€” only how finely the finished "
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, pdf_min_thk, pdf_max_thk, pdf_min_len,
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,