Spaces:
Sleeping
Sleeping
| 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}") | |
| async def lifespan(app: FastAPI): | |
| load_all_models() | |
| yield | |
| models.clear() | |
| app = FastAPI(lifespan=lifespan) | |
| 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)}") | |
| def home(): | |
| return FileResponse("index.html") | |
| if __name__ == "__main__": | |
| import uvicorn | |
| uvicorn.run(app, host="0.0.0.0", port=7860) |