File size: 6,156 Bytes
565aecf | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 | """
app.py
------
Flask REST API backend for Leaf Disease Detector.
"""
import argparse
import base64
import io
import time
from functools import wraps
from pathlib import Path
from flask import Flask, jsonify, request, send_from_directory
from flask_cors import CORS
from PIL import Image
from predict import LeafDiseasePredictor
# βββ App Setup ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
app = Flask(__name__, static_folder="frontend", static_url_path="")
CORS(app, resources={r"/api/*": {"origins": "*"}})
MAX_FILE_SIZE = 10 * 1024 * 1024
ALLOWED_EXTENSIONS = {".jpg", ".jpeg", ".png", ".webp", ".bmp", ".gif"}
_predictor = None
def get_predictor() -> LeafDiseasePredictor:
global _predictor
if _predictor is None:
_predictor = LeafDiseasePredictor()
return _predictor
# βββ Helpers ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def allowed_file(filename: str) -> bool:
return Path(filename).suffix.lower() in ALLOWED_EXTENSIONS
def error_response(message: str, code: int = 400):
return jsonify({"success": False, "error": message}), code
def timing(f):
@wraps(f)
def wrapper(*args, **kwargs):
t0 = time.time()
result = f(*args, **kwargs)
elapsed = (time.time() - t0) * 1000
try:
data = result[0].get_json()
if data:
data["inference_ms"] = round(elapsed, 1)
return jsonify(data), result[1]
except Exception:
pass
return result
return wrapper
# βββ Routes βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
@app.route("/")
def index():
frontend_path = Path("frontend/index.html")
if frontend_path.exists():
return send_from_directory("frontend", "index.html")
return jsonify({"error": "Frontend not found"}), 404
@app.route("/api/health", methods=["GET"])
def health():
try:
p = get_predictor()
return jsonify({
"status": "ok",
"num_classes": p.num_classes,
"device": str(p.device),
})
except Exception as e:
return jsonify({"status": "error", "message": str(e)}), 503
@app.route("/api/classes", methods=["GET"])
def list_classes():
predictor = get_predictor()
classes_info = []
for cls in predictor.classes:
info = predictor.disease_info.get(cls, {})
parts = cls.split("___")
classes_info.append({
"class_id": cls,
"plant": parts[0].replace("_", " ") if len(parts) > 0 else cls,
"disease": parts[1].replace("_", " ") if len(parts) > 1 else "",
"severity": info.get("severity", "Unknown"),
})
return jsonify({"success": True, "classes": classes_info, "count": len(classes_info)})
@app.route("/api/predict", methods=["POST"])
@timing
def predict_file():
if "image" not in request.files:
return error_response("No image file provided.")
file = request.files["image"]
if file.filename == "":
return error_response("Empty filename.")
if not allowed_file(file.filename):
return error_response("Unsupported file type.")
data = file.read()
if len(data) > MAX_FILE_SIZE:
return error_response("File too large.")
try:
img = Image.open(io.BytesIO(data)).convert("RGB")
except Exception as e:
return error_response(f"Cannot open image: {e}")
try:
predictor = get_predictor()
result = predictor.predict(img)
thumb = img.copy()
thumb.thumbnail((300, 300))
buf = io.BytesIO()
thumb.save(buf, format="JPEG", quality=75)
result["thumbnail"] = "data:image/jpeg;base64," + base64.b64encode(buf.getvalue()).decode()
return jsonify({"success": True, "result": result}), 200
except Exception as e:
return error_response(f"Prediction failed: {e}", 500)
@app.route("/api/predict-url", methods=["POST"])
@timing
def predict_url():
body = request.get_json(silent=True)
if not body or "url" not in body:
return error_response("URL required.")
url = body["url"]
try:
predictor = get_predictor()
result = predictor.predict(url)
return jsonify({"success": True, "result": result}), 200
except Exception as e:
return error_response(f"Prediction failed: {e}", 500)
@app.route("/api/predict-base64", methods=["POST"])
@timing
def predict_base64():
body = request.get_json(silent=True)
if not body or "image" not in body:
return error_response("Base64 image required.")
try:
b64 = body["image"].split(",")[-1]
img_bytes = base64.b64decode(b64)
img = Image.open(io.BytesIO(img_bytes)).convert("RGB")
except Exception as e:
return error_response(f"Invalid base64: {e}")
try:
predictor = get_predictor()
result = predictor.predict(img)
return jsonify({"success": True, "result": result}), 200
except Exception as e:
return error_response(f"Prediction failed: {e}", 500)
# βββ Main βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("--port", type=int, default=7860)
parser.add_argument("--host", type=str, default="0.0.0.0")
parser.add_argument("--debug", action="store_true")
args = parser.parse_args()
print("πΏ LeafScan API starting...")
get_predictor()
app.run(host=args.host, port=args.port, debug=args.debug) |