marian522 commited on
Commit
f57d14e
Β·
verified Β·
1 Parent(s): 9a6827d

Upload 4 files

Browse files
Files changed (4) hide show
  1. Dockerfile +12 -0
  2. app.py +159 -0
  3. requirements.txt +5 -0
  4. resnet18_rice_v2.pth +3 -0
Dockerfile ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.10-slim
2
+
3
+ WORKDIR /app
4
+
5
+ COPY requirements.txt .
6
+ RUN pip install --no-cache-dir -r requirements.txt
7
+
8
+ COPY . .
9
+
10
+ EXPOSE 7860
11
+
12
+ CMD ["python", "app.py"]
app.py ADDED
@@ -0,0 +1,159 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Flask, request, jsonify
2
+ from flask_cors import CORS
3
+ import torch
4
+ import torch.nn as nn
5
+ from torchvision import models, transforms
6
+ from PIL import Image
7
+ import io
8
+ import os
9
+
10
+ # ══════════════════════════════════════════════════════════════
11
+ # Flask App Setup
12
+ # ══════════════════════════════════════════════════════════════
13
+ app = Flask(__name__)
14
+ CORS(app) # Allow mobile app to connect
15
+
16
+ # ══════════════════════════════════════════════════════════════
17
+ # Configuration
18
+ # ══════════════════════════════════════════════════════════════
19
+ MODEL_PATH = "resnet18_rice_v2.pth" # ← your saved model
20
+ CLASS_NAMES = ["ClassA-Drought", "ClassB-PestInfestation", "ClassC-Healthy"]
21
+ THRESHOLD = 0.80 # below this = uncertain
22
+ IMG_SIZE = 224
23
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
24
+
25
+ # ══════════════════════════════════════════════════════════════
26
+ # Load Model Once at Startup
27
+ # ══════════════════════════════════════════════════════════════
28
+ def load_model():
29
+ model = models.resnet18(weights=None)
30
+ model.fc = nn.Linear(model.fc.in_features, len(CLASS_NAMES))
31
+ checkpoint = torch.load(MODEL_PATH, map_location=device)
32
+ model.load_state_dict(checkpoint["model_state_dict"])
33
+ model = model.to(device)
34
+ model.eval()
35
+ print(f"βœ… Model loaded from {MODEL_PATH}")
36
+ print(f"βœ… Classes: {CLASS_NAMES}")
37
+ return model
38
+
39
+ model = load_model()
40
+
41
+ # ══════════════════════════════════════════════════════════════
42
+ # Image Transform
43
+ # ══════════════════════════════════════════════════════════════
44
+ transform = transforms.Compose([
45
+ transforms.Resize((IMG_SIZE, IMG_SIZE)),
46
+ transforms.ToTensor(),
47
+ transforms.Normalize(mean=[0.485, 0.456, 0.406],
48
+ std=[0.229, 0.224, 0.225])
49
+ ])
50
+
51
+ # ══════════════════════════════════════════════════════════════
52
+ # Routes
53
+ # ══════════════════════════════════════════════════════════════
54
+
55
+ # Health check β€” test if API is running
56
+ @app.route("/", methods=["GET"])
57
+ def home():
58
+ return jsonify({
59
+ "status" : "running",
60
+ "message" : "Rice Stress Detection API",
61
+ "classes" : CLASS_NAMES,
62
+ "model" : "ResNet18 β€” 99.66% accuracy"
63
+ })
64
+
65
+ # Main prediction endpoint
66
+ @app.route("/predict", methods=["POST"])
67
+ def predict():
68
+ # ── Check if image was sent ─────────────────────────────
69
+ if "image" not in request.files:
70
+ return jsonify({
71
+ "error": "No image provided. Send image as form-data with key 'image'"
72
+ }), 400
73
+
74
+ file = request.files["image"]
75
+
76
+ # ── Check file is valid ─────────────────────────────────
77
+ if file.filename == "":
78
+ return jsonify({"error": "Empty filename"}), 400
79
+
80
+ allowed = {"jpg", "jpeg", "png", "bmp", "webp"}
81
+ ext = file.filename.rsplit(".", 1)[-1].lower()
82
+ if ext not in allowed:
83
+ return jsonify({"error": f"File type .{ext} not allowed. Use jpg/png"}), 400
84
+
85
+ try:
86
+ # ── Read and process image ──────────────────────────
87
+ image_bytes = file.read()
88
+ image = Image.open(io.BytesIO(image_bytes)).convert("RGB")
89
+ tensor = transform(image).unsqueeze(0).to(device)
90
+
91
+ # ── Run prediction ──────────────────────────────────
92
+ with torch.no_grad():
93
+ outputs = model(tensor)
94
+ probs = torch.softmax(outputs, dim=1)[0]
95
+ confidence, predicted = torch.max(probs, 0)
96
+
97
+ predicted_class = CLASS_NAMES[predicted.item()]
98
+ conf_value = confidence.item()
99
+ is_uncertain = conf_value < THRESHOLD
100
+
101
+ # ── Build response ──────────────────────────────────
102
+ all_probs = {
103
+ CLASS_NAMES[i]: round(probs[i].item() * 100, 2)
104
+ for i in range(len(CLASS_NAMES))
105
+ }
106
+
107
+ # Clean class name for display
108
+ display_name = predicted_class.replace("ClassA-", "")\
109
+ .replace("ClassB-", "")\
110
+ .replace("ClassC-", "")
111
+
112
+ response = {
113
+ "predicted_class" : predicted_class,
114
+ "display_name" : display_name,
115
+ "confidence" : round(conf_value * 100, 2),
116
+ "is_uncertain" : is_uncertain,
117
+ "all_probabilities": all_probs,
118
+ "recommendation" : get_recommendation(display_name, is_uncertain)
119
+ }
120
+
121
+ return jsonify(response), 200
122
+
123
+ except Exception as e:
124
+ return jsonify({"error": str(e)}), 500
125
+
126
+
127
+ # ══════════════════════════════════════════════════════════════
128
+ # Recommendation Logic
129
+ # ══════════════════════════════════════════════════════════════
130
+ def get_recommendation(class_name, is_uncertain):
131
+ if is_uncertain:
132
+ return "Low confidence β€” recommend manual inspection by agronomist"
133
+
134
+ recommendations = {
135
+ "Drought": (
136
+ "Rice plant shows drought stress. "
137
+ "Recommended actions: increase irrigation frequency, "
138
+ "check soil moisture levels, consider mulching."
139
+ ),
140
+ "PestInfestation": (
141
+ "Rice plant shows pest infestation signs. "
142
+ "Recommended actions: inspect leaves for insects, "
143
+ "apply appropriate pesticide, monitor surrounding plants."
144
+ ),
145
+ "Healthy": (
146
+ "Rice plant appears healthy. "
147
+ "Continue regular monitoring and maintenance."
148
+ )
149
+ }
150
+ return recommendations.get(class_name, "No recommendation available")
151
+
152
+
153
+ # ══════════════════════════════════════════════════════════════
154
+ # Run Server
155
+ # ══════════════════════════════════════════════════════════════
156
+ if __name__ == "__main__":
157
+ port = int(os.environ.get("PORT", 7860))
158
+ print("🌾 Rice Stress Detection API starting...")
159
+ app.run(debug=False, host="0.0.0.0", port=port)
requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ flask==3.0.0
2
+ flask-cors==4.0.0
3
+ torch==2.1.0
4
+ torchvision==0.16.0
5
+ Pillow==10.0.0
resnet18_rice_v2.pth ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:3288b9d17663c0fbc42a2d6d9609659544d019ac42ca129e602ad018804d19a8
3
+ size 128778411