Krishna-123-priya commited on
Commit
c72f8e1
·
verified ·
1 Parent(s): dacc55f

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +5 -113
app.py CHANGED
@@ -706,9 +706,6 @@ def terminal_nets_via_kdtree(tree, labels, terminal_xy, max_dist):
706
  return set()
707
 
708
  def component_terminal_points(d):
709
- """Heuristic fallback terminal points (box-edge midpoints). Used only
710
- when no real skeleton-endpoint pin is found near the component (see
711
- find_pin_terminal_points / PIN-LEVEL CONNECTIVITY below)."""
712
  x1, y1 = d['x'], d['y']
713
  x2, y2 = x1 + d['w'], y1 + d['h']
714
  cx, cy = (x1 + x2) // 2, (y1 + y2) // 2
@@ -717,90 +714,6 @@ def component_terminal_points(d):
717
  return [(x1, cy), (x2, cy), (cx, y1), (cx, y2)]
718
  return [(cx, y1), (cx, y2), (x1, cy), (x2, cy)]
719
 
720
-
721
- # ── PIN-LEVEL CONNECTIVITY ──────────────────────────────────────────────────
722
- #
723
- # FIX (priority #1 -- pin-level connectivity instead of component-center
724
- # connectivity): component_terminal_points() above only ever guesses 2-4
725
- # fixed points on the bounding-box edge (left/right/top/bottom midpoints).
726
- # That's not where a real pin is -- it's just "somewhere on the box
727
- # perimeter" -- so every KDTree snap downstream inherits that imprecision,
728
- # regardless of how tight the search radius is tuned. A 2-terminal device
729
- # only ever has 1-2 real wire leads touching it; guessing 4 candidate
730
- # points per component (and firing a kdtree query at each) is part of why
731
- # GND/V1/V2/etc keep getting roped onto shared rails.
732
- #
733
- # Real fix: a wire lead is, by construction, a skeleton *endpoint* (a
734
- # skeleton pixel with exactly one 8-connected neighbor) sitting just
735
- # outside a component's bounding box, since break_skeleton_at_component_
736
- # boxes() cuts the skeleton at the box edge. So instead of guessing where
737
- # the pin is, find the actual skeleton endpoints near the box and use
738
- # those as the terminal points. This gives true pin-level precision
739
- # without needing new YOLO keypoint training.
740
-
741
- def find_skeleton_endpoints(skel):
742
- """Return (N,2) array of (x,y) skeleton pixels with exactly one
743
- 8-connected skeleton neighbor (= a wire lead tip / dangling end)."""
744
- if not np.any(skel):
745
- return np.empty((0, 2), dtype=int)
746
- sk = (skel > 0).astype(np.uint8)
747
- kernel = np.array([[1, 1, 1], [1, 0, 1], [1, 1, 1]], dtype=np.uint8)
748
- neighbor_count = cv2.filter2D(sk, -1, kernel, borderType=cv2.BORDER_CONSTANT)
749
- endpoint_mask = (sk == 1) & (neighbor_count == 1)
750
- ys, xs = np.where(endpoint_mask)
751
- return np.column_stack((xs, ys))
752
-
753
- def find_pin_terminal_points(d, skel_endpoints, search_margin):
754
- """For component d, find real skeleton-endpoint pins within
755
- `search_margin` px of its bounding box (the box was eroded out of the
756
- skeleton, so a genuine lead's endpoint sits just outside the edge).
757
- Returns up to 2 points (one per expected lead side for 2-terminal
758
- parts), picked as the nearest endpoint on each side of the box.
759
- Falls back to an empty list if nothing found -- caller should then
760
- use the box-edge heuristic instead."""
761
- if len(skel_endpoints) == 0:
762
- return []
763
- x1, y1 = d['x'] - search_margin, d['y'] - search_margin
764
- x2, y2 = d['x'] + d['w'] + search_margin, d['y'] + d['h'] + search_margin
765
- xs, ys = skel_endpoints[:, 0], skel_endpoints[:, 1]
766
- in_box = (xs >= x1) & (xs <= x2) & (ys >= y1) & (ys <= y2)
767
- cand = skel_endpoints[in_box]
768
- if len(cand) == 0:
769
- return []
770
-
771
- cx = d['x'] + d['w'] / 2.0
772
- cy = d['y'] + d['h'] / 2.0
773
- horizontal = d['w'] >= d['h']
774
-
775
- if horizontal:
776
- left = cand[cand[:, 0] <= cx]
777
- right = cand[cand[:, 0] > cx]
778
- sides = [left, right]
779
- else:
780
- top = cand[cand[:, 1] <= cy]
781
- bot = cand[cand[:, 1] > cy]
782
- sides = [top, bot]
783
-
784
- pts = []
785
- for side in sides:
786
- if len(side) == 0:
787
- continue
788
- # nearest-to-box-edge candidate on this side (closest to the
789
- # component center along the perpendicular axis -- i.e. the lead
790
- # tip nearest the body, not a far-away unrelated endpoint)
791
- d2 = (side[:, 0] - cx) ** 2 + (side[:, 1] - cy) ** 2
792
- best = side[np.argmin(d2)]
793
- pts.append((int(best[0]), int(best[1])))
794
-
795
- # if both sides collapsed to the same side (e.g. an L-shaped lead),
796
- # still return whatever we found rather than nothing
797
- if not pts:
798
- d2 = (cand[:, 0] - cx) ** 2 + (cand[:, 1] - cy) ** 2
799
- best = cand[np.argmin(d2)]
800
- pts.append((int(best[0]), int(best[1])))
801
- return pts
802
-
803
-
804
  def build_graph_wire_cc(details, img_np):
