Pream912 commited on
Commit
bc97b83
Β·
verified Β·
1 Parent(s): 3001bfe

Update wall_pipeline.py

Browse files
Files changed (1) hide show
  1. wall_pipeline.py +206 -64
wall_pipeline.py CHANGED
@@ -397,99 +397,241 @@ class WallPipeline:
397
  return result
398
 
399
  # ══════════════════════════════════════════════════════════════════════════
400
- # Stage 4 β€” Extract walls (original)
401
  # ══════════════════════════════════════════════════════════════════════════
402
  def _extract_walls(self, img: np.ndarray) -> np.ndarray:
 
 
 
 
 
 
403
  gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
404
  h, w = gray.shape
405
 
406
- otsu_val, _ = cv2.threshold(gray, 0, 255,
407
- cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)
408
  brightness = float(np.mean(gray))
 
 
 
 
409
  if brightness > 220:
410
- thr = max(200, int(otsu_val * 1.1))
411
  elif brightness < 180:
412
- thr = max(150, int(otsu_val * 0.9))
413
  else:
414
- thr = int(otsu_val)
415
 
416
- _, binary = cv2.threshold(gray, thr, 255, cv2.THRESH_BINARY_INV)
417
 
418
- min_line = max(8, int(0.012 * w))
419
- body_thickness = self._estimate_wall_thickness(binary)
420
  body_thickness = int(np.clip(body_thickness, 9, 30))
421
- self._wall_thickness = body_thickness
422
 
423
- k_h = cv2.getStructuringElement(cv2.MORPH_RECT, (min_line, 1))
424
- k_v = cv2.getStructuringElement(cv2.MORPH_RECT, (1, min_line))
425
 
426
- if _GPU:
427
- # GPU morphology via cupy β€” simulate with erosion+dilation
428
- long_h = _to_cpu(cp.asarray(
429
- cv2.morphologyEx(binary, cv2.MORPH_OPEN, k_h)))
430
- long_v = _to_cpu(cp.asarray(
431
- cv2.morphologyEx(binary, cv2.MORPH_OPEN, k_v)))
432
- else:
433
- long_h = cv2.morphologyEx(binary, cv2.MORPH_OPEN, k_h)
434
- long_v = cv2.morphologyEx(binary, cv2.MORPH_OPEN, k_v)
435
 
436
  orig_walls = cv2.bitwise_or(long_h, long_v)
437
- k_bh = cv2.getStructuringElement(cv2.MORPH_RECT, (1, body_thickness))
438
- k_bv = cv2.getStructuringElement(cv2.MORPH_RECT, (body_thickness, 1))
439
- dilated_h = cv2.dilate(long_h, k_bh)
440
- dilated_v = cv2.dilate(long_v, k_bv)
441
- walls = cv2.bitwise_or(dilated_h, dilated_v)
442
- collision = cv2.bitwise_and(dilated_h, dilated_v)
443
- safe_zone = cv2.bitwise_and(collision, orig_walls)
444
- walls = cv2.bitwise_or(
445
- cv2.bitwise_and(walls, cv2.bitwise_not(collision)), safe_zone)
446
- dist = cv2.distanceTransform(
447
- cv2.bitwise_not(orig_walls), cv2.DIST_L2, 5)
448
- keep_mask = (dist <= (body_thickness / 2)).astype(np.uint8) * 255
449
- walls = cv2.bitwise_and(walls, keep_mask)
450
- walls = self._thin_line_filter(walls, body_thickness)
451
- n, labels, stats, _ = cv2.connectedComponentsWithStats(walls, connectivity=8)
452
- if n > 1:
 
 
 
 
 
453
  areas = stats[1:, cv2.CC_STAT_AREA]
454
  min_noise = max(20, int(np.median(areas) * 0.0001))
455
- lut = np.zeros(n, np.uint8)
456
- lut[1:] = (areas >= min_noise).astype(np.uint8)
457
- walls = (lut[labels] * 255).astype(np.uint8)
 
 
 
 
 
 
458
  return walls
459
 
460
- def _estimate_wall_thickness(self, binary: np.ndarray, fallback: int = 12) -> int:
461
- h, w = binary.shape
462
- n_cols = min(200, w)
463
- col_idx = np.linspace(0, w-1, n_cols, dtype=int)
464
- runs = []
465
- max_run = max(2, int(h * 0.05))
466
- for ci in col_idx:
467
- col = (binary[:, ci] > 0).astype(np.int8)
468
- pad = np.concatenate([[0], col, [0]])
469
- d = np.diff(pad.astype(np.int16))
470
- s = np.where(d == 1)[0]
471
- e = np.where(d == -1)[0]
472
- n_ = min(len(s), len(e))
473
- r = (e[:n_] - s[:n_]).astype(int)
474
- runs.extend(r[(r >= 2) & (r <= max_run)].tolist())
475
- if runs:
476
- return int(np.median(runs))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
477
  return fallback
