Spaces:
Sleeping
Sleeping
| # --- 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() |