Spaces:
Sleeping
Sleeping
| 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." | |
| ) | |
| } | |
| def home(): | |
| return render_template('index.html') | |
| 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: | |
| # A imagem é recebida do site | |
| image_path = os.path.join(app.config['UPLOAD_FOLDER'], "temp_leaf.jpg") | |
| file.save(image_path) | |
| # --- 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) | |
| print("Iniciando detecção com Roboflow...") | |
| model_roboflow = get_model("bracol-validado-region-detect-oupfv/1") | |
| predictions = model_roboflow.infer(image, confidence=0.65)[0] | |
| detections = sv.Detections.from_inference(predictions) | |
| # Salvar recortes com padding condicional | |
| print(f"Salvando novos recortes com padding condicional em '{output_dir}'...") | |
| # Obter dimensões da imagem original | |
| img_width, img_height = image.size | |
| padding_percentage = 0.20 # 20% de padding | |
| min_dim_for_padding = 100 # Dimensão mínima para aplicar padding | |
| for i, (xyxy, _, _, _, _, _) in enumerate(detections): | |
| x_min_orig, y_min_orig, x_max_orig, y_max_orig = xyxy | |
| # Calcular largura e altura da caixa delimitadora original | |
| bbox_width_orig = x_max_orig - x_min_orig | |
| bbox_height_orig = y_max_orig - y_min_orig | |
| # Inicializar as coordenadas finais com as originais | |
| final_x_min, final_y_min, final_x_max, final_y_max = x_min_orig, y_min_orig, x_max_orig, y_max_orig | |
| # Verificar se o recorte é pequeno o suficiente para aplicar o padding | |
| if bbox_width_orig < min_dim_for_padding or bbox_height_orig < min_dim_for_padding: | |
| # Calcular o valor do padding | |
| pad_x = bbox_width_orig * padding_percentage | |
| pad_y = bbox_height_orig * padding_percentage | |
| # Aplicar padding e garantir que as coordenadas não excedam os limites da imagem | |
| 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) | |
| # Converter para inteiros, pois image.crop espera inteiros | |
| 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)) | |
| filename = f"recorte_{i}.jpg" | |
| cropped_image.save(os.path.join(output_dir, filename)) | |
| # --- 2. CLASSIFICAÇÃO (LONGCLIP) --- | |
| 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) | |
| results_list = [] | |
| image_urls = [] | |
| print("\nIniciando classificação dos recortes com LongCLIP...") | |
| 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) | |
| # Cálculo de similaridade de cosseno (LongCLIP) | |
| 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 | |
| }) | |
| print(f"Arquivo {file_name}: {predicted_class} ({probs[top_idx]*100:.2f}%)") | |
| # --- 3. RELATÓRIO FINAL --- | |
| print("\n" + "="*30) | |
| print("RELATÓRIO DE SAÚDE DO CAFEEIRO") | |
| print("="*30) | |
| contagem = Counter(results_list) | |
| for doenca, qtd in contagem.items(): | |
| print(f"{doenca}: {qtd} ocorrência(s)") | |
| if not results_list: | |
| print("Nenhuma detecção encontrada para análise.") | |
| return jsonify({ | |
| "contagem": {}, | |
| "mais_frequente": "Nenhuma detecção encontrada", | |
| "total": 0, | |
| "imagens": [] | |
| }) | |
| mais_frequente = contagem.most_common(1)[0][0] | |
| return jsonify({ | |
| "contagem": dict(contagem), | |
| "mais_frequente": mais_frequente, | |
| "total": sum(contagem.values()), | |
| "imagens": image_urls | |
| }) | |
| except Exception as e: | |
| return jsonify({'error': str(e)}), 500 | |
| if __name__ == '__main__': | |
| app.run(debug=True, use_reloader=False) | |