esquema-gerber / app.py
Luis-Filipe's picture
Update app.py
a5aa8ae verified
# --- START OF FILE app.py ---
import gradio as gr
import cv2
import numpy as np
from PIL import Image, ImageDraw, ImageFont
import pytesseract
import re
import requests
import json
# --- MÓDULO 1: API WRAPPER E BIBLIOTECAS (Mantido) ---
OCTOPART_API_KEY = "eyJhbGciOiJSUzI1NiIsImtpZCI6IjA5NzI5QTkyRDU0RDlERjIyRDQzMENBMjNDNkI4QjJFIiwidHlwIjoiYXQrand0In0.eyJuYmYiOjE3NTI2NTA3NDgsImV4cCI6MTc1MjczNzE0OCwiaXNzIjoiaHR0cHM6Ly9pZGVudGl0eS5uZXhhci5jb20iLCJjbGllbnRfaWQiOiI5NDJiY2U5NC1mYmYwLTRhM2YtODFlNS1iOWZhYjRkMzI2MDUiLCJzdWIiOiJBNDVBMzU5OS0xNDEwLTQ3NUMtQTg3RC04RDg4ODc5QTg4RTkiLCJhdXRoX3RpbWUiOjE3NTI2NTA0NDgsImlkcCI6Ikdvb2dsZSIsInByaXZhdGVfY2xhaW1zX2lkIjoiNzcwZDUwMWYtNjc5YS00YjVjLWFmYzctYzhlN2FhNzdkNTNmIiwicHJpdmF0ZV9jbGFpbXNfc2VjcmV0IjoiTWF5TnhaYjFxKzlEdis2UGJkMTlRZFp4OWQ4ek9sR0IrVjVvT2hGZGFmTT0iLCJqdGkiOiI4NUFGMkU3QUQ1MzI0MzAxNUZDRkQ1QjMyQzc0QjVDMSIsInNpZCI6Ijg4OUQ1RDM0MUMyQ0U4MjZDODFBMThFMURFODkxMjY0IiwiaWF0IjoxNzUyNjUwNzQ4LCJzY29wZSI6WyJvcGVuaWQiLCJ1c2VyLmFjY2VzcyIsInByb2ZpbGUiLCJlbWFpbCIsInVzZXIuZGV0YWlscyIsInN1cHBseS5kb21haW4iLCJkZXNpZ24uZG9tYWluIl0sImFtciI6WyJleHRlcm5hbCJdfQ.tLu5fClrokUwW-5W3crUIR8DXp5RiSbtduvz-RIT3RAaNCkuMYSQQaSGg9CtLF_LzlcNQ3a5vIKLT47aX1CQRnvc2M-Q_R28SP2-AuNqHs_O4UwRFoVsbuSQfa3enGCUpm7L2ZCtWtkW4AG38iwH97zBYAHryl6cMRmvi-dGmc6H26sSBAcg7G7sDbHlnxxjM_I7V3wSTzrQUAgkQcCsu2h_fxhEiWzWWiJ2SjNKZt6_gv3vnMWKI3Y4FC_zytv-Yf0dcBGtLhX8Z9o5DdB2V1uLXEF6VUsnl_zIz52-YuxUn0wuztnig3g0hy-fS6OnWW6zb3uI_A_mBNjsOJH36Q" # <-- SUBSTITUA PELA SUA CHAVE REAL
FOOTPRINT_LIBRARY = {
"DIP-8": {"size_mm": (9.8, 6.35), "pin_count": 8},
"DIP-16": {"size_mm": (19.3, 6.35), "pin_count": 16},
"TO-92": {"size_mm": (4.5, 4.5), "pin_count": 3},
"AXIAL-0.4": {"size_mm": (10.2, 3.6), "pin_count": 2},
"RADIAL-0.2": {"size_mm": (5, 5), "pin_count": 2},
"UNKNOWN": {"size_mm": (10, 10), "pin_count": 2}
}
def get_component_data_from_api(part_number):
if OCTOPART_API_KEY == "eyJhbGciOiJSUzI1NiIsImtpZCI6IjA5NzI5QTkyRDU0RDlERjIyRDQzMENBMjNDNkI4QjJFIiwidHlwIjoiYXQrand0In0.eyJuYmYiOjE3NTI2NTA3NDgsImV4cCI6MTc1MjczNzE0OCwiaXNzIjoiaHR0cHM6Ly9pZGVudGl0eS5uZXhhci5jb20iLCJjbGllbnRfaWQiOiI5NDJiY2U5NC1mYmYwLTRhM2YtODFlNS1iOWZhYjRkMzI2MDUiLCJzdWIiOiJBNDVBMzU5OS0xNDEwLTQ3NUMtQTg3RC04RDg4ODc5QTg4RTkiLCJhdXRoX3RpbWUiOjE3NTI2NTA0NDgsImlkcCI6Ikdvb2dsZSIsInByaXZhdGVfY2xhaW1zX2lkIjoiNzcwZDUwMWYtNjc5YS00YjVjLWFmYzctYzhlN2FhNzdkNTNmIiwicHJpdmF0ZV9jbGFpbXNfc2VjcmV0IjoiTWF5TnhaYjFxKzlEdis2UGJkMTlRZFp4OWQ4ek9sR0IrVjVvT2hGZGFmTT0iLCJqdGkiOiI4NUFGMkU3QUQ1MzI0MzAxNUZDRkQ1QjMyQzc0QjVDMSIsInNpZCI6Ijg4OUQ1RDM0MUMyQ0U4MjZDODFBMThFMURFODkxMjY0IiwiaWF0IjoxNzUyNjUwNzQ4LCJzY29wZSI6WyJvcGVuaWQiLCJ1c2VyLmFjY2VzcyIsInByb2ZpbGUiLCJlbWFpbCIsInVzZXIuZGV0YWlscyIsInN1cHBseS5kb21haW4iLCJkZXNpZ24uZG9tYWluIl0sImFtciI6WyJleHRlcm5hbCJdfQ.tLu5fClrokUwW-5W3crUIR8DXp5RiSbtduvz-RIT3RAaNCkuMYSQQaSGg9CtLF_LzlcNQ3a5vIKLT47aX1CQRnvc2M-Q_R28SP2-AuNqHs_O4UwRFoVsbuSQfa3enGCUpm7L2ZCtWtkW4AG38iwH97zBYAHryl6cMRmvi-dGmc6H26sSBAcg7G7sDbHlnxxjM_I7V3wSTzrQUAgkQcCsu2h_fxhEiWzWWiJ2SjNKZt6_gv3vnMWKI3Y4FC_zytv-Yf0dcBGtLhX8Z9o5DdB2V1uLXEF6VUsnl_zIz52-YuxUn0wuztnig3g0hy-fS6OnWW6zb3uI_A_mBNjsOJH36Q":
return None, "Erro: Chave de API da Octopart não configurada."
url = f"http://octopart.com/api/v3/parts/match?apikey={OCTOPART_API_KEY}&queries=[{{\"mpn\":\"{part_number}\"}}]"
try:
response = requests.get(url)
response.raise_for_status()
data = response.json()
if data['results'] and data['results'][0]['hits'] > 0:
item = data['results'][0]['items'][0]
specs = item.get('specs', {})
footprint = specs.get('case_package', {}).get('value', ["Desconhecido"])[0]
datasheet_url = item.get('datasheet_url', "N/A")
return {'footprint': footprint, 'datasheet': datasheet_url}, None
except requests.exceptions.RequestException as e:
return None, f"Erro de API: {e}"
return None, f"Componente '{part_number}' não encontrado."
# --- MÓDULO 2: OCR CIRÚRGICO E ANÁLISE (A GRANDE MUDANÇA) ---
def analyze_schematic_with_surgical_ocr(image_pil):
if image_pil is None:
return Image.new('RGB', (1,1)), "Faça o upload de uma imagem."
image_cv = np.array(image_pil.convert("RGB"))
gray = cv2.cvtColor(image_cv, cv2.COLOR_BGR2GRAY)
# Pré-processamento da imagem para melhorar OCR e deteção de contornos
_, thresh = cv2.threshold(gray, 200, 255, cv2.THRESH_BINARY_INV)
contours, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
components = []
for cnt in contours:
# Ignorar contornos muito pequenos (ruído) ou muito grandes (bordas da imagem)
if not (50 < cv2.contourArea(cnt) < 50000):
continue
x, y, w, h = cv2.boundingRect(cnt)
# OCR CIRÚRGICO: Ler texto apenas na vizinhança do contorno
roi_x, roi_y = max(0, x - 30), max(0, y - 30)
roi_w, roi_h = w + 60, h + 60
roi = gray[roi_y:roi_y+roi_h, roi_x:roi_x+roi_w]
text = pytesseract.image_to_string(roi, config='--psm 6').strip()
# Expressões Regulares mais abrangentes
identifier_match = re.search(r'([RUCTZDQ]\d{1,2})', text, re.IGNORECASE)
value_match = re.search(r'(\d{3,4}|[A-Z]{2}\d{4,5}|BC\d{3}|[0-9.]+[MUKPFVnµ])', text, re.IGNORECASE)
if identifier_match:
identifier = identifier_match.group(1).upper()
value = value_match.group(1).upper() if value_match else "N/A"
# Limpar valores
if "555" in value: value = "555"
if "CD4017" in value: value = "CD4017"
if "BC547" in value: value = "BC547"
components.append({
'id': identifier,
'value': value,
'box': (x, y, w, h)
})
# --- MÓDULO 3: CONSULTA À API E VISUALIZAÇÃO ---
log = ""
vis_image = image_cv.copy()
if not components:
return Image.new('RGB', (1,1)), "Nenhum componente detetado pelos contornos."
for comp in components:
log += f"--- Componente Detetado ---\n"
log += f" ID: {comp['id']}\n"
log += f" Valor/Nome: {comp['value']}\n"
# Desenha a caixa delimitadora do componente
x, y, w, h = comp['box']
cv2.rectangle(vis_image, (x, y), (x+w, y+h), (0, 0, 255), 2)
cv2.putText(vis_image, f"{comp['id']}: {comp['value']}", (x, y-10),
cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 0, 0), 2)
# O valor/nome é o que procuramos na API
part_number = comp['value']
api_data, error = get_component_data_from_api(part_number)
if error:
log += f" API: {error}\n"
else:
footprint = api_data.get('footprint', "Desconhecido")
datasheet = api_data.get('datasheet', "N/A")
log += f" Footprint (API): {footprint}\n"
log += f" Datasheet (API): {datasheet}\n"
return Image.fromarray(cv2.cvtColor(vis_image, cv2.COLOR_BGR2RGB)), log
# --- Interface Gradio ---
app = gr.Interface(
fn=analyze_schematic_with_surgical_ocr,
inputs=gr.Image(type="pil", label="Upload do Esquema Elétrico"),
outputs=[
gr.Image(type="pil", label="Análise do Esquema"),
gr.Textbox(label="Log da Análise (OCR + API)", lines=20)
],
title="Analisador de Esquemas com OCR Cirúrgico (v17)",
description="""**Esta versão usa uma abordagem de OCR muito mais precisa.**
1. **Deteta** a localização de cada símbolo de componente.
2. **Lê** o texto (identificador e valor) apenas na área próxima a cada símbolo.
3. **Consulta** a API da Octopart usando o valor/nome lido.
4. **Apresenta** um log detalhado da análise."""
)
if __name__ == "__main__":
app.launch()