Janani-V's picture
Upload app.py with huggingface_hub
912898d verified
Raw
History Blame Contribute Delete
17 kB
import gradio as gr
from huggingface_hub import snapshot_download
import sys
# =========================
# Load model + inspector
# =========================
MODEL_REPO = "Janani-V/pcb-defect-yolov8s-deeppcb"
local_dir = snapshot_download(repo_id=MODEL_REPO)
sys.path.append(local_dir)
from inspector import PCBDefectInspector
inspector = PCBDefectInspector(weights_path=f"{local_dir}/best.pt")
# =========================
# Severity config
# =========================
SEVERITY_COLORS = {
"Critical": "#ef4444",
"High": "#f97316",
"Medium-High": "#f59e0b",
"Medium": "#eab308",
"Low-Medium": "#84cc16",
"Low": "#22c55e",
"Unknown": "#64748b",
}
SEVERITY_RANK = {
"Critical": 6,
"High": 5,
"Medium-High": 4,
"Medium": 3,
"Low-Medium": 2,
"Low": 1,
"Unknown": 0,
}
# =========================
# Example images
# Make sure these exist in your Space repo
# =========================
EXAMPLES = [
"sample1_input.jpg",
"sample2_input.jpg",
"sample3_input.jpg",
"sample4_input.jpg",
"sample5_input.jpg",
"sample6_input.jpg",
"sample7_input.jpg",
"sample8_input.jpg",
]
# =========================
# Helper functions
# =========================
def empty_summary_cards():
return """
<div class="summary-grid">
<div class="summary-card">
<div class="summary-title">Defects Found</div>
<div class="summary-value">-</div>
</div>
<div class="summary-card">
<div class="summary-title">Highest Severity</div>
<div class="summary-value">-</div>
</div>
<div class="summary-card">
<div class="summary-title">Unique Defect Types</div>
<div class="summary-value">-</div>
</div>
<div class="summary-card">
<div class="summary-title">Avg Confidence</div>
<div class="summary-value">-</div>
</div>
</div>
"""
def empty_report():
return """
<div class="empty-report">
<h3>Inspection report will appear here</h3>
<p>Upload a PCB image and run inspection to view defect analysis, severity, likely root cause, impact, and recommended action.</p>
</div>
"""
def make_summary_cards(findings):
if not findings:
return """
<div class="summary-grid">
<div class="summary-card">
<div class="summary-title">Defects Found</div>
<div class="summary-value">0</div>
</div>
<div class="summary-card">
<div class="summary-title">Highest Severity</div>
<div class="summary-value">None</div>
</div>
<div class="summary-card">
<div class="summary-title">Unique Defect Types</div>
<div class="summary-value">0</div>
</div>
<div class="summary-card">
<div class="summary-title">Avg Confidence</div>
<div class="summary-value">0.00</div>
</div>
</div>
"""
total_defects = len(findings)
unique_classes = len(set(f["class"] for f in findings))
avg_conf = sum(float(f["confidence"]) for f in findings) / max(total_defects, 1)
highest_severity = max(
(f.get("severity", "Unknown") for f in findings),
key=lambda s: SEVERITY_RANK.get(s, 0),
default="Unknown"
)
sev_color = SEVERITY_COLORS.get(highest_severity, "#64748b")
return f"""
<div class="summary-grid">
<div class="summary-card">
<div class="summary-title">Defects Found</div>
<div class="summary-value">{total_defects}</div>
</div>
<div class="summary-card">
<div class="summary-title">Highest Severity</div>
<div class="summary-value" style="color:{sev_color};">{highest_severity}</div>
</div>
<div class="summary-card">
<div class="summary-title">Unique Defect Types</div>
<div class="summary-value">{unique_classes}</div>
</div>
<div class="summary-card">
<div class="summary-title">Avg Confidence</div>
<div class="summary-value">{avg_conf:.2f}</div>
</div>
</div>
"""
def make_report_html(result):
findings = result.get("findings", [])
summary = result.get("summary", "Inspection completed.")
if not findings:
return f"""
<div class="report-wrapper">
<div class="report-banner neutral">
<div class="report-banner-title">Inspection Summary</div>
<div class="report-banner-text">{summary}</div>
</div>
<div class="empty-report">
<h3>No defects detected above the selected confidence threshold.</h3>
<p>The model did not identify any PCB manufacturing defect in the uploaded image.</p>
</div>
</div>
"""
# Group by class so repeated instances are summarized together
grouped = {}
for f in findings:
cls = f["class"]
if cls not in grouped:
grouped[cls] = {
"count": 0,
"confidences": [],
"info": f
}
grouped[cls]["count"] += 1
grouped[cls]["confidences"].append(f["confidence"])
sorted_items = sorted(
grouped.items(),
key=lambda x: SEVERITY_RANK.get(x[1]["info"].get("severity", "Unknown"), 0),
reverse=True
)
highest_severity = sorted_items[0][1]["info"].get("severity", "Unknown")
banner_color = SEVERITY_COLORS.get(highest_severity, "#64748b")
html = f"""
<div class="report-wrapper">
<div class="report-banner" style="border-left: 6px solid {banner_color};">
<div class="report-banner-title">Inspection Summary</div>
<div class="report-banner-text">{summary}</div>
<div class="report-banner-sub">Highest severity detected: <b>{highest_severity}</b></div>
</div>
"""
for cls, data in sorted_items:
info = data["info"]
severity = info.get("severity", "Unknown")
sev_color = SEVERITY_COLORS.get(severity, "#64748b")
conf_list = ", ".join(f"{float(c):.2f}" for c in data["confidences"])
explanation = info.get("explanation", "No explanation available.")
root_cause = info.get("root_cause", "Not available.")
impact = info.get("impact", "Not available.")
action = info.get("action", "Not available.")
html += f"""
<div class="defect-card">
<div class="defect-header">
<div>
<div class="defect-name">{cls.upper()}</div>
<div class="defect-count">Detected instances: {data['count']}</div>
</div>
<div class="severity-pill" style="background:{sev_color}; color:#111827;">
{severity}
</div>
</div>
<div class="defect-meta">
<span><b>Confidence:</b> {conf_list}</span>
</div>
<div class="defect-section">
<div class="defect-section-title">Explanation</div>
<div class="defect-section-text">{explanation}</div>
</div>
<div class="defect-section">
<div class="defect-section-title">Likely Root Cause</div>
<div class="defect-section-text">{root_cause}</div>
</div>
<div class="defect-section">
<div class="defect-section-title">Potential Impact</div>
<div class="defect-section-text">{impact}</div>
</div>
<div class="defect-section">
<div class="defect-section-title">Recommended Action</div>
<div class="defect-section-text">{action}</div>
</div>
</div>
"""
html += "</div>"
return html
# =========================
# Main functions
# =========================
def detect(image, conf):
if image is None:
return None, empty_summary_cards(), empty_report()
inspector.conf = conf
result = inspector.inspect(image)
annotated = result.get("annotated_image", None)
findings = result.get("findings", [])
summary_cards = make_summary_cards(findings)
report_html = make_report_html(result)
return annotated, summary_cards, report_html
def clear_all():
return None, 0.25, None, empty_summary_cards(), empty_report()
# =========================
# CSS
# =========================
CUSTOM_CSS = """
body, .gradio-container {
background: #0b1220 !important;
color: #f3f4f6 !important;
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
.gradio-container {
max-width: 1400px !important;
margin: auto !important;
padding-top: 18px !important;
padding-bottom: 24px !important;
}
.main-title {
font-size: 34px;
font-weight: 800;
margin-bottom: 6px;
color: #f8fafc;
}
.sub-title {
font-size: 16px;
line-height: 1.8;
color: #cbd5e1;
margin-bottom: 24px;
}
.section-title {
font-size: 22px;
font-weight: 700;
color: #f8fafc;
margin: 8px 0 14px 0;
}
.card-subtext {
color: #94a3b8;
font-size: 14px;
margin-top: 8px;
line-height: 1.7;
}
.summary-grid {
display: grid;
grid-template-columns: repeat(4, minmax(120px, 1fr));
gap: 12px;
margin-top: 12px;
margin-bottom: 6px;
}
.summary-card {
background: linear-gradient(180deg, #141d2d 0%, #0f172a 100%);
border: 1px solid #243042;
border-radius: 16px;
padding: 16px;
min-height: 92px;
display: flex;
flex-direction: column;
justify-content: center;
}
.summary-title {
color: #94a3b8;
font-size: 13px;
margin-bottom: 8px;
}
.summary-value {
font-size: 24px;
font-weight: 800;
color: #f8fafc;
}
.report-wrapper {
margin-top: 6px;
}
.report-banner {
background: #111827;
border: 1px solid #243042;
border-radius: 16px;
padding: 18px 18px 16px 18px;
margin-bottom: 16px;
}
.report-banner-title {
font-size: 18px;
font-weight: 800;
color: #f8fafc;
margin-bottom: 6px;
}
.report-banner-text {
color: #dbeafe;
font-size: 15px;
line-height: 1.7;
margin-bottom: 6px;
}
.report-banner-sub {
color: #cbd5e1;
font-size: 14px;
}
.defect-card {
background: #111827;
border: 1px solid #243042;
border-radius: 16px;
padding: 18px;
margin-bottom: 14px;
}
.defect-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 12px;
margin-bottom: 12px;
}
.defect-name {
font-size: 20px;
font-weight: 800;
color: #f8fafc;
}
.defect-count {
color: #94a3b8;
font-size: 14px;
margin-top: 4px;
}
.severity-pill {
padding: 8px 14px;
border-radius: 999px;
font-weight: 800;
font-size: 13px;
white-space: nowrap;
}
.defect-meta {
font-size: 14px;
color: #cbd5e1;
margin-bottom: 14px;
}
.defect-section {
margin-top: 12px;
padding-top: 10px;
border-top: 1px solid #223045;
}
.defect-section-title {
font-size: 14px;
font-weight: 800;
color: #93c5fd;
margin-bottom: 6px;
letter-spacing: 0.2px;
}
.defect-section-text {
color: #e5e7eb;
line-height: 1.7;
font-size: 14px;
}
.empty-report {
background: #111827;
border: 1px dashed #334155;
border-radius: 16px;
padding: 28px;
text-align: center;
color: #cbd5e1;
margin-top: 8px;
}
.empty-report h3 {
margin: 0 0 10px 0;
color: #f8fafc;
font-size: 20px;
}
.note-box {
background: #0f172a;
border: 1px solid #223045;
border-radius: 16px;
padding: 16px;
color: #cbd5e1;
font-size: 14px;
line-height: 1.7;
margin-top: 18px;
}
.note-box b {
color: #f8fafc;
}
.footer-link {
color: #8b5cf6 !important;
text-decoration: none !important;
font-weight: 700;
}
.footer-link:hover {
text-decoration: underline !important;
}
/* Make buttons cleaner */
button.primary {
background: linear-gradient(90deg, #6D5DF6, #7C3AED) !important;
border: none !important;
color: white !important;
}
button.secondary {
background: #1f2937 !important;
border: 1px solid #374151 !important;
color: #f8fafc !important;
}
@media (max-width: 900px) {
.summary-grid {
grid-template-columns: repeat(2, minmax(120px, 1fr));
}
}
@media (max-width: 560px) {
.summary-grid {
grid-template-columns: 1fr;
}
}
/* Even, uniform example thumbnails (Gradio 5 renders Examples as a
[data-testid="dataset"] grid, not a <table>) */
.gradio-container [data-testid="dataset"] {
display: grid !important;
grid-template-columns: repeat(4, minmax(90px, 1fr)) !important;
gap: 14px !important;
}
.gradio-container [data-testid="dataset"] > div,
.gradio-container [data-testid="dataset"] button {
width: 100% !important;
aspect-ratio: 1 / 1 !important;
padding: 0 !important;
margin: 0 !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
overflow: hidden !important;
border-radius: 10px !important;
}
.gradio-container [data-testid="dataset"] img {
width: 100% !important;
height: 100% !important;
object-fit: cover !important;
border-radius: 10px !important;
}
@media (max-width: 900px) {
.gradio-container [data-testid="dataset"] {
grid-template-columns: repeat(2, minmax(90px, 1fr)) !important;
}
}
"""
# =========================
# Build UI
# =========================
with gr.Blocks(css=CUSTOM_CSS) as demo:
gr.HTML("""
<div class="main-title">PCB Defect Detection Dashboard</div>
<div class="sub-title">
Upload a PCB image to detect manufacturing defects and receive an inspection report with
<b>severity assessment</b>, <b>likely root cause</b>, <b>impact</b>, and
<b>recommended action</b> for each detected defect type.
This interface combines a fine-tuned <b>YOLOv8s defect detector</b> with a structured PCB defect interpretation layer.
</div>
""")
with gr.Row(equal_height=False):
with gr.Column(scale=4):
gr.HTML('<div class="section-title">📤 Input Panel</div>')
input_image = gr.Image(
type="numpy",
label="Upload PCB Image",
height=360
)
conf_slider = gr.Slider(
minimum=0.10,
maximum=0.90,
value=0.25,
step=0.05,
label="Confidence Threshold"
)
with gr.Row():
detect_btn = gr.Button("Run Inspection", variant="primary")
clear_btn = gr.Button("Clear", variant="secondary")
gr.HTML("""
<div class="card-subtext">
Supported formats: <b>JPG, JPEG, PNG</b><br>
Recommended: clear grayscale PCB images similar to DeepPCB-style inspection images.
</div>
""")
# RIGHT PANEL
with gr.Column(scale=6):
gr.HTML('<div class="section-title">🔍 Detection Output</div>')
output_image = gr.Image(
type="numpy",
label="Annotated PCB Output",
height=360
)
summary_html = gr.HTML(empty_summary_cards())
gr.HTML('<div class="section-title" style="margin-top:18px;">📋 Intelligent Inspection Report</div>')
report_html = gr.HTML(empty_report())
gr.HTML('<div class="section-title" style="margin-top:20px;">🖼 Example PCB Images</div>')
gr.Examples(
examples=EXAMPLES,
inputs=input_image,
label="Click any example to load it into the input panel"
)
gr.HTML(f"""
<div class="note-box">
<b>Note:</b> Severity, root cause, impact, and recommended action are generated from a structured PCB defect knowledge layer built on top of the model's class predictions.
The YOLO model itself performs visual defect detection; higher-level interpretation should still be reviewed in the context of PCB manufacturing and inspection workflows.
<br><br>
<b>Model card:</b>
<a class="footer-link" href="https://huggingface.co/{MODEL_REPO}" target="_blank">
{MODEL_REPO}
</a>
</div>
""")
detect_btn.click(
fn=detect,
inputs=[input_image, conf_slider],
outputs=[output_image, summary_html, report_html]
)
clear_btn.click(
fn=clear_all,
inputs=[],
outputs=[input_image, conf_slider, output_image, summary_html, report_html]
)
if __name__ == "__main__":
demo.launch()