805
  """Full Stage 3 pipeline.
806
  FIX: merge gap and KDTree query radius are scale-relative and
@@ -808,19 +721,11 @@ def build_graph_wire_cc(details, img_np):
808
  receives net_pixels so it can cap merge span (see merge_split_nets
809
  docstring); GND kdtree radius tightened from 18*scale to 10*scale
810
  since the looser radius was eagerly grabbing onto unrelated rail
811
- segments, compounding the mega-net problem.
812
-
813
- FIX (pin-level connectivity): terminal points are now real skeleton
814
- endpoints found near each component's box (find_pin_terminal_points),
815
- not the old 4 guessed box-edge midpoints. The box-edge heuristic is
816
- kept ONLY as a per-component fallback for when no real endpoint is
817
- found nearby (e.g. component drawn flush against the wire with no
818
- visible stub), so unusual schematics don't go fully disconnected."""
819
  scale = _img_scale(img_np)
820
  merge_gap = max(3, int(4 * scale))
821
  kdtree_radius = max(4, int(5 * scale))
822
  gnd_kdtree_radius = max(8, int(10 * scale)) # was 18*scale -- too eager to grab onto unrelated rail segments
823
- pin_search_margin = max(3, int(6 * scale)) # how far outside the box to look for a real lead endpoint
824
 
825
  wire_layer = extract_wire_layer(img_np)
826
  wire_clean = erase_component_bodies(wire_layer, details)
@@ -828,9 +733,6 @@ def build_graph_wire_cc(details, img_np):
828
  skel = skeletonize_wire(wire_clean)
829
  skel = break_skeleton_at_component_boxes(skel, details)
830
 
831
- skel_endpoints = find_skeleton_endpoints(skel)
832
- debug_log("PINS", f"{len(skel_endpoints)} skeleton endpoint candidates (margin={pin_search_margin}px)")
833
-
834
  num_cc, cc_map = compute_nets(skel)
835
 
836
  # DEBUG: raw (pre-merge) net sizes/bboxes -- helps confirm whether a
@@ -860,25 +762,15 @@ def build_graph_wire_cc(details, img_np):
860
  ctype = d.get('component', '').lower()
861
  is_gnd = 'ground' in ctype or 'gnd' in ctype
862
  radius = gnd_kdtree_radius if is_gnd else kdtree_radius
863
-
864
- pin_pts = find_pin_terminal_points(d, skel_endpoints, pin_search_margin)
865
- used_fallback = False
866
- if not pin_pts:
867
- pin_pts = component_terminal_points(d)
868
- used_fallback = True
869
-
870
  nets_found = set()
871
- for tx, ty in pin_pts:
872
  nets_found |= terminal_nets_via_kdtree(tree, labels, (tx, ty), max_dist=radius)
873
-
874
  # GND: also try its own centroid in case the lead is short/centered
875
  if is_gnd and not nets_found:
876
  cx, cy = d['x'] + d['w'] // 2, d['y'] + d['h'] // 2
877
  nets_found |= terminal_nets_via_kdtree(tree, labels, (cx, cy), max_dist=gnd_kdtree_radius)
878
-
879
  comp_nets[d['label']] = nets_found
880
- debug_log("TERMINAL", f"{d['label']}({d['component']}) -> nets={sorted(nets_found)} "
881
- f"r={radius} pins={pin_pts}{' [fallback-bbox]' if used_fallback else ' [real-pin]'}")
882
 
883
  G = nx.Graph()
884
  for d in details:
@@ -2046,7 +1938,7 @@ with gr.Blocks(
2046
  theme=gr.themes.Base(primary_hue="blue", neutral_hue="slate",
2047
  font=[gr.themes.GoogleFont("DM Sans"), "sans-serif"],
2048
  font_mono=[gr.themes.GoogleFont("JetBrains Mono"), "monospace"]),
2049
- css=css, title="Circuit Detector v25"
2050
  ) as app:
2051
  base_state = gr.State([])
2052
  gr.HTML("""
@@ -2059,7 +1951,7 @@ with gr.Blocks(
2059
  <span class="badge b1">YOLOv8 3-pass</span>
2060
  <span class="badge b2">OCR + LLM</span>
2061
  <span class="badge b3">Skeleton + Union-Find + KDTree</span>
2062
- <span class="badge b4">v25 - pin-level connectivity: real skeleton-endpoint leads, box-edge guess as fallback only</span>
2063
  </div>""")
2064
 
2065
  with gr.Row():
 
706
  return set()
707
 
708
  def component_terminal_points(d):
 
 
 
709
  x1, y1 = d['x'], d['y']
710
  x2, y2 = x1 + d['w'], y1 + d['h']
711
  cx, cy = (x1 + x2) // 2, (y1 + y2) // 2
 
714
  return [(x1, cy), (x2, cy), (cx, y1), (cx, y2)]
715
  return [(cx, y1), (cx, y2), (x1, cy), (x2, cy)]
716
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
717
  def build_graph_wire_cc(details, img_np):
718
  """Full Stage 3 pipeline.
719
  FIX: merge gap and KDTree query radius are scale-relative and
 
721
  receives net_pixels so it can cap merge span (see merge_split_nets
722
  docstring); GND kdtree radius tightened from 18*scale to 10*scale
723
  since the looser radius was eagerly grabbing onto unrelated rail
724
+ segments, compounding the mega-net problem."""
 
 
 
 
 
 
 
725
  scale = _img_scale(img_np)
726
  merge_gap = max(3, int(4 * scale))
727
  kdtree_radius = max(4, int(5 * scale))
728
  gnd_kdtree_radius = max(8, int(10 * scale)) # was 18*scale -- too eager to grab onto unrelated rail segments
 
729
 
730
  wire_layer = extract_wire_layer(img_np)
731
  wire_clean = erase_component_bodies(wire_layer, details)
 
733
  skel = skeletonize_wire(wire_clean)
734
  skel = break_skeleton_at_component_boxes(skel, details)
735
 
 
 
 
736
  num_cc, cc_map = compute_nets(skel)
737
 
738
  # DEBUG: raw (pre-merge) net sizes/bboxes -- helps confirm whether a
 
762
  ctype = d.get('component', '').lower()
763
  is_gnd = 'ground' in ctype or 'gnd' in ctype
764
  radius = gnd_kdtree_radius if is_gnd else kdtree_radius
 
 
 
 
 
 
 
765
  nets_found = set()
766
+ for tx, ty in component_terminal_points(d):
767
  nets_found |= terminal_nets_via_kdtree(tree, labels, (tx, ty), max_dist=radius)
 
768
  # GND: also try its own centroid in case the lead is short/centered
769
  if is_gnd and not nets_found:
770
  cx, cy = d['x'] + d['w'] // 2, d['y'] + d['h'] // 2
771
  nets_found |= terminal_nets_via_kdtree(tree, labels, (cx, cy), max_dist=gnd_kdtree_radius)
 
772
  comp_nets[d['label']] = nets_found
773
+ debug_log("TERMINAL", f"{d['label']}({d['component']}) -> nets={sorted(nets_found)} r={radius}")
 
774
 
775
  G = nx.Graph()
776
  for d in details:
 
1938
  theme=gr.themes.Base(primary_hue="blue", neutral_hue="slate",
1939
  font=[gr.themes.GoogleFont("DM Sans"), "sans-serif"],
1940
  font_mono=[gr.themes.GoogleFont("JetBrains Mono"), "monospace"]),
1941
+ css=css, title="Circuit Detector v24"
1942
  ) as app:
1943
  base_state = gr.State([])
1944
  gr.HTML("""
 
1951
  <span class="badge b1">YOLOv8 3-pass</span>
1952
  <span class="badge b2">OCR + LLM</span>
1953
  <span class="badge b3">Skeleton + Union-Find + KDTree</span>
1954
+ <span class="badge b4">v24 - terminal nets now nearest-only (kdtree query, not ball_point) -- fixes spurious multi-net terminals</span>
1955
  </div>""")
1956
 
1957
  with gr.Row():