478
 
479
- def _thin_line_filter(self, walls: np.ndarray, min_thickness: int) -> np.ndarray:
 
 
480
  dist = cv2.distanceTransform(walls, cv2.DIST_L2, 5)
481
  thick_mask = dist >= (min_thickness / 2)
482
- n, labels, _, _ = cv2.connectedComponentsWithStats(walls, connectivity=8)
483
- if n <= 1:
 
484
  return walls
 
485
  thick_labels = labels[thick_mask]
486
  if len(thick_labels) == 0:
487
  return np.zeros_like(walls)
488
- has_thick = np.zeros(n, dtype=bool)
 
489
  has_thick[thick_labels] = True
490
- lut = has_thick.astype(np.uint8) * 255
491
- lut[0] = 0
492
- return lut[labels]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
493
 
494
  # ══════════════════════════════════════════════════════════════════════════
495
  # Stage 5b β€” Remove fixture symbols (original)
 
397
  return result
398
 
399
  # ══════════════════════════════════════════════════════════════════════════
400
+ # Stage 4 β€” Extract walls (exact GeometryAgent.extract_walls_adaptive)
401
  # ══════════════════════════════════════════════════════════════════════════
402
  def _extract_walls(self, img: np.ndarray) -> np.ndarray:
403
+ """
404
+ Exact port of GeometryAgent.extract_walls_adaptive().
405
+ Uses analyze_image_characteristics() for the threshold, then:
406
+ H/V morph-open β†’ body dilate β†’ collision resolve β†’ distance gate
407
+ β†’ _remove_thin_lines β†’ small-CC noise filter β†’ _filter_double_lines_and_thick
408
+ """
409
  gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
410
  h, w = gray.shape
411
 
412
+ # ── adaptive threshold (identical to analyze_image_characteristics) ──
 
413
  brightness = float(np.mean(gray))
414
+ contrast = float(np.std(gray))
415
+ otsu_thr, _ = cv2.threshold(gray, 0, 255,
416
+ cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)
417
+ wall_pct = np.sum(_ > 0) / _.size * 100
418
  if brightness > 220:
419
+ wall_threshold = max(200, int(otsu_thr * 1.1))
420
  elif brightness < 180:
421
+ wall_threshold = max(150, int(otsu_thr * 0.9))
422
  else:
423
+ wall_threshold = int(otsu_thr)
424
 
425
+ _, binary = cv2.threshold(gray, wall_threshold, 255, cv2.THRESH_BINARY_INV)
426
 
427
+ min_line_len = max(8, int(0.012 * w))
428
+ body_thickness = self._estimate_wall_body_thickness(binary, fallback=12)
429
  body_thickness = int(np.clip(body_thickness, 9, 30))
 
430
 
431
+ print(f" min_line={min_line_len}px body={body_thickness}px (w={w}px)")
 
432
 
433
+ k_h = cv2.getStructuringElement(cv2.MORPH_RECT, (min_line_len, 1))
434
+ k_v = cv2.getStructuringElement(cv2.MORPH_RECT, (1, min_line_len))
435
+ long_h = cv2.morphologyEx(binary, cv2.MORPH_OPEN, k_h)
436
+ long_v = cv2.morphologyEx(binary, cv2.MORPH_OPEN, k_v)
 
 
 
 
 
437
 
438
  orig_walls = cv2.bitwise_or(long_h, long_v)
439
+
440
+ k_bh = cv2.getStructuringElement(cv2.MORPH_RECT, (1, body_thickness))
441
+ k_bv = cv2.getStructuringElement(cv2.MORPH_RECT, (body_thickness, 1))
442
+ dilated_h = cv2.dilate(long_h, k_bh)
443
+ dilated_v = cv2.dilate(long_v, k_bv)
444
+ walls = cv2.bitwise_or(dilated_h, dilated_v)
445
+
446
+ collision = cv2.bitwise_and(dilated_h, dilated_v)
447
+ safe_zone = cv2.bitwise_and(collision, orig_walls)
448
+ walls = cv2.bitwise_or(
449
+ cv2.bitwise_and(walls, cv2.bitwise_not(collision)),
450
+ safe_zone
451
+ )
452
+
453
+ dist = cv2.distanceTransform(cv2.bitwise_not(orig_walls), cv2.DIST_L2, 5)
454
+ keep_mask = (dist <= (body_thickness / 2)).astype(np.uint8) * 255
455
+ walls = cv2.bitwise_and(walls, keep_mask)
456
+ walls = self._remove_thin_lines(walls, min_thickness=body_thickness)
457
+
458
+ n_lbl, labels, stats, _ = cv2.connectedComponentsWithStats(walls, connectivity=8)
459
+ if n_lbl > 1:
460
  areas = stats[1:, cv2.CC_STAT_AREA]
