Proyecto / main.py
dfernandezl12's picture
Upload 11 files
c1da183 verified
Raw
History Blame Contribute Delete
8.41 kB
import os
import numpy as np
import tensorflow as tf
from PIL import Image
import io
from fastapi import FastAPI, File, UploadFile, HTTPException
from fastapi.responses import FileResponse
from contextlib import asynccontextmanager
# --- CONFIGURACIÓN Y MAPEO DE CLASES ---
MODEL_PATHS = {
"basico": "modelos/modelo_basico.keras",
"cnn": "modelos/modelo_cnn_2.keras",
"data_aug": "modelos/modelo_cnn_color_data.keras",
"dropout": "modelos/modelo_cnn_color_data_dropout2.keras",
"cnn_gris": "modelos/modelo_cnn_gris.keras"
}
# Diccionario completo de las 38 clases (PlantVillage estándar)
PLANT_CLASSES = [
"Manzana: Escara", "Manzana: Podredumbre negra", "Manzana: Roya del cedro", "Manzana: Sana",
"Arándano: Sano", "Cereza: Oídio", "Cereza: Sana", "Maíz: Cercospora (Mancha gris)",
"Maíz: Roya común", "Maíz: Tizón del norte", "Maíz: Sano", "Uva: Podredumbre negra",
"Uva: Escariosis", "Uva: Mildiu", "Uva: Sana", "Naranja: Huanglongbing (Greening)",
"Melocotón: Mancha bacteriana", "Melocotón: Sano", "Pimiento: Mancha bacteriana", "Pimiento: Sano",
"Patata: Tizón temprano", "Patata: Tizón tardío", "Patata: Sana", "Frambuesa: Sana",
"Soja: Sana", "Calabaza: Oídio", "Fresa: Mancha foliar", "Fresa: Sana",
"Tomate: Mancha bacteriana", "Tomate: Tizón temprano", "Tomate: Tizón tardío", "Tomate: Moho foliar",
"Tomate: Mancha Septoria", "Tomate: Araña roja (Ácaros)", "Tomate: Mancha diana",
"Tomate: Virus del rizado amarillo", "Tomate: Virus del mosaico", "Tomate: Sano"
]
models = {}
# --- ARQUITECTURAS MANUALES ---
def crear_arquitectura_color(input_shape=(200, 200, 3)):
model = tf.keras.Sequential([
tf.keras.layers.Input(shape=input_shape),
tf.keras.layers.Resizing(200, 200),
tf.keras.layers.Rescaling(1./255),
tf.keras.layers.Conv2D(32, (3, 3), padding='same', activation='relu'),
tf.keras.layers.BatchNormalization(axis=3),
tf.keras.layers.MaxPooling2D((2, 2)),
tf.keras.layers.Conv2D(64, (3, 3), padding='same', activation='relu'),
tf.keras.layers.BatchNormalization(axis=3),
tf.keras.layers.MaxPooling2D((2, 2)),
tf.keras.layers.Conv2D(128, (3, 3), padding='same', activation='relu'),
tf.keras.layers.BatchNormalization(axis=3),
tf.keras.layers.MaxPooling2D((2, 2)),
tf.keras.layers.Conv2D(256, (3, 3), padding='same', activation='relu'),
tf.keras.layers.BatchNormalization(axis=3),
tf.keras.layers.MaxPooling2D((2, 2)),
tf.keras.layers.Flatten(),
tf.keras.layers.Dense(256, activation='relu'),
tf.keras.layers.Dense(38, activation='softmax')
])
return model
def crear_arquitectura_dropout(input_shape=(200, 200, 3)):
"""Arquitectura corregida para coincidir con los pesos (Shape 256, 256)."""
model = tf.keras.Sequential([
tf.keras.layers.Input(shape=input_shape),
tf.keras.layers.Resizing(200, 200),
tf.keras.layers.Rescaling(1./255),
tf.keras.layers.Conv2D(32, (3, 3), padding='same', activation='relu'),
tf.keras.layers.MaxPooling2D((2, 2)),
tf.keras.layers.Conv2D(64, (3, 3), padding='same', activation='relu'),
tf.keras.layers.MaxPooling2D((2, 2)),
tf.keras.layers.Conv2D(128, (3, 3), padding='same', activation='relu'),
tf.keras.layers.MaxPooling2D((2, 2)),
# Añadimos una capa extra de 256 para que el Global sea de 256
tf.keras.layers.Conv2D(256, (3, 3), padding='same', activation='relu'),
tf.keras.layers.MaxPooling2D((2, 2)),
tf.keras.layers.GlobalAveragePooling2D(),
tf.keras.layers.Dense(256, activation='relu'),
tf.keras.layers.Dropout(0.5),
tf.keras.layers.Dense(38, activation='softmax')
])
return model
def crear_arquitectura_gris(input_shape=(200, 200, 1)):
model = tf.keras.Sequential([
tf.keras.layers.Input(shape=input_shape),
tf.keras.layers.Resizing(200, 200),
tf.keras.layers.Rescaling(1./255),
tf.keras.layers.Conv2D(32, (3, 3), padding='same', activation='relu'),
tf.keras.layers.BatchNormalization(axis=3),
tf.keras.layers.MaxPooling2D((2, 2)),
tf.keras.layers.Conv2D(64, (3, 3), padding='same', activation='relu'),
tf.keras.layers.BatchNormalization(axis=3),
tf.keras.layers.MaxPooling2D((2, 2)),
tf.keras.layers.Conv2D(128, (3, 3), padding='same', activation='relu'),
tf.keras.layers.BatchNormalization(axis=3),
tf.keras.layers.MaxPooling2D((2, 2)),
tf.keras.layers.Flatten(),
tf.keras.layers.Dense(256, activation='relu'),
tf.keras.layers.Dropout(0.5),
tf.keras.layers.Dense(38, activation='softmax')
])
return model
# --- LÓGICA DE CARGA Y APP ---
def load_all_models():
for mid, path in MODEL_PATHS.items():
if not os.path.exists(path):
print(f"⚠️ Archivo no encontrado: {path}")
continue
try:
models[mid] = tf.keras.models.load_model(path, compile=False)
print(f"✅ {mid} cargado directamente.")
except Exception:
try:
if mid == "cnn_gris": model = crear_arquitectura_gris()
elif mid == "dropout": model = crear_arquitectura_dropout()
else: model = crear_arquitectura_color()
model.load_weights(path)
models[mid] = model
print(f"✅ {mid} reconstruido manualmente.")
except Exception as e:
print(f"❌ Error crítico en {mid}: {e}")
@asynccontextmanager
async def lifespan(app: FastAPI):
load_all_models()
yield
models.clear()
app = FastAPI(lifespan=lifespan)
@app.post("/predict_all")
async def predict_all(file: UploadFile = File(...)):
if not models:
raise HTTPException(status_code=500, detail="No hay modelos cargados.")
try:
contents = await file.read()
results = []
names = {
"basico": "Modelo Lineal Básico",
"cnn": "CNN Color V2",
"dropout": "CNN Dropout (Mejorada)",
"data_aug": "CNN Data Augmentation",
"cnn_gris": "CNN Escala de Grises"
}
for mid, model in models.items():
try:
image_bytes = io.BytesIO(contents)
img = Image.open(image_bytes).resize((200, 200))
# Preprocesamiento según el modelo
if "gris" in mid:
img = img.convert("L")
img_array = np.array(img).astype('float32') / 255.0
img_array = np.expand_dims(img_array, axis=(0, -1))
else:
img = img.convert("RGB")
img_array = np.array(img).astype('float32') / 255.0
img_array = np.expand_dims(img_array, axis=0)
prediction = model.predict(img_array, verbose=0)
# Lógica para obtener el nombre de la clase
idx = np.argmax(prediction[0])
conf = float(prediction[0][idx])
# Intentar obtener el nombre de PLANT_CLASSES, si no, usar el índice
label = PLANT_CLASSES[idx] if idx < len(PLANT_CLASSES) else f"Clase {idx}"
# Clasificación binaria simple para la UI (Sana/Enferma)
estado = "Sana" if "Sana" in label or "Sano" in label else "Enferma"
results.append({
"id": mid,
"name": names.get(mid, mid),
"prediction": label, # Ahora muestra el nombre específico
"status": estado, # Para filtros rápidos en el frontend
"confidence": round(conf * 100, 2)
})
except Exception as e:
print(f"Error prediciendo con {mid}: {e}")
continue
return results
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error procesando imagen: {str(e)}")
@app.get("/")
def home():
return FileResponse("index.html")
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=7860)