Spaces:
Running
Running
| import gradio as gr | |
| import cv2 | |
| import numpy as np | |
| import json | |
| import os | |
| import tempfile | |
| import shutil | |
| from PIL import Image | |
| from collections import Counter | |
| from inference_sdk import InferenceHTTPClient | |
| import easyocr | |
| # βββββββββββββββββββββββββββββββββββββββββββββ | |
| # IMPORTS FROM OUR PIPELINE | |
| # βββββββββββββββββββββββββββββββββββββββββββββ | |
| from detector import run_detection, detect_traces, draw_detections | |
| from ocr import run_ocr_on_detections, print_ocr_summary | |
| from netlist import (assign_reference_designators, | |
| extract_trace_mask, | |
| find_trace_connections, | |
| find_proximity_connections, | |
| build_nets) | |
| from kicad_writer import generate_kicad_schematic | |
| # βββββββββββββββββββββββββββββββββββββββββββββ | |
| # GLOBAL OCR READER (load once) | |
| # βββββββββββββββββββββββββββββββββββββββββββββ | |
| print("[->] Loading EasyOCR...") | |
| ocr_reader = easyocr.Reader(['en'], gpu=False) # CPU for HuggingFace | |
| print("[OK] EasyOCR ready") | |
| # βββββββββββββββββββββββββββββββββββββββββββββ | |
| # HELPER β numpy image to PIL | |
| # βββββββββββββββββββββββββββββββββββββββββββββ | |
| def to_pil(img_bgr: np.ndarray) -> Image.Image: | |
| return Image.fromarray(cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB)) | |
| # βββββββββββββββββββββββββββββββββββββββββββββ | |
| # TAB 1 β COMPONENT DETECTION | |
| # βββββββββββββββββββββββββββββββββββββββββββββ | |
| def run_detection_tab(image: Image.Image): | |
| if image is None: | |
| return None, "β Please upload a PCB image first.", "{}" | |
| # Save uploaded image to temp file | |
| tmp_dir = tempfile.mkdtemp() | |
| img_path = os.path.join(tmp_dir, "input.jpg") | |
| image.save(img_path) | |
| try: | |
| # Run Roboflow detection | |
| detections = run_detection(img_path) | |
| if not detections: | |
| return image, "β οΈ No components detected. Try a clearer PCB image.", "{}" | |
| # Draw detections | |
| img_bgr = cv2.imread(img_path) | |
| annotated = draw_detections(img_bgr, detections) | |
| result_pil= to_pil(annotated) | |
| # Build summary text | |
| counts = Counter(d['label'] for d in detections) | |
| summary = f"β **{len(detections)} components detected**\n\n" | |
| summary += "| Component | Count |\n|-----------|-------|\n" | |
| for label, count in sorted(counts.items(), key=lambda x: -x[1]): | |
| summary += f"| {label} | {count} |\n" | |
| # Save detections to JSON for next tabs | |
| det_json = json.dumps({ | |
| "image_path": img_path, | |
| "components": [ | |
| {**d, "bbox": list(d["bbox"])} | |
| for d in detections | |
| ] | |
| }, indent=2) | |
| return result_pil, summary, det_json | |
| except Exception as e: | |
| return image, f"β Error: {str(e)}", "{}" | |
| # βββββββββββββββββββββββββββββββββββββββββββββ | |
| # TAB 2 β OCR / PART NUMBER READING | |
| # βββββββββββββββββββββββββββββββββββββββββββββ | |
| def run_ocr_tab(det_json: str): | |
| if not det_json or det_json == "{}": | |
| return "β οΈ Run Detection first!", "{}" | |
| try: | |
| data = json.loads(det_json) | |
| img_path = data.get("image_path") | |
| detections = data.get("components", []) | |
| for d in detections: | |
| d['bbox'] = tuple(d['bbox']) | |
| IC_LABELS = ['ic', 'transistor', 'clock', 'display'] | |
| # Run OCR using global reader | |
| img = cv2.imread(img_path) | |
| ih, iw = img.shape[:2] | |
| PADDING = 10 | |
| updated = [] | |
| for det in detections: | |
| label = det['label'] | |
| if label not in IC_LABELS: | |
| det['ocr_text'] = [] | |
| det['part_number'] = "N/A" | |
| updated.append(det) | |
| continue | |
| x1, y1, x2, y2 = det['bbox'] | |
| x1p = max(0, x1 - PADDING) | |
| y1p = max(0, y1 - PADDING) | |
| x2p = min(iw, x2 + PADDING) | |
| y2p = min(ih, y2 + PADDING) | |
| patch = img[y1p:y2p, x1p:x2p] | |
| if patch.size == 0: | |
| det['ocr_text'] = [] | |
| det['part_number'] = "unknown" | |
| updated.append(det) | |
| continue | |
| # Upscale for better OCR | |
| h, w = patch.shape[:2] | |
| scale = 3 if max(h, w) < 100 else 2 | |
| patch = cv2.resize(patch, (w*scale, h*scale), | |
| interpolation=cv2.INTER_CUBIC) | |
| results = ocr_reader.readtext(patch) | |
| texts = [(t.strip(), round(c, 3)) | |
| for _, t, c in results | |
| if c >= 0.4 and len(t.strip()) >= 2] | |
| combined = " ".join(t for t, c in texts).strip() | |
| det['ocr_text'] = texts | |
| det['part_number'] = combined if combined else "unknown" | |
| updated.append(det) | |
| # Build output table | |
| ic_dets = [d for d in updated if d['label'] in IC_LABELS] | |
| identified = [d for d in ic_dets | |
| if d.get('part_number', 'unknown') not in | |
| ('unknown', 'N/A', '')] | |
| summary = f"β **OCR complete β {len(identified)}/{len(ic_dets)} ICs identified**\n\n" | |
| summary += "| RefDes | Label | Part Number | Confidence |\n" | |
| summary += "|--------|-------|-------------|------------|\n" | |
| for i, det in enumerate(ic_dets): | |
| ref = f"U{i+1}" | |
| part = det.get('part_number', 'unknown') | |
| conf = det['confidence'] | |
| summary += f"| {ref} | {det['label']} | {part} | {conf:.0%} |\n" | |
| # Pass updated detections forward | |
| out_json = json.dumps({ | |
| "image_path": img_path, | |
| "components": [ | |
| {**d, | |
| "bbox": list(d["bbox"]), | |
| "ocr_text": [[t, c] for t, c in d.get("ocr_text", [])]} | |
| for d in updated | |
| ] | |
| }, indent=2) | |
| return summary, out_json | |
| except Exception as e: | |
| return f"β Error: {str(e)}", "{}" | |
| # βββββββββββββββββββββββββββββββββββββββββββββ | |
| # TAB 3 β NETLIST GENERATION | |
| # βββββββββββββββββββββββββββββββββββββββββββββ | |
| def run_netlist_tab(ocr_json: str): | |
| if not ocr_json or ocr_json == "{}": | |
| return "β οΈ Run OCR first!", "{}", None | |
| try: | |
| data = json.loads(ocr_json) | |
| img_path = data.get("image_path") | |
| detections = data.get("components", []) | |
| for d in detections: | |
| d['bbox'] = tuple(d['bbox']) | |
| img = cv2.imread(img_path) | |
| components = assign_reference_designators(detections) | |
| trace_mask = extract_trace_mask(img) | |
| trace_conn = find_trace_connections(components, trace_mask, img.shape) | |
| prox_conn = find_proximity_connections(components, trace_conn) | |
| all_conn = trace_conn + prox_conn | |
| nets = build_nets(all_conn) | |
| # Save netlist JSON to temp file | |
| tmp_dir = os.path.dirname(img_path) | |
| netlist_path = os.path.join(tmp_dir, "netlist.json") | |
| out_data = { | |
| "total_components": len(components), | |
| "total_connections": len(all_conn), | |
| "total_nets": len(nets), | |
| "components": [ | |
| {**c, | |
| "bbox": list(c["bbox"]), | |
| "ocr_text": [[t, conf] for t, conf | |
| in c.get("ocr_text", [])]} | |
| for c in components | |
| ], | |
| "connections": [ | |
| {"from": a, "to": b, "method": m} | |
| for a, b, m in all_conn | |
| ], | |
| "nets": nets | |
| } | |
| with open(netlist_path, "w") as f: | |
| json.dump(out_data, f, indent=2) | |
| # Build summary | |
| trace_c = len([c for c in all_conn if c[2] == "trace"]) | |
| prox_c = len([c for c in all_conn if c[2] == "proximity"]) | |
| summary = f"β **Netlist generated successfully**\n\n" | |
| summary += f"- **Components:** {len(components)}\n" | |
| summary += f"- **Connections:** {len(all_conn)} " | |
| summary += f"({trace_c} via traces, {prox_c} via proximity)\n" | |
| summary += f"- **Nets:** {len(nets)}\n\n" | |
| summary += "| Net | Members |\n|-----|--------|\n" | |
| for net_name, members in nets.items(): | |
| summary += f"| {net_name} | {', '.join(members[:5])}" | |
| if len(members) > 5: | |
| summary += f" ... +{len(members)-5} more" | |
| summary += " |\n" | |
| return summary, json.dumps({"netlist_path": netlist_path}), netlist_path | |
| except Exception as e: | |
| return f"β Error: {str(e)}", "{}", None | |
| # βββββββββββββββββββββββββββββββββββββββββββββ | |
| # TAB 4 β KICAD SCHEMATIC OUTPUT | |
| # βββββββββββββββββββββββββββββββββββββββββββββ | |
| def run_kicad_tab(netlist_ref: str): | |
| if not netlist_ref or netlist_ref == "{}": | |
| return "β οΈ Run Netlist generation first!", None | |
| try: | |
| data = json.loads(netlist_ref) | |
| netlist_path = data.get("netlist_path") | |
| if not netlist_path or not os.path.exists(netlist_path): | |
| return "β Netlist file not found. Re-run previous steps.", None | |
| # Generate KiCAD schematic | |
| tmp_dir = os.path.dirname(netlist_path) | |
| sch_path = os.path.join(tmp_dir, "schematic.kicad_sch") | |
| generate_kicad_schematic(netlist_path, sch_path) | |
| summary = f"β **KiCAD schematic generated!**\n\n" | |
| summary += f"- File: `schematic.kicad_sch`\n" | |
| summary += f"- Format: KiCAD 6/7 compatible\n\n" | |
| summary += "**How to open:**\n" | |
| summary += "1. Download the file below\n" | |
| summary += "2. Open KiCAD β File β Open Schematic\n" | |
| summary += " OR drag into [kicanvas.org](https://kicanvas.org) for instant preview\n" | |
| return summary, sch_path | |
| except Exception as e: | |
| return f"β Error: {str(e)}", None | |
| # βββββββββββββββββββββββββββββββββββββββββββββ | |
| # BUILD GRADIO UI | |
| # βββββββββββββββββββββββββββββββββββββββββββββ | |
| def build_ui(): | |
| with gr.Blocks( | |
| title="PCB Image β Schematic", | |
| theme=gr.themes.Soft(), | |
| css=""" | |
| .tab-header { font-size: 1.1em; font-weight: bold; } | |
| .output-panel { background: #1a1a2e; border-radius: 8px; } | |
| """ | |
| ) as demo: | |
| # ββ Header ββ | |
| gr.Markdown(""" | |
| # π PCB Image β Schematic | |
| ### Convert a PCB photo into a KiCAD schematic automatically | |
| Upload a PCB image and step through each stage of the pipeline. | |
| """) | |
| # ββ Shared state between tabs ββ | |
| detection_state = gr.State("{}") | |
| ocr_state = gr.State("{}") | |
| netlist_state = gr.State("{}") | |
| # ββ Tab 1: Detection ββ | |
| with gr.Tab("π· 1 β Component Detection"): | |
| gr.Markdown("Upload a PCB image or click one of the example images below.") | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| img_input = gr.Image(type="pil", label="PCB Image") | |
| detect_btn = gr.Button("π Detect Components", variant="primary") | |
| gr.Examples( | |
| examples=[ | |
| ["sample 1.jpg"], | |
| ["sample 2.jpg"], | |
| ["sample 3.jpg"], | |
| ["sample 4.jpg"], | |
| ["sample 5.jpg"], | |
| ], | |
| inputs=img_input, | |
| label="π Example PCB Images β click to load", | |
| examples_per_page=5, | |
| ) | |
| with gr.Column(scale=1): | |
| detect_out = gr.Image(label="Detected Components") | |
| detect_text = gr.Markdown() | |
| detect_btn.click( | |
| fn=run_detection_tab, | |
| inputs=[img_input], | |
| outputs=[detect_out, detect_text, detection_state] | |
| ) | |
| # ββ Tab 2: OCR ββ | |
| with gr.Tab("π€ 2 β Read IC Text"): | |
| gr.Markdown("Reads part numbers from IC chips using OCR.") | |
| ocr_btn = gr.Button("π Run OCR on ICs", variant="primary") | |
| ocr_text = gr.Markdown() | |
| ocr_btn.click( | |
| fn=run_ocr_tab, | |
| inputs=[detection_state], | |
| outputs=[ocr_text, ocr_state] | |
| ) | |
| # ββ Tab 3: Netlist ββ | |
| with gr.Tab("π 3 β Generate Netlist"): | |
| gr.Markdown("Finds connections between components using trace detection + proximity.") | |
| netlist_btn = gr.Button("β‘ Generate Netlist", variant="primary") | |
| netlist_text = gr.Markdown() | |
| netlist_file = gr.File(label="Download Netlist JSON", visible=False) | |
| netlist_btn.click( | |
| fn=run_netlist_tab, | |
| inputs=[ocr_state], | |
| outputs=[netlist_text, netlist_state, netlist_file] | |
| ) | |
| # ββ Tab 4: KiCAD ββ | |
| with gr.Tab("π 4 β KiCAD Schematic"): | |
| gr.Markdown("Generates a KiCAD `.kicad_sch` file you can open in KiCAD or kicanvas.org") | |
| kicad_btn = gr.Button("πΎ Generate KiCAD File", variant="primary") | |
| kicad_text = gr.Markdown() | |
| kicad_file = gr.File(label="Download .kicad_sch") | |
| kicad_btn.click( | |
| fn=run_kicad_tab, | |
| inputs=[netlist_state], | |
| outputs=[kicad_text, kicad_file] | |
| ) | |
| # ββ Footer ββ | |
| gr.Markdown(""" | |
| --- | |
| Built with Roboflow YOLOv8 Β· EasyOCR Β· OpenCV Β· KiCAD | |
| """) | |
| return demo | |
| # βββββββββββββββββββββββββββββββββββββββββββββ | |
| # ENTRY POINT | |
| # βββββββββββββββββββββββββββββββββββββββββββββ | |
| if __name__ == "__main__": | |
| demo = build_ui() | |
| demo.launch( | |
| server_name="0.0.0.0", | |
| server_port=7860, | |
| share=False | |
| ) |