import os import sys import torch import numpy as np import supervision as sv from inference import get_model from PIL import Image from typing import List, Dict from collections import Counter import shutil import tempfile from flask import Flask, request, jsonify, render_template from dotenv import load_dotenv # Carrega as variáveis do arquivo .env para o ambiente load_dotenv() # Adiciona o diretório 'Long-CLIP' ao path para encontrar a pasta 'model' long_clip_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "Long-CLIP") sys.path.append(long_clip_path) try: from model import longclip except ImportError: print(f"Erro: A pasta 'model' do LongCLIP não foi encontrada em: {long_clip_path}") # Suprime avisos de dependências ausentes da biblioteca inference (opcional) os.environ["CORE_MODEL_SAM_ENABLED"] = "False" os.environ["CORE_MODEL_SAM3_ENABLED"] = "False" os.environ["CORE_MODEL_GAZE_ENABLED"] = "False" os.environ["CORE_MODEL_YOLO_WORLD_ENABLED"] = "False" app = Flask(__name__) # --- CONFIGURAÇÕES INICIAIS --- # A chave da API do Roboflow agora é carregada automaticamente do arquivo .env device = "cuda" if torch.cuda.is_available() else "cpu" output_dir = "static/outputs" app.config['UPLOAD_FOLDER'] = tempfile.gettempdir() # --- FUNÇÕES DO LONGCLIP --- def load_longclip_model(checkpoint_path, device="cpu"): checkpoint_path = os.path.normpath(checkpoint_path) print(f"Carregando LongCLIP de {checkpoint_path}...") if not os.path.exists(checkpoint_path): raise FileNotFoundError(f"Erro: O arquivo de checkpoint não foi encontrado em: {checkpoint_path}") model, preprocess = longclip.load(checkpoint_path, device=device) return model, preprocess def get_text_features(model, text_descriptions: Dict[str, str], device="cpu"): class_names = list(text_descriptions.keys()) descriptions = list(text_descriptions.values()) print(f"Tokenizando {len(descriptions)} descrições detalhadas...") tokens = longclip.tokenize(descriptions).to(device) with torch.no_grad(): features = model.encode_text(tokens) features /= features.norm(dim=-1, keepdim=True) return features, class_names # --- DICIONÁRIO DE DESCRIÇÕES OTIMIZADO PARA LONGCLIP --- text_descriptions_longclip = { "Mancha de Olho Pardo (Cercospora)": ( "A close-up photograph of a green coffee leaf showing scattered, circular or oval necrotic spots. " "The most prominent feature is a large dark reddish-brown ring surrounding a paler, lighter-colored center (often pale grey, beige, or light brown). " "This creates a clear 'eye' or 'bullseye' appearance. " "These dark brown spots are frequently surrounded by a very prominent, extensive, and diffuse yellow or orange halo spreading across the green leaf. " "The spot surface is perfectly flat and dry. " "Crucially, it completely lacks any raised orange powder or granular dust. " "There is also absolutely no pitch-black color in the lesion; it is only dark brown, never black." ), "Ferrugem do Cafeeiro": ( "A highly detailed, close-up photograph of a green coffee leaf infected with Coffee Rust. " "The most striking visual feature is the presence of bright yellow to vivid cadmium-orange patches. " "These patches have a highly textured, three-dimensional granular and powdery appearance, " "looking exactly like thick orange powder, fine loose dust, or tiny accumulated pollen spores sitting entirely on top of the leaf surface. " "The edges of these bright orange spots are soft, diffuse, and blurred, seamlessly blending into the surrounding green leaf tissue. " "There are absolutely no sharp, well-defined dark borders. " "The spots are irregular in shape and frequently merge together to form large, amorphous, powdery orange masses. " "Crucially, the rust powder is never dark, never brown, and has absolutely no dark tones; it is exclusively bright yellow and vivid orange. " "The leaf must NOT have any large circular dark-brown necrotic spots. " "It completely lacks distinct dark brown concentric rings or 'bullseye' shapes, " "and is clearly characterized by its vibrant, bright, powdery, and dusty orange texture without any dark dead tissue." ), "Bicho Mineiro do Café": ( "A close-up photograph of a green coffee leaf severely damaged by the Coffee Leaf Miner insect. " "The damage presents as highly irregular, dry, papery, and translucent 'mines' or blisters. " "These mines often coalesce into massive, sprawling, irregular necrotic patches of light-brown, beige, or purplish-brown dead tissue. " "Crucially, these large dead patches are frequently surrounded by a very prominent and wide bright yellow or pale-green halo. " "The surface of the dead tissue looks wrinkled, completely dry, and papery, sometimes with peeling translucent skin. " "It completely lacks any perfectly circular 'bullseye' spots with a central dot. " "It completely lacks any pitch-black scorch marks on the extreme leaf edges. " "It completely lacks any raised, powdery orange dust or granular texture." ), "Mancha de Phoma": ( "A close-up photograph of a green coffee leaf severely infected with Phoma Leaf Spot. " "The absolute defining characteristic is a thick, solid, opaque, dark necrotic mass, usually dark-brown to pitch-black. " "This solid dark lesion almost always originates directly on the extreme margins (edges) or the tip of the leaf, aggressively spreading inward. " "It forms a solid, compact block of thick, dead rotting tissue, never a network of irregular serpentine galleries. " "It often causes the leaf edge to curl and tear. " "It frequently has tiny black dots (fungal fruiting bodies) inside the solid dark mass. " "It is never a translucent, flat, hollow papery blister or a network of dry serpentine galleries in the middle of the leaf. " "It completely lacks any perfectly circular geometries with a bright white central 'bullseye' dot. " "It completely lacks any raised, bright, granular powdery orange dust." ) } @app.route('/') def home(): supabase_url = os.environ.get('SUPABASE_URL') supabase_key = os.environ.get('SUPABASE_ANON_KEY') return render_template('index.html', supabase_url=supabase_url, supabase_key=supabase_key) @app.route('/redefinir-senha') def redefinir_senha(): supabase_url = os.environ.get('SUPABASE_URL') supabase_key = os.environ.get('SUPABASE_ANON_KEY') return render_template('redefinir_senha.html', supabase_url=supabase_url, supabase_key=supabase_key) def process_leaf_image(image_path, model_roboflow, long_model, long_preprocess, text_features, class_names, device): # LÓGICA DE LIMPEZA DA PASTA if os.path.exists(output_dir): for filename in os.listdir(output_dir): file_path = os.path.join(output_dir, filename) try: if os.path.isfile(file_path) or os.path.islink(file_path): os.unlink(file_path) elif os.path.isdir(file_path): shutil.rmtree(file_path) except Exception as e: print(f'Falha ao deletar {file_path}. Motivo: {e}') else: os.makedirs(output_dir) # 1. DETECÇÃO (ROBOFLOW) image = Image.open(image_path) predictions = model_roboflow.infer(image, confidence=0.65)[0] detections = sv.Detections.from_inference(predictions) img_width, img_height = image.size padding_percentage = 0.20 min_dim_for_padding = 100 for i, xyxy in enumerate(detections.xyxy): x_min_orig, y_min_orig, x_max_orig, y_max_orig = xyxy bbox_width_orig = x_max_orig - x_min_orig bbox_height_orig = y_max_orig - y_min_orig final_x_min, final_y_min, final_x_max, final_y_max = x_min_orig, y_min_orig, x_max_orig, y_max_orig if bbox_width_orig < min_dim_for_padding or bbox_height_orig < min_dim_for_padding: pad_x = bbox_width_orig * padding_percentage pad_y = bbox_height_orig * padding_percentage final_x_min = max(0, x_min_orig - pad_x) final_y_min = max(0, y_min_orig - pad_y) final_x_max = min(img_width, x_max_orig + pad_x) final_y_max = min(img_height, y_max_orig + pad_y) final_x_min, final_y_min, final_x_max, final_y_max = int(final_x_min), int(final_y_min), int(final_x_max), int(final_y_max) cropped_image = image.crop((final_x_min, final_y_min, final_x_max, final_y_max)) cropped_image.save(os.path.join(output_dir, f"recorte_{i}.jpg")) # 2. CLASSIFICAÇÃO (LONGCLIP) results_list = [] image_urls = [] for file_name in os.listdir(output_dir): if file_name.startswith("recorte_") and file_name.endswith(".jpg"): img_path = os.path.join(output_dir, file_name) img_input = Image.open(img_path) image_input = long_preprocess(img_input).unsqueeze(0).to(device) with torch.no_grad(): image_features = long_model.encode_image(image_input) image_features /= image_features.norm(dim=-1, keepdim=True) logits = (100.0 * image_features @ text_features.T) probs = logits.softmax(dim=-1).cpu().numpy()[0] top_idx = np.argmax(probs) predicted_class = class_names[top_idx] top_prob = float(probs[top_idx]) results_list.append(predicted_class) image_urls.append({ "url": f"/static/outputs/{file_name}", "classe": predicted_class, "probabilidade": top_prob }) contagem = Counter(results_list) mais_frequente = contagem.most_common(1)[0][0] if results_list else "Nenhuma detecção encontrada" return { "contagem": dict(contagem), "mais_frequente": mais_frequente, "total": sum(contagem.values()), "imagens": image_urls } @app.route('/predict', methods=['POST']) def predict(): if 'file' not in request.files: return jsonify({'error': 'Nenhum arquivo enviado'}), 400 file = request.files['file'] if file.filename == '': return jsonify({'error': 'Arquivo vazio selecionado'}), 400 try: image_path = os.path.join(app.config['UPLOAD_FOLDER'], "temp_leaf.jpg") file.save(image_path) model_roboflow = get_model("bracol-validado-region-detect-oupfv/1") checkpoint_abs_path = os.path.join(long_clip_path, "checkpoints", "longclip-L.pt") long_model, long_preprocess = load_longclip_model(checkpoint_abs_path, device) text_features, class_names = get_text_features(long_model, text_descriptions_longclip, device) result = process_leaf_image( image_path, model_roboflow, long_model, long_preprocess, text_features, class_names, device ) return jsonify(result) except Exception as e: return jsonify({'error': str(e)}), 500 @app.route('/predict_batch', methods=['POST']) def predict_batch(): if 'files' not in request.files: return jsonify({'error': 'Nenhum arquivo enviado'}), 400 files = request.files.getlist('files') if not files or files[0].filename == '': return jsonify({'error': 'Nenhum arquivo selecionado'}), 400 try: # Carregar modelos uma única vez para o lote inteiro model_roboflow = get_model("bracol-validado-region-detect-oupfv/1") checkpoint_abs_path = os.path.join(long_clip_path, "checkpoints", "longclip-L.pt") long_model, long_preprocess = load_longclip_model(checkpoint_abs_path, device) text_features, class_names = get_text_features(long_model, text_descriptions_longclip, device) batch_results = [] for idx, file in enumerate(files): image_path = os.path.join(app.config['UPLOAD_FOLDER'], f"temp_leaf_batch_{idx}.jpg") file.save(image_path) result = process_leaf_image( image_path, model_roboflow, long_model, long_preprocess, text_features, class_names, device ) batch_results.append({ "filename": file.filename, "mais_frequente": result["mais_frequente"] }) # Consolidar a contagem da doença principal de cada folha do lote doencas_principais = [res["mais_frequente"] for res in batch_results if res["mais_frequente"] != "Nenhuma detecção encontrada"] contagem_geral = Counter(doencas_principais) # Calcular porcentagens e garantir que todas as classes (doenças) apareçam no relatório total_validas = len(doencas_principais) estatisticas = {} for doenca in class_names: qtd = contagem_geral.get(doenca, 0) pct = (qtd / total_validas * 100) if total_validas > 0 else 0 estatisticas[doenca] = { "quantidade": qtd, "porcentagem": round(pct, 1) } return jsonify({ "estatisticas": estatisticas, "detalhes": batch_results, "total_folhas": len(files), "folhas_com_deteccao": total_validas }) except Exception as e: return jsonify({'error': str(e)}), 500 if __name__ == '__main__': app.run(debug=True, use_reloader=False)