Maulidaaa commited on
Commit
7b65de5
·
verified ·
1 Parent(s): c447c97

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +171 -164
app.py CHANGED
@@ -13,34 +13,74 @@ import requests
13
  from dotenv import load_dotenv
14
  from werkzeug.utils import secure_filename
15
 
16
- # Load environment variables
 
 
17
  load_dotenv()
18
  OPENWEATHER_API_KEY = os.getenv("OPENWEATHER_API_KEY")
19
 
20
- # Inisialisasi Flask
21
  app = Flask(__name__)
22
- UPLOAD_FOLDER = '/tmp/uploads'
23
  os.makedirs(UPLOAD_FOLDER, exist_ok=True)
24
 
25
- # Label klasifikasi tanah
26
- class_labels = ['Alluvial Soil', 'Black Soil', 'Clay Soil', 'Non Soil', 'Red Soil']
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
  num_classes = len(class_labels)
28
 
29
- # Load model ViT (PyTorch)
30
- soil_model = timm.create_model("vit_base_patch16_224", pretrained=False, num_classes=num_classes)
31
- soil_model.head = nn.Linear(soil_model.head.in_features, num_classes)
32
- soil_model.load_state_dict(torch.load("models/best_vit_model.pth", map_location=torch.device("cpu")))
33
- soil_model.eval()
34
-
35
- # Load model crop recommendation
36
- crop_model = joblib.load("models/model_random_forest.joblib")
37
- crop_model_label = joblib.load("models/label_encoder.joblib")
38
-
39
- # Load data referensi
40
- soil_df = pd.read_csv("data/soil_data.csv")
41
- agri_df = pd.read_csv("data/tips_menanam_dan_manfaat_tanaman.csv")
42
-
43
- # Fungsi preprocessing gambar (PyTorch style)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
  def preprocess_image(img_path):
