Spaces:
Runtime error
Runtime error
Update app.py
Browse files
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
|
| 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
|
| 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">
|
| 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():
|