461
  min_noise = max(20, int(np.median(areas) * 0.0001))
462
+ keep_lut = np.zeros(n_lbl, dtype=np.uint8)
463
+ keep_lut[1:] = (areas >= min_noise).astype(np.uint8)
464
+ walls = (keep_lut[labels] * 255).astype(np.uint8)
465
+
466
+ walls = self._filter_double_lines_and_thick(walls)
467
+
468
+ self._wall_thickness = body_thickness
469
+ print(f" Walls: {np.count_nonzero(walls)} px "
470
+ f"({100*np.count_nonzero(walls)/walls.size:.1f}%)")
471
  return walls
472
 
473
+ def _estimate_wall_body_thickness(self, binary: np.ndarray,
474
+ fallback: int = 12) -> int:
475
+ """Exact GeometryAgent._estimate_wall_body_thickness β€” vectorised column scan."""
476
+ try:
477
+ h, w = binary.shape
478
+ n_cols = min(200, w)
479
+ col_indices = np.linspace(0, w - 1, n_cols, dtype=int)
480
+
481
+ cols = (binary[:, col_indices] > 0).astype(np.int8)
482
+ padded = np.concatenate(
483
+ [np.zeros((1, n_cols), dtype=np.int8), cols,
484
+ np.zeros((1, n_cols), dtype=np.int8)], axis=0
485
+ )
486
+ diff = np.diff(padded.astype(np.int16), axis=0)
487
+
488
+ run_lengths = []
489
+ for ci in range(n_cols):
490
+ d = diff[:, ci]
491
+ starts = np.where(d == 1)[0]
492
+ ends = np.where(d == -1)[0]
493
+ if len(starts) == 0 or len(ends) == 0:
494
+ continue
495
+ runs = ends - starts
496
+ runs = runs[(runs >= 2) & (runs <= h * 0.15)]
497
+ if len(runs):
498
+ run_lengths.append(runs)
499
+
500
+ if run_lengths:
501
+ all_runs = np.concatenate(run_lengths)
502
+ thickness = int(np.median(all_runs))
503
+ print(f" [WallThickness] Estimated: {thickness} px")
504
+ return thickness
505
+ except Exception as exc:
506
+ print(f" [WallThickness] Estimation failed ({exc}), fallback={fallback}")
507
  return fallback
508
 
509
+ def _remove_thin_lines(self, walls: np.ndarray,
510
+ min_thickness: int) -> np.ndarray:
511
+ """Exact GeometryAgent._remove_thin_lines β€” distance transform CC gate."""
512
  dist = cv2.distanceTransform(walls, cv2.DIST_L2, 5)
513
  thick_mask = dist >= (min_thickness / 2)
514
+
515
+ n_lbl, labels, _, _ = cv2.connectedComponentsWithStats(walls, connectivity=8)
516
+ if n_lbl <= 1:
517
  return walls
518
+
519
  thick_labels = labels[thick_mask]
520
  if len(thick_labels) == 0:
521
  return np.zeros_like(walls)
522
+
523
+ has_thick = np.zeros(n_lbl, dtype=bool)
524
  has_thick[thick_labels] = True
