| from flask import Flask, request, jsonify | |
| from flask_cors import CORS | |
| import torch | |
| import torch.nn as nn | |
| from torchvision import models, transforms | |
| from PIL import Image | |
| import io | |
| import os | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Flask App Setup | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| app = Flask(__name__) | |
| CORS(app) # Allow mobile app to connect | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Configuration | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| MODEL_PATH = "resnet18_rice_v2.pth" # β your saved model | |
| CLASS_NAMES = ["ClassA-Drought", "ClassB-PestInfestation", "ClassC-Healthy"] | |
| THRESHOLD = 0.80 # below this = uncertain | |
| IMG_SIZE = 224 | |
| device = torch.device("cuda" if torch.cuda.is_available() else "cpu") | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Load Model Once at Startup | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def load_model(): | |
| model = models.resnet18(weights=None) | |
| model.fc = nn.Linear(model.fc.in_features, len(CLASS_NAMES)) | |
| checkpoint = torch.load(MODEL_PATH, map_location=device) | |
| model.load_state_dict(checkpoint["model_state_dict"]) | |
| model = model.to(device) | |
| model.eval() | |
| print(f"β Model loaded from {MODEL_PATH}") | |
| print(f"β Classes: {CLASS_NAMES}") | |
| return model | |
| model = load_model() | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Image Transform | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| transform = transforms.Compose([ | |
| transforms.Resize((IMG_SIZE, IMG_SIZE)), | |
| transforms.ToTensor(), | |
| transforms.Normalize(mean=[0.485, 0.456, 0.406], | |
| std=[0.229, 0.224, 0.225]) | |
| ]) | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Routes | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Health check β test if API is running | |
| def home(): | |
| return jsonify({ | |
| "status" : "running", | |
| "message" : "Rice Stress Detection API", | |
| "classes" : CLASS_NAMES, | |
| "model" : "ResNet18 β 99.66% accuracy" | |
| }) | |
| # Main prediction endpoint | |
| def predict(): | |
| # ββ Check if image was sent βββββββββββββββββββββββββββββ | |
| if "image" not in request.files: | |
| return jsonify({ | |
| "error": "No image provided. Send image as form-data with key 'image'" | |
| }), 400 | |
| file = request.files["image"] | |
| # ββ Check file is valid βββββββββββββββββββββββββββββββββ | |
| if file.filename == "": | |
| return jsonify({"error": "Empty filename"}), 400 | |
| allowed = {"jpg", "jpeg", "png", "bmp", "webp"} | |
| ext = file.filename.rsplit(".", 1)[-1].lower() | |
| if ext not in allowed: | |
| return jsonify({"error": f"File type .{ext} not allowed. Use jpg/png"}), 400 | |
| try: | |
| # ββ Read and process image ββββββββββββββββββββββββββ | |
| image_bytes = file.read() | |
| image = Image.open(io.BytesIO(image_bytes)).convert("RGB") | |
| tensor = transform(image).unsqueeze(0).to(device) | |
| # ββ Run prediction ββββββββββββββββββββββββββββββββββ | |
| with torch.no_grad(): | |
| outputs = model(tensor) | |
| probs = torch.softmax(outputs, dim=1)[0] | |
| confidence, predicted = torch.max(probs, 0) | |
| predicted_class = CLASS_NAMES[predicted.item()] | |
| conf_value = confidence.item() | |
| is_uncertain = conf_value < THRESHOLD | |
| # ββ Build response ββββββββββββββββββββββββββββββββββ | |
| all_probs = { | |
| CLASS_NAMES[i]: round(probs[i].item() * 100, 2) | |
| for i in range(len(CLASS_NAMES)) | |
| } | |
| # Clean class name for display | |
| display_name = predicted_class.replace("ClassA-", "")\ | |
| .replace("ClassB-", "")\ | |
| .replace("ClassC-", "") | |
| response = { | |
| "predicted_class" : predicted_class, | |
| "display_name" : display_name, | |
| "confidence" : round(conf_value * 100, 2), | |
| "is_uncertain" : is_uncertain, | |
| "all_probabilities": all_probs, | |
| "recommendation" : get_recommendation(display_name, is_uncertain) | |
| } | |
| return jsonify(response), 200 | |
| except Exception as e: | |
| return jsonify({"error": str(e)}), 500 | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Recommendation Logic | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def get_recommendation(class_name, is_uncertain): | |
| if is_uncertain: | |
| return "Low confidence β recommend manual inspection by agronomist" | |
| recommendations = { | |
| "Drought": ( | |
| "Rice plant shows drought stress. " | |
| "Recommended actions: increase irrigation frequency, " | |
| "check soil moisture levels, consider mulching." | |
| ), | |
| "PestInfestation": ( | |
| "Rice plant shows pest infestation signs. " | |
| "Recommended actions: inspect leaves for insects, " | |
| "apply appropriate pesticide, monitor surrounding plants." | |
| ), | |
| "Healthy": ( | |
| "Rice plant appears healthy. " | |
| "Continue regular monitoring and maintenance." | |
| ) | |
| } | |
| return recommendations.get(class_name, "No recommendation available") | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Run Server | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| if __name__ == "__main__": | |
| port = int(os.environ.get("PORT", 7860)) | |
| print("πΎ Rice Stress Detection API starting...") | |
| app.run(debug=False, host="0.0.0.0", port=port) | |