45
  transform = transforms.Compose([
46
  transforms.Resize((224, 224)),
@@ -50,197 +90,164 @@ def preprocess_image(img_path):
50
  img = Image.open(img_path).convert("RGB")
51
  return transform(img).unsqueeze(0)
52
 
53
- # Fungsi prediksi jenis tanah
 
 
54
  def predict_soil_type(img_path):
 
55
  img_tensor = preprocess_image(img_path)
56
  with torch.no_grad():
57
- outputs = soil_model(img_tensor)
58
  probabilities = torch.softmax(outputs, dim=1).numpy()[0]
59
- predicted_index = np.argmax(probabilities)
60
- predicted_class = class_labels[predicted_index]
61
- accuracy = float(probabilities[predicted_index])
62
- return predicted_class, accuracy
63
-
64
- # Hitung jarak dengan haversine
65
- def find_nearest_soil_data_weighted(soil_type, lat, lon, n_points=1):
66
- filtered = soil_df[soil_df['Soil_Type'] == soil_type].copy()
 
67
  if filtered.empty:
68
  return None
69
 
70
  user_lat, user_lon = radians(lat), radians(lon)
71
 
72
  def haversine(row):
73
- lat2, lon2 = radians(row['Location_Latitude']), radians(row['Location_Longitude'])
74
- dlat = lat2 - user_lat
75
- dlon = lon2 - user_lon
76
- a = sin(dlat / 2)**2 + cos(user_lat) * cos(lat2) * sin(dlon / 2)**2
77
- c = 2 * atan2(sqrt(a), sqrt(1 - a))
78
  return 6371 * c
79
 
80
- filtered['Distance_km'] = filtered.apply(haversine, axis=1)
81
- nearest = filtered.nsmallest(n_points, 'Distance_km').copy()
82
 
83
- row = nearest.iloc[0]
84
  return {
85
- "latitude": float(row['Location_Latitude']),
86
- "longitude": float(row['Location_Longitude']),
87
- "pH": float(row['pH']),
88
- "N": float(row['Nitrogen_N_ppm']),
89
- "P": float(row['Phosphorus_P_ppm']),
90
- "K": float(row['Potassium_K_ppm']),
91
- "distance_km": float(row['Distance_km'])
92
  }
93
 
94
- # Ambil data cuaca dari OpenWeather
 
 
95
  def get_weather_data(lat, lon):
96
- url = f"https://api.openweathermap.org/data/2.5/weather?lat={lat}&lon={lon}&appid={OPENWEATHER_API_KEY}&units=metric"
97
- res = requests.get(url)
 
 
 
98
  if res.status_code != 200:
99
  return None
100
  data = res.json()
101
  return {
102
- "temperature": float(data['main']['temp']),
103
- "humidity": float(data['main']['humidity']),
104
  }
105
 
106
- # Ambil tips bertani dan manfaat tanaman
107
- def get_farming_tips(df, crop_name):
108
- crop_name_str = str(crop_name)
109
- match = df[df['Nama Tanaman'].str.lower() == crop_name_str.lower()]
110
- if not match.empty:
111
- row = match.iloc[0]
112
- return {
113
- "id": int(row['ID']) if 'ID' in row else None,
114
- "Nama Tanaman": row['Nama Tanaman'],
115
- "Tips Menanam": row.get('Tips Menanam', 'Tidak tersedia'),
116
- "Manfaat": row.get('Manfaat', 'Tidak tersedia')
117
- }
118
- return None
119
 
120
- # Reverse geocoding
121
  def get_location_name(lat, lon):
122
  try:
123
  url = f"https://nominatim.openstreetmap.org/reverse?lat={lat}&lon={lon}&format=json"
124
  headers = {"User-Agent": "soil-api/1.0"}
125
- response = requests.get(url, headers=headers, timeout=10)
126
- if response.status_code == 200:
127
- data = response.json()
128
- return data.get("display_name", "Tidak ditemukan")
129
- else:
130
- return "Tidak ditemukan"
131
  except Exception:
132
- return "Tidak ditemukan"
 
133
 
134
- # Halaman landing
135
- @app.route('/')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
136
  def index():
137
- html_content = """
138
- <!DOCTYPE html>
139
- <html>
140
- <head>
141
- <title>Soil & Crop Recommender</title>
142
- </head>
143
- <body style="font-family: Arial; text-align: center; padding: 50px;">
144
- <h1>Welcome to the Soil & Crop Recommendation API 🌱</h1>
145
- <p>Use the <code>/analyze</code> endpoint with an image and coordinates to get soil predictions and crop suggestions.</p>
146
- <p><strong>POST /analyze</strong> with form-data:</p>
147
- <ul style="list-style: none;">
148
- <li><code>image</code>: soil image file</li>
149
- <li><code>lat</code>: latitude</li>
150
- <li><code>lon</code>: longitude</li>
151
- </ul>
152
- <p>Happy Farming! 🌾</p>
153
- </body>
154
- </html>
155
- """
156
- return render_template_string(html_content)
157
-
158
- # Endpoint analisis
159
- @app.route('/analyze', methods=['POST'])
160
  def analyze():
161
- if 'image' not in request.files:
162
  return jsonify({"error": "Gambar tidak ditemukan"}), 400
163
 
164
- image_file = request.files['image']
165
- if image_file.filename == '':
166
- return jsonify({"error": "Nama file gambar kosong"}), 400
167
-
168
  try:
169
- lat = float(request.form.get('lat'))
170
- lon = float(request.form.get('lon'))
171
- if not (-90 <= lat <= 90 and -180 <= lon <= 180):
172
- raise ValueError("Out of bounds")
173
  except:
174
  return jsonify({"error": "Koordinat tidak valid"}), 400
175
 
 
176
  filename = secure_filename(image_file.filename)
177
  image_path = os.path.join(UPLOAD_FOLDER, filename)
178
  image_file.save(image_path)
179
 
180
- predicted_soil, soil_accuracy = predict_soil_type(image_path)
181
- nearest_data = find_nearest_soil_data_weighted(predicted_soil, lat, lon)
182
- if nearest_data is None:
183
- return jsonify({"error": "Data jenis tanah tidak ditemukan"}), 404
184
 
185
- location_name = get_location_name(nearest_data['latitude'], nearest_data['longitude'])
186
  weather = get_weather_data(lat, lon)
187
  if not weather:
188
- return jsonify({"error": "Gagal mengambil data cuaca"}), 500
189
-
190
- input_data = pd.DataFrame([{
191
- 'temperature': weather['temperature'],
192
- 'humidity': weather['humidity'],
193
- 'ph': nearest_data['pH'],
194
- 'N': nearest_data['N'],
195
- 'P': nearest_data['P'],
196
- 'K': nearest_data['K'],
 
197
  }])
198
 
199
- if hasattr(crop_model, "predict_proba"):
200
- proba = crop_model.predict_proba(input_data)[0]
201
- top_idx = np.argsort(proba)[::-1][:5]
202
- recommended_crops = [
203
- str(crop_model_label.inverse_transform([crop_model.classes_[i]])[0])
204
- if hasattr(crop_model_label, "inverse_transform") else str(crop_model.classes_[i])
205
- for i in top_idx
206
- ]
207
- crop_percentages = [round(float(proba[i]) * 100, 2) for i in top_idx]
208
- else:
209
- pred = crop_model.predict(input_data)[0]
210
- recommended_crops = [str(crop_model_label.inverse_transform([pred])[0])] if hasattr(crop_model_label, "inverse_transform") else [str(pred)]
211
- crop_percentages = [100.0]
212
-
213
- tips_list = []
214
- for crop in recommended_crops:
215
- tips = get_farming_tips(agri_df, crop)
216
- tips_list.append(tips if tips else {"Tanaman": crop, "Pesan": "Data tidak tersedia"})
217
-
218
- result = {
219
- "Class_Name": predicted_soil,
220
- "soil_prediction_accuracy": round(soil_accuracy * 100, 2),
221
- "nearest_soil_data": {
222
- "latitude": nearest_data['latitude'],
223
- "longitude": nearest_data['longitude'],
224
- "location_name": location_name,
225
- "pH": nearest_data['pH'],
226
- "N": nearest_data['N'],
227
- "P": nearest_data['P'],
228
- "K": nearest_data['K'],
229
- "distance_km": round(nearest_data['distance_km'], 2)
230
- },
231
  "weather": weather,
232
  "recommended_crops": [
233
- {
234
- "crop": crop,
235
- "recommendation_percentage": percent
236
- }
237
- for crop, percent in zip(recommended_crops, crop_percentages)
238
  ],
239
- "farming_tips": tips_list
240
- }
241
-
242
- return jsonify(result)
243
-
244
- # Jalankan server
245
- if __name__ == '__main__':
246
- app.run(debug=True)
 
13
  from dotenv import load_dotenv
14
  from werkzeug.utils import secure_filename
15
 
16
+ # =========================================================
17
+ # ENV & APP CONFIG
18
+ # =========================================================
19
  load_dotenv()
20
  OPENWEATHER_API_KEY = os.getenv("OPENWEATHER_API_KEY")
21
 
 
22
  app = Flask(__name__)
23
+ UPLOAD_FOLDER = "/tmp/uploads"
24
  os.makedirs(UPLOAD_FOLDER, exist_ok=True)
25
 
26
+ # =========================================================
27
+ # GLOBAL PLACEHOLDERS (LAZY LOADING)
28
+ # =========================================================
29
+ soil_model = None
30
+ crop_model = None
31
+ crop_model_label = None
32
+ soil_df = None
33
+ agri_df = None
34
+
35
+ class_labels = [
36
+ "Alluvial Soil",
37
+ "Black Soil",
38
+ "Clay Soil",
39
+ "Non Soil",
40
+ "Red Soil"
41
+ ]
42
  num_classes = len(class_labels)
43
 
44
+ # =========================================================
45
+ # MODEL & DATA LOADERS (LAZY)
46
+ # =========================================================
47
+ def load_soil_model():
48
+ global soil_model
49
+ if soil_model is None:
50
+ model = timm.create_model(
51
+ "vit_base_patch16_224",
52
+ pretrained=False,
53
+ num_classes=num_classes
54
+ )
55
+ model.head = nn.Linear(model.head.in_features, num_classes)
56
+ state = torch.load(
57
+ "models/best_vit_model.pth",
58
+ map_location=torch.device("cpu")
59
+ )
60
+ model.load_state_dict(state)
61
+ model.eval()
62
+ soil_model = model
63
+ return soil_model
64
+
65
+
66
+ def load_crop_model():
67
+ global crop_model, crop_model_label
68
+ if crop_model is None:
69
+ crop_model = joblib.load("models/model_random_forest.joblib")
70
+ crop_model_label = joblib.load("models/label_encoder.joblib")
71
+ return crop_model, crop_model_label
72
+
73
+
74
+ def load_dataframes():
75
+ global soil_df, agri_df
76
+ if soil_df is None:
77
+ soil_df = pd.read_csv("data/soil_data.csv")
78
+ if agri_df is None:
79
+ agri_df = pd.read_csv("data/tips_menanam_dan_manfaat_tanaman.csv")
80
+
81
+ # =========================================================
82
+ # IMAGE PREPROCESS
83
+ # =========================================================
84
  def preprocess_image(img_path):
85
  transform = transforms.Compose([
86
  transforms.Resize((224, 224)),
 
90
  img = Image.open(img_path).convert("RGB")
91
  return transform(img).unsqueeze(0)
92
 
93
+ # =========================================================
94
+ # SOIL PREDICTION
95
+ # =========================================================
96
  def predict_soil_type(img_path):
97
+ model = load_soil_model()
98
  img_tensor = preprocess_image(img_path)
99
  with torch.no_grad():
100
+ outputs = model(img_tensor)
101
  probabilities = torch.softmax(outputs, dim=1).numpy()[0]
102
+ idx = int(np.argmax(probabilities))
103
+ return class_labels[idx], float(probabilities[idx])
104
+
105
+ # =========================================================
106
+ # HAVERSINE & SOIL DATA
107
+ # =========================================================
108
+ def find_nearest_soil_data_weighted(soil_type, lat, lon):
109
+ load_dataframes()
110
+ filtered = soil_df[soil_df["Soil_Type"] == soil_type].copy()
111
  if filtered.empty:
112
  return None
113
 
114
  user_lat, user_lon = radians(lat), radians(lon)
115
 
116
  def haversine(row):
117
+ lat2, lon2 = radians(row["Location_Latitude"]), radians(row["Location_Longitude"])
118
+ dlat, dlon = lat2 - user_lat, lon2 - user_lon
119
+ a = sin(dlat/2)**2 + cos(user_lat)*cos(lat2)*sin(dlon/2)**2
120
+ c = 2 * atan2(sqrt(a), sqrt(1-a))
 
121
  return 6371 * c
122
 
123
+ filtered["Distance_km"] = filtered.apply(haversine, axis=1)
124
+ row = filtered.nsmallest(1, "Distance_km").iloc[0]
125
 
 
126
  return {
127
+ "latitude": float(row["Location_Latitude"]),
128
+ "longitude": float(row["Location_Longitude"]),
129
+ "pH": float(row["pH"]),
130
+ "N": float(row["Nitrogen_N_ppm"]),
131
+ "P": float(row["Phosphorus_P_ppm"]),
132
+ "K": float(row["Potassium_K_ppm"]),
133
+ "distance_km": float(row["Distance_km"])
134
  }
135
 
136
+ # =========================================================
137
+ # WEATHER & GEO
138
+ # =========================================================
139
  def get_weather_data(lat, lon):
140
+ url = (
141
+ f"https://api.openweathermap.org/data/2.5/weather"
142
+ f"?lat={lat}&lon={lon}&appid={OPENWEATHER_API_KEY}&units=metric"
143
+ )
144
+ res = requests.get(url, timeout=10)
145
  if res.status_code != 200:
146
  return None
147
  data = res.json()
148
  return {
149
+ "temperature": float(data["main"]["temp"]),
150
+ "humidity": float(data["main"]["humidity"])
151
  }
152
 
 
 
 
 
 
 
 
 
 
 
 
 
 
153
 
 
154
  def get_location_name(lat, lon):
155
  try:
156
  url = f"https://nominatim.openstreetmap.org/reverse?lat={lat}&lon={lon}&format=json"
157
  headers = {"User-Agent": "soil-api/1.0"}
158
+ res = requests.get(url, headers=headers, timeout=10)
159
+ if res.status_code == 200:
160
+ return res.json().get("display_name", "Tidak ditemukan")
 
 
 
161
  except Exception:
162
+ pass
163
+ return "Tidak ditemukan"
164
 
165
+ # =========================================================
166
+ # FARMING TIPS
167
+ # =========================================================
168
+ def get_farming_tips(df, crop_name):
169
+ match = df[df["Nama Tanaman"].str.lower() == str(crop_name).lower()]
170
+ if not match.empty:
171
+ row = match.iloc[0]
172
+ return {
173
+ "Nama Tanaman": row["Nama Tanaman"],
174
+ "Tips Menanam": row.get("Tips Menanam", "Tidak tersedia"),
175
+ "Manfaat": row.get("Manfaat", "Tidak tersedia")
176
+ }
177
+ return {"Tanaman": crop_name, "Pesan": "Data tidak tersedia"}
178
+
179
+ # =========================================================
180
+ # ROUTES
181
+ # =========================================================
182
+ @app.route("/")
183
  def index():
184
+ return render_template_string("""
185
+ <h1>🌱 Soil & Crop Recommendation API</h1>
186
+ <p>POST <code>/analyze</code> with image, lat, lon</p>
187
+ """)
188
+
189
+
190
+ @app.route("/analyze", methods=["POST"])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
191
  def analyze():
192
+ if "image" not in request.files:
193
  return jsonify({"error": "Gambar tidak ditemukan"}), 400
194
 
 
 
 
 
195
  try:
196
+ lat = float(request.form.get("lat"))
197
+ lon = float(request.form.get("lon"))
 
 
198
  except:
199
  return jsonify({"error": "Koordinat tidak valid"}), 400
200
 
201
+ image_file = request.files["image"]
202
  filename = secure_filename(image_file.filename)
203
  image_path = os.path.join(UPLOAD_FOLDER, filename)
204
  image_file.save(image_path)
205
 
206
+ soil_type, soil_acc = predict_soil_type(image_path)
207
+ nearest = find_nearest_soil_data_weighted(soil_type, lat, lon)
208
+ if not nearest:
209
+ return jsonify({"error": "Data tanah tidak ditemukan"}), 404
210
 
 
211
  weather = get_weather_data(lat, lon)
212
  if not weather:
213
+ return jsonify({"error": "Gagal mengambil cuaca"}), 500
214
+
215
+ crop_model, crop_label = load_crop_model()
216
+ input_df = pd.DataFrame([{
217
+ "temperature": weather["temperature"],
218
+ "humidity": weather["humidity"],
219
+ "ph": nearest["pH"],
220
+ "N": nearest["N"],
221
+ "P": nearest["P"],
222
+ "K": nearest["K"]
223
  }])
224
 
225
+ proba = crop_model.predict_proba(input_df)[0]
226
+ top_idx = np.argsort(proba)[::-1][:5]
227
+
228
+ load_dataframes()
229
+ crops = [
230
+ crop_label.inverse_transform([crop_model.classes_[i]])[0]
231
+ for i in top_idx
232
+ ]
233
+ percentages = [round(float(proba[i]) * 100, 2) for i in top_idx]
234
+
235
+ tips = [get_farming_tips(agri_df, c) for c in crops]
236
+
237
+ return jsonify({
238
+ "soil_type": soil_type,
239
+ "soil_accuracy": round(soil_acc * 100, 2),
240
+ "location": get_location_name(nearest["latitude"], nearest["longitude"]),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
241
  "weather": weather,
242
  "recommended_crops": [
243
+ {"crop": c, "percentage": p}
244
+ for c, p in zip(crops, percentages)
 
 
 
245
  ],
246
+ "farming_tips": tips
247
+ })
248
+
249
+ # =========================================================
250
+ # RUN SERVER (HF SPACES)
251
+ # =========================================================
252
+ if __name__ == "__main__":
253
+ app.run(host="0.0.0.0", port=7860)