525
+ keep_lut = has_thick.astype(np.uint8) * 255
526
+ keep_lut[0] = 0
527
+ return keep_lut[labels]
528
+
529
+ def _filter_double_lines_and_thick(
530
+ self,
531
+ walls: np.ndarray,
532
+ min_single_dim: int = 20,
533
+ double_line_gap: int = 60,
534
+ double_line_search_pct: int = 12,
535
+ ) -> np.ndarray:
536
+ """
537
+ Exact GeometryAgent._filter_double_lines_and_thick.
538
+ Keeps blobs that either:
539
+ (a) have min(bbox_w, bbox_h) >= min_single_dim (proper wall body), OR
540
+ (b) have a parallel partner blob within double_line_gap px
541
+ (double-line wall conventions used in CAD drawings).
542
+ """
543
+ n_lbl, labels, stats, _ = cv2.connectedComponentsWithStats(walls, connectivity=8)
544
+ if n_lbl <= 1:
545
+ return walls
546
+
547
+ # Try ximgproc thinning, fall back to morphological skeleton
548
+ try:
549
+ skel_full = cv2.ximgproc.thinning(
550
+ walls, thinningType=cv2.ximgproc.THINNING_ZHANGSUEN
551
+ )
552
+ except AttributeError:
553
+ skel_full = self._morphological_skeleton(walls)
554
+
555
+ skel_bin = (skel_full > 0)
556
+ keep_ids: set = set()
557
+
558
+ thin_candidates = []
559
+ for i in range(1, n_lbl):
560
+ bw = int(stats[i, cv2.CC_STAT_WIDTH])
561
+ bh = int(stats[i, cv2.CC_STAT_HEIGHT])
562
+ if min(bw, bh) >= min_single_dim:
563
+ keep_ids.add(i)
564
+ else:
565
+ thin_candidates.append(i)
566
+
567
+ if not thin_candidates:
568
+ filtered = np.zeros_like(walls)
569
+ for i in keep_ids:
570
+ filtered[labels == i] = 255
571
+ print(f" [DblLineFilter] Kept {len(keep_ids)}/{n_lbl-1} blobs "
572
+ "(all passed size test)")
573
+ return filtered
574
+
575
+ skel_labels = labels * skel_bin
576
+ img_h, img_w = labels.shape
577
+ probe_dists = np.arange(3, double_line_gap + 1, 3, dtype=np.float32)
578
+
579
+ for i in thin_candidates:
580
+ blob_skel_ys, blob_skel_xs = np.where(skel_labels == i)
581
+ if len(blob_skel_ys) < 4:
582
+ continue
583
+
584
+ step = max(1, len(blob_skel_ys) // 80)
585
+ sy = blob_skel_ys[::step].astype(np.float32)
586
+ sx = blob_skel_xs[::step].astype(np.float32)
587
+ n_s = len(sy)
588
+
589
+ sy_prev = np.roll(sy, 1); sy_prev[0] = sy[0]
590
+ sy_next = np.roll(sy, -1); sy_next[-1] = sy[-1]
591
+ sx_prev = np.roll(sx, 1); sx_prev[0] = sx[0]
592
+ sx_next = np.roll(sx, -1); sx_next[-1] = sx[-1]
593
+
594
+ dr = (sy_next - sy_prev).astype(np.float32)
595
+ dc = (sx_next - sx_prev).astype(np.float32)
596
+ dlen = np.maximum(1.0, np.hypot(dr, dc))
597
+
598
+ pr = (-dc / dlen)[:, np.newaxis]
599
+ pc = ( dr / dlen)[:, np.newaxis]
600
+
601
+ for sign in (1.0, -1.0):
602
+ rr = np.round(sy[:, np.newaxis] + sign * pr * probe_dists).astype(np.int32)
603
+ cc = np.round(sx[:, np.newaxis] + sign * pc * probe_dists).astype(np.int32)
604
+
605
+ valid = (rr >= 0) & (rr < img_h) & (cc >= 0) & (cc < img_w)
606
+ safe_rr = np.clip(rr, 0, img_h - 1)
607
+ safe_cc = np.clip(cc, 0, img_w - 1)
608
+ lbl_at = labels[safe_rr, safe_cc]
609
+
610
+ partner_mask = valid & (lbl_at > 0) & (lbl_at != i)
611
+ hit_any = partner_mask.any(axis=1)
612
+ hit_rows = np.where(hit_any)[0]
613
+ if len(hit_rows) == 0:
614
+ continue
615
+
616
+ first_hit_col = partner_mask[hit_rows].argmax(axis=1)
617
+ partner_ids = lbl_at[hit_rows, first_hit_col]
618
+ keep_ids.update(partner_ids.tolist())
619
+
620
+ if 100.0 * len(hit_rows) / n_s >= double_line_search_pct:
621
+ keep_ids.add(i)
622
+ break
623
+
624
+ if keep_ids:
625
+ keep_arr = np.array(sorted(keep_ids), dtype=np.int32)
626
+ keep_lut = np.zeros(n_lbl, dtype=np.uint8)
627
+ keep_lut[keep_arr] = 255
628
+ filtered = keep_lut[labels]
629
+ else:
630
+ filtered = np.zeros_like(walls)
631
+
632
+ print(f" [DblLineFilter] Kept {len(keep_ids)}/{n_lbl-1} blobs "
633
+ f"(min_dim>={min_single_dim}px OR double-line partner found)")
634
+ return filtered
635
 
636
  # ══════════════════════════════════════════════════════════════════════════
637
  # Stage 5b β€” Remove fixture symbols (original)