CoffeeHealth-AI / app.py
SucoCafe's picture
Removido bloco inutilizado de calculo de veracidade em app.py
754523d
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():
return render_template('index.html')
@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:
# 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)