agentsay commited on
Commit
1c63d8d
·
verified ·
1 Parent(s): d06b225

Update engine.py

Browse files
Files changed (1) hide show
  1. engine.py +428 -427
engine.py CHANGED
@@ -1,427 +1,428 @@
1
- import os
2
- from PIL import Image
3
- import piexif
4
- import cv2
5
- import numpy as np
6
- from geopy.geocoders import Nominatim
7
- from geopy.exc import GeocoderTimedOut
8
- import torch
9
- import timm
10
- from torchvision import transforms
11
- import torch.nn.functional as F
12
- import pandas as pd
13
- import re
14
- from prophet import Prophet
15
- from sklearn.metrics import mean_absolute_error, mean_absolute_percentage_error
16
- import requests
17
- import json
18
- import config
19
- # Load environment variables from .env file
20
- try:
21
- from dotenv import load_dotenv
22
- load_dotenv()
23
- except ImportError:
24
- print("Warning: python-dotenv not installed. Using system environment variables only.")
25
-
26
- # --- EXIF Metadata Extraction ---
27
- def get_exif_data(image_path):
28
- if not os.path.exists(image_path):
29
- return {"error": f"File not found at path {image_path}"}
30
-
31
- suspicious_reasons = []
32
- authenticity_score = 100
33
-
34
- try:
35
- exif_dict = piexif.load(image_path)
36
- gps_info = exif_dict.get('GPS', {})
37
-
38
- def _convert_to_degrees(value):
39
- d, m, s = value
40
- return d[0]/d[1] + (m[0]/m[1])/60 + (s[0]/s[1])/3600
41
-
42
- lat = lon = None
43
- if gps_info:
44
- try:
45
- lat = round(_convert_to_degrees(gps_info[2]), 6)
46
- lon = round(_convert_to_degrees(gps_info[4]), 6)
47
- if gps_info[1] == b'S': lat *= -1
48
- if gps_info[3] == b'W': lon *= -1
49
- except:
50
- lat, lon = None, None
51
- suspicious_reasons.append("GPS data could not be parsed correctly.")
52
- else:
53
- suspicious_reasons.append("GPS metadata missing.")
54
- authenticity_score -= 30
55
-
56
- address = None
57
- if lat and lon:
58
- try:
59
- geolocator = Nominatim(user_agent="agrisure_exif_reader")
60
- location = geolocator.reverse((lat, lon))
61
- address = location.address if location else None # type: ignore
62
- except:
63
- address = "Geocoder error"
64
-
65
- model = exif_dict['0th'].get(piexif.ImageIFD.Model, b"").decode('utf-8', errors='ignore')
66
- timestamp = exif_dict['Exif'].get(piexif.ExifIFD.DateTimeOriginal, b"").decode('utf-8', errors='ignore')
67
- software = exif_dict['0th'].get(piexif.ImageIFD.Software, b"").decode('utf-8', errors='ignore')
68
-
69
- if not model:
70
- suspicious_reasons.append("Device model missing.")
71
- authenticity_score -= 10
72
- if not timestamp:
73
- suspicious_reasons.append("Timestamp missing.")
74
- authenticity_score -= 20
75
- if software:
76
- suspicious_reasons.append(f"Image was edited using software: {software}")
77
- authenticity_score -= 25
78
-
79
- try:
80
- ela_path = image_path.replace(".jpg", "_ela.jpg")
81
- original = Image.open(image_path).convert('RGB')
82
- original.save(ela_path, 'JPEG', quality=90)
83
- ela_image = Image.open(ela_path)
84
- ela = Image.blend(original, ela_image, alpha=10)
85
- ela_cv = np.array(ela)
86
- std_dev = np.std(ela_cv)
87
- if std_dev > 25:
88
- suspicious_reasons.append("High ELA deviation — possible image tampering.")
89
- authenticity_score -= 15
90
- os.remove(ela_path)
91
- except:
92
- suspicious_reasons.append("ELA check failed.")
93
- authenticity_score -= 5
94
-
95
- return {
96
- "verifier": "exif_metadata_reader",
97
- "device_model": model or "N/A",
98
- "timestamp": timestamp or "N/A",
99
- "gps_latitude": lat,
100
- "gps_longitude": lon,
101
- "address": address,
102
- "authenticity_score": max(0, authenticity_score),
103
- "suspicious_reasons": suspicious_reasons or ["None"]
104
- }
105
- except Exception as e:
106
- return {"error": f"Failed to analyze image: {str(e)}"}
107
-
108
- # --- Crop Damage Detection ---
109
- device = "cuda" if torch.cuda.is_available() else "cpu"
110
- val_transform = transforms.Compose([
111
- transforms.Resize((384, 384)),
112
- transforms.ToTensor(),
113
- transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
114
- ])
115
- model_damage = timm.create_model('efficientnetv2_rw_m', pretrained=False, num_classes=2)
116
- model_damage.load_state_dict(torch.load("models/efficientnetv2_rw_m_crop_damage.pt", map_location=device))
117
- model_damage.to(device)
118
- model_damage.eval()
119
- class_names = ['damaged', 'non_damaged']
120
-
121
- def predict_damage(image_path):
122
- if not os.path.exists(image_path):
123
- return {"status": "error", "message": f"File not found: {image_path}"}
124
-
125
- try:
126
- image = Image.open(image_path).convert('RGB')
127
- input_tensor = val_transform(image).unsqueeze(0).to(device)
128
- with torch.no_grad():
129
- output = model_damage(input_tensor)
130
- probs = torch.softmax(output, dim=1)
131
- predicted_class = int(torch.argmax(probs, dim=1).item())
132
- confidence = float(probs[0][predicted_class].item())
133
- predicted_label = class_names[predicted_class]
134
- return {
135
- "verifier": "crop_damage_classifier",
136
- "model": "efficientnetv2_rw_m",
137
- "prediction": predicted_label,
138
- "confidence": round(confidence * 100, 2),
139
- "class_names": class_names,
140
- "status": "success"
141
- }
142
- except Exception as e:
143
- return {"status": "error", "message": str(e)}
144
-
145
- # --- Crop Type Detection ---
146
- val_transforms_crop = transforms.Compose([
147
- transforms.Resize((224, 224)),
148
- transforms.ToTensor(),
149
- transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
150
- ])
151
- idx_to_class = {
152
- 0: 'Coffee-plant', 1: 'Cucumber', 2: 'Fox_nut(Makhana)', 3: 'Lemon', 4: 'Olive-tree',
153
- 5: 'Pearl_millet(bajra)', 6: 'Tobacco-plant', 7: 'almond', 8: 'banana', 9: 'cardamom',
154
- 10: 'cherry', 11: 'chilli', 12: 'clove', 13: 'coconut', 14: 'cotton', 15: 'gram',
155
- 16: 'jowar', 17: 'jute', 18: 'maize', 19: 'mustard-oil', 20: 'papaya', 21: 'pineapple',
156
- 22: 'rice', 23: 'soyabean', 24: 'sugarcane', 25: 'sunflower', 26: 'tea', 27: 'tomato',
157
- 28: 'vigna-radiati(Mung)', 29: 'wheat'
158
- }
159
- model_crop = timm.create_model('convnext_tiny', pretrained=False, num_classes=30)
160
- model_crop.load_state_dict(torch.load('models/crop_type_detection_model.pth', map_location=device))
161
- model_crop.to(device)
162
- model_crop.eval()
163
-
164
- def predict_crop(image_path):
165
- if not os.path.exists(image_path):
166
- return {"status": "error", "message": f"File not found: {image_path}"}
167
-
168
- try:
169
- image = Image.open(image_path).convert('RGB')
170
- image_tensor = val_transforms_crop(image).unsqueeze(0).to(device)
171
- with torch.no_grad():
172
- outputs = model_crop(image_tensor)
173
- probs = F.softmax(outputs, dim=1)
174
- conf, pred = torch.max(probs, 1)
175
- predicted_label = idx_to_class[pred.item()]
176
- confidence = round(float(conf.item()) * 100, 2)
177
- return {
178
- "status": "success",
179
- "predicted_class": predicted_label,
180
- "confidence_percent": confidence
181
- }
182
- except Exception as e:
183
- return {"status": "error", "message": str(e)}
184
-
185
- # --- Crop Yield Prediction Utilities ---
186
- def get_district_from_coordinates(lat, lon):
187
- geolocator = Nominatim(user_agent="agrisure-ai")
188
- try:
189
- location = geolocator.reverse((lat, lon))
190
- except GeocoderTimedOut:
191
- return None, None, "Reverse geocoding service timed out."
192
- except Exception as e:
193
- return None, None, f"Geocoding error: {str(e)}"
194
-
195
- if not location:
196
- return None, None, "Could not get district from coordinates."
197
-
198
- try:
199
- address = location.raw.get('address', {}) # type: ignore
200
- except (AttributeError, TypeError):
201
- return None, None, "Could not parse location data."
202
-
203
- if not address:
204
- return None, None, "Could not get district from coordinates."
205
- district = (
206
- address.get('district') or
207
- address.get('state_district') or
208
- address.get('county')
209
- )
210
- if not district:
211
- return None, None, "District not found in address data."
212
- if 'district' in district.lower():
213
- district = district.replace("District", "").strip()
214
- place_name = district # Set place_name to district name
215
- return district, place_name, None
216
-
217
- def clean_district_name(district):
218
- if not isinstance(district, str):
219
- return district
220
- district = re.sub(r"\s*[-\u2013]\s*(I{1,3}|IV|V|VI|VII|VIII|IX|X|\d+)$", "", district, flags=re.IGNORECASE)
221
- district = district.replace("District", "").strip()
222
- aliases = {
223
- "Purba Bardhaman": "Burdwan",
224
- "Paschim Bardhaman": "Burdwan",
225
- "Bardhaman": "Burdwan",
226
- "Kalna": "Burdwan",
227
- "Kalyani": "Nadia",
228
- "Raiganj": "Uttar Dinajpur",
229
- "Kolkata": "North 24 Parganas"
230
- }
231
- return aliases.get(district, district)
232
-
233
- def get_soil_category(score):
234
- if score == 0:
235
- return "No Soil Health Data"
236
- elif score >= 4.5:
237
- return "Very Excellent Soil Health"
238
- elif score >= 4:
239
- return "Excellent Soil Health"
240
- elif score >= 3:
241
- return "Good Soil Health"
242
- elif score >= 2:
243
- return "Poor Soil Health"
244
- else:
245
- return "Very Poor Soil Health"
246
-
247
- def calculate_dynamic_climate_score(predicted_yield, soil_score, max_yield=8000, max_soil=5.0):
248
- norm_yield = (predicted_yield / max_yield) ** 0.8
249
- norm_soil = (soil_score / max_soil) ** 1.2
250
- return round((0.6 * norm_yield + 0.4 * norm_soil) * 100, 2)
251
-
252
- def forecast_yield(ts_data):
253
- model = Prophet(yearly_seasonality='auto', growth='flat')
254
- model.fit(ts_data)
255
- forecast = model.predict(model.make_future_dataframe(periods=1, freq='YS'))
256
- return max(forecast.iloc[-1]['yhat'], 0)
257
-
258
- def forecast_yield_with_accuracy(ts_data):
259
- model = Prophet(yearly_seasonality='auto', growth='flat')
260
- model.fit(ts_data)
261
- future = model.make_future_dataframe(periods=1, freq='YS')
262
- forecast = model.predict(future)
263
- predicted_yield = max(forecast.iloc[-1]['yhat'], 0)
264
-
265
- try:
266
- past = forecast[forecast['ds'] < ts_data['ds'].max()]
267
- merged = ts_data.merge(past[['ds', 'yhat']], on='ds')
268
- mae = mean_absolute_error(merged['y'], merged['yhat'])
269
- mape = mean_absolute_percentage_error(merged['y'], merged['yhat']) * 100
270
- except:
271
- mae, mape = None, None
272
-
273
- return predicted_yield, mae, mape
274
-
275
- def get_crop_priority_list(district_yield, base_crop_names):
276
- priority_list = []
277
- for crop, column in base_crop_names.items():
278
- crop_data = district_yield[['Year', column]].dropna()
279
- crop_data.columns = ['ds', 'y']
280
- crop_data['ds'] = pd.to_datetime(crop_data['ds'], format='%Y')
281
- if len(crop_data) >= 5:
282
- yield_pred = forecast_yield(crop_data)
283
- priority_list.append((crop, yield_pred))
284
- return sorted(priority_list, key=lambda x: x[1], reverse=True)
285
-
286
- def get_weather_data(lat, lon):
287
- try:
288
- # Get weather API key from environment variables
289
- weather_api_key = config.OPENWEATHER_API
290
- if weather_api_key and weather_api_key != "your_openweather_api_key_here":
291
- url = f"https://api.weatherapi.com/v1/current.json?key={weather_api_key}&q={lat},{lon}"
292
- response = requests.get(url)
293
- data = response.json()
294
- return {
295
- "temp_c": data['current']['temp_c'],
296
- "humidity": data['current']['humidity'],
297
- "condition": data['current']['condition']['text'],
298
- "wind_kph": data['current']['wind_kph']
299
- }
300
- else:
301
- return {"error": "Weather API key not configured or placeholder value"}
302
- except Exception as e:
303
- return {"error": "Weather fetch failed", "details": str(e)}
304
-
305
- def predict_crop_yield_from_location(crop_input, lat, lon):
306
- district, place_name, error = get_district_from_coordinates(lat, lon)
307
- if error:
308
- return {"error": error}
309
-
310
- if district is None:
311
- return {"error": "Could not determine district from coordinates"}
312
-
313
- district_input = clean_district_name(district)
314
-
315
- try:
316
- yield_df = pd.read_csv(r"data\ICRISAT-District_Level_Data_30_Years.csv")
317
- soil_df = pd.read_csv(r"data\SoilHealthScores_by_District_2.csv")
318
- except Exception as e:
319
- return {"error": f"Failed to read data files: {str(e)}"}
320
-
321
- soil_df['Soil_Category'] = soil_df['SoilHealthScore'].apply(get_soil_category)
322
- yield_columns = [col for col in yield_df.columns if 'YIELD (Kg per ha)' in col]
323
- base_crop_names = {col.split(' YIELD')[0]: col for col in yield_columns}
324
-
325
- if crop_input not in base_crop_names:
326
- return {"error": f"'{crop_input}' not found in crop list."}
327
-
328
- yield_col = base_crop_names[crop_input]
329
-
330
- # Ensure district_input is not None before using lower()
331
- if district_input is None:
332
- return {"error": "Could not determine district name"}
333
-
334
- district_yield = yield_df[yield_df['Dist Name'].str.lower() == district_input.lower()]
335
- district_soil = soil_df[soil_df['Dist Name'].str.lower() == district_input.lower()]
336
-
337
- if district_yield.empty or district_soil.empty:
338
- return {"error": f"Data for district '{district_input}' not found."}
339
-
340
- ts_data = district_yield[['Year', yield_col]].dropna()
341
- ts_data.columns = ['ds', 'y']
342
- ts_data['ds'] = pd.to_datetime(ts_data['ds'], format='%Y')
343
- ts_data['year'] = ts_data['ds'].dt.year
344
-
345
- valid_data = ts_data[ts_data['y'] > 0]
346
- if len(valid_data) < 6:
347
- predicted_yield = ts_data['y'].mean()
348
- mae, mape = None, None
349
- else:
350
- predicted_yield, mae, mape = forecast_yield_with_accuracy(valid_data)
351
-
352
- if predicted_yield > 1000:
353
- yield_cat = "Highly Recommended Crop"
354
- elif predicted_yield > 500:
355
- yield_cat = "Good Crop"
356
- elif predicted_yield > 200:
357
- yield_cat = "Poor Crop"
358
- else:
359
- yield_cat = "Very Poor Crop"
360
-
361
- soil_score = district_soil['SoilHealthScore'].values[0]
362
- soil_cat = district_soil['Soil_Category'].values[0]
363
- climate_score = calculate_dynamic_climate_score(predicted_yield, soil_score)
364
-
365
- sorted_crops = get_crop_priority_list(district_yield, base_crop_names)
366
- best_crop = sorted_crops[0][0] if sorted_crops else None
367
- best_yield = sorted_crops[0][1] if sorted_crops else None
368
-
369
- weather_data = get_weather_data(lat, lon)
370
-
371
- crop_priority_list = []
372
- for c, y in sorted_crops:
373
- if y > 1000:
374
- yc = "Highly Recommended Crop"
375
- elif y > 500:
376
- yc = "Good Crop"
377
- elif y > 200:
378
- yc = "Poor Crop"
379
- else:
380
- yc = "Very Poor Crop"
381
-
382
- score = calculate_dynamic_climate_score(y, soil_score)
383
-
384
- crop_priority_list.append({
385
- "crop": c,
386
- "predicted_yield": {
387
- "kg_per_ha": round(y, 2),
388
- "kg_per_acre": round(y / 2.47105, 2)
389
- },
390
- "yield_category": yc,
391
- "climate_score": score
392
- })
393
-
394
- return {
395
- "location": {
396
- "input_coordinates": {"lat": lat, "lon": lon},
397
- "place_name": place_name,
398
- "detected_district": district,
399
- },
400
- "input_crop_analysis": {
401
- "crop": crop_input,
402
- "predicted_yield": {
403
- "kg_per_ha": round(predicted_yield, 2),
404
- "kg_per_acre": round(predicted_yield / 2.47105, 2)
405
- },
406
- "yield_category": yield_cat,
407
- "prediction_accuracy": {
408
- "mae": round(mae, 2) if mae is not None else "Not enough data",
409
- "mape_percent": round(mape, 2) if mape is not None else "Not enough data",
410
- "accuracy_score": round(100 - mape, 2) if mape is not None else "Not enough data"
411
- }
412
- },
413
- "soil_health": {
414
- "score": soil_score,
415
- "category": soil_cat
416
- },
417
- "climate_score": climate_score,
418
- "weather_now": weather_data,
419
- "best_crop": {
420
- "name": best_crop,
421
- "predicted_yield": {
422
- "kg_per_ha": round(best_yield, 2) if best_crop and best_yield is not None else None,
423
- "kg_per_acre": round(best_yield / 2.47105, 2) if best_crop and best_yield is not None else None,
424
- }
425
- },
426
- "crop_priority_list": crop_priority_list
427
- }
 
 
1
+ import os
2
+ from PIL import Image
3
+ import piexif
4
+ import cv2
5
+ import numpy as np
6
+ from geopy.geocoders import Nominatim
7
+ from geopy.exc import GeocoderTimedOut
8
+ import torch
9
+ import timm
10
+ from torchvision import transforms
11
+ import torch.nn.functional as F
12
+ import pandas as pd
13
+ import re
14
+ from prophet import Prophet
15
+ from sklearn.metrics import mean_absolute_error, mean_absolute_percentage_error
16
+ import requests
17
+ import json
18
+ import config
19
+ # Load environment variables from .env file
20
+ try:
21
+ from dotenv import load_dotenv
22
+ load_dotenv()
23
+ except ImportError:
24
+ print("Warning: python-dotenv not installed. Using system environment variables only.")
25
+
26
+ # --- EXIF Metadata Extraction ---
27
+ def get_exif_data(image_path):
28
+ if not os.path.exists(image_path):
29
+ return {"error": f"File not found at path {image_path}"}
30
+
31
+ suspicious_reasons = []
32
+ authenticity_score = 100
33
+
34
+ try:
35
+ exif_dict = piexif.load(image_path)
36
+ gps_info = exif_dict.get('GPS', {})
37
+
38
+ def _convert_to_degrees(value):
39
+ d, m, s = value
40
+ return d[0]/d[1] + (m[0]/m[1])/60 + (s[0]/s[1])/3600
41
+
42
+ lat = lon = None
43
+ if gps_info:
44
+ try:
45
+ lat = round(_convert_to_degrees(gps_info[2]), 6)
46
+ lon = round(_convert_to_degrees(gps_info[4]), 6)
47
+ if gps_info[1] == b'S': lat *= -1
48
+ if gps_info[3] == b'W': lon *= -1
49
+ except:
50
+ lat, lon = None, None
51
+ suspicious_reasons.append("GPS data could not be parsed correctly.")
52
+ else:
53
+ suspicious_reasons.append("GPS metadata missing.")
54
+ authenticity_score -= 30
55
+
56
+ address = None
57
+ if lat and lon:
58
+ try:
59
+ geolocator = Nominatim(user_agent="agrisure_exif_reader")
60
+ location = geolocator.reverse((lat, lon))
61
+ address = location.address if location else None # type: ignore
62
+ except:
63
+ address = "Geocoder error"
64
+
65
+ model = exif_dict['0th'].get(piexif.ImageIFD.Model, b"").decode('utf-8', errors='ignore')
66
+ timestamp = exif_dict['Exif'].get(piexif.ExifIFD.DateTimeOriginal, b"").decode('utf-8', errors='ignore')
67
+ software = exif_dict['0th'].get(piexif.ImageIFD.Software, b"").decode('utf-8', errors='ignore')
68
+
69
+ if not model:
70
+ suspicious_reasons.append("Device model missing.")
71
+ authenticity_score -= 10
72
+ if not timestamp:
73
+ suspicious_reasons.append("Timestamp missing.")
74
+ authenticity_score -= 20
75
+ if software:
76
+ suspicious_reasons.append(f"Image was edited using software: {software}")
77
+ authenticity_score -= 25
78
+
79
+ try:
80
+ ela_path = image_path.replace(".jpg", "_ela.jpg")
81
+ original = Image.open(image_path).convert('RGB')
82
+ original.save(ela_path, 'JPEG', quality=90)
83
+ ela_image = Image.open(ela_path)
84
+ ela = Image.blend(original, ela_image, alpha=10)
85
+ ela_cv = np.array(ela)
86
+ std_dev = np.std(ela_cv)
87
+ if std_dev > 25:
88
+ suspicious_reasons.append("High ELA deviation — possible image tampering.")
89
+ authenticity_score -= 15
90
+ os.remove(ela_path)
91
+ except:
92
+ suspicious_reasons.append("ELA check failed.")
93
+ authenticity_score -= 5
94
+
95
+ return {
96
+ "verifier": "exif_metadata_reader",
97
+ "device_model": model or "N/A",
98
+ "timestamp": timestamp or "N/A",
99
+ "gps_latitude": lat,
100
+ "gps_longitude": lon,
101
+ "address": address,
102
+ "authenticity_score": max(0, authenticity_score),
103
+ "suspicious_reasons": suspicious_reasons or ["None"]
104
+ }
105
+ except Exception as e:
106
+ return {"error": f"Failed to analyze image: {str(e)}"}
107
+
108
+ # --- Crop Damage Detection ---
109
+ device = "cuda" if torch.cuda.is_available() else "cpu"
110
+ val_transform = transforms.Compose([
111
+ transforms.Resize((384, 384)),
112
+ transforms.ToTensor(),
113
+ transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
114
+ ])
115
+ model_damage = timm.create_model('efficientnetv2_rw_m', pretrained=False, num_classes=2)
116
+ model_damage.load_state_dict(torch.load("models/efficientnetv2_rw_m_crop_damage.pt", map_location=device))
117
+ model_damage.to(device)
118
+ model_damage.eval()
119
+ class_names = ['damaged', 'non_damaged']
120
+
121
+ def predict_damage(image_path):
122
+ if not os.path.exists(image_path):
123
+ return {"status": "error", "message": f"File not found: {image_path}"}
124
+
125
+ try:
126
+ image = Image.open(image_path).convert('RGB')
127
+ input_tensor = val_transform(image).unsqueeze(0).to(device)
128
+ with torch.no_grad():
129
+ output = model_damage(input_tensor)
130
+ probs = torch.softmax(output, dim=1)
131
+ predicted_class = int(torch.argmax(probs, dim=1).item())
132
+ confidence = float(probs[0][predicted_class].item())
133
+ predicted_label = class_names[predicted_class]
134
+ return {
135
+ "verifier": "crop_damage_classifier",
136
+ "model": "efficientnetv2_rw_m",
137
+ "prediction": predicted_label,
138
+ "confidence": round(confidence * 100, 2),
139
+ "class_names": class_names,
140
+ "status": "success"
141
+ }
142
+ except Exception as e:
143
+ return {"status": "error", "message": str(e)}
144
+
145
+ # --- Crop Type Detection ---
146
+ val_transforms_crop = transforms.Compose([
147
+ transforms.Resize((224, 224)),
148
+ transforms.ToTensor(),
149
+ transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
150
+ ])
151
+ idx_to_class = {
152
+ 0: 'Coffee-plant', 1: 'Cucumber', 2: 'Fox_nut(Makhana)', 3: 'Lemon', 4: 'Olive-tree',
153
+ 5: 'Pearl_millet(bajra)', 6: 'Tobacco-plant', 7: 'almond', 8: 'banana', 9: 'cardamom',
154
+ 10: 'cherry', 11: 'chilli', 12: 'clove', 13: 'coconut', 14: 'cotton', 15: 'gram',
155
+ 16: 'jowar', 17: 'jute', 18: 'maize', 19: 'mustard-oil', 20: 'papaya', 21: 'pineapple',
156
+ 22: 'rice', 23: 'soyabean', 24: 'sugarcane', 25: 'sunflower', 26: 'tea', 27: 'tomato',
157
+ 28: 'vigna-radiati(Mung)', 29: 'wheat'
158
+ }
159
+ model_crop = timm.create_model('convnext_tiny', pretrained=False, num_classes=30)
160
+ model_crop.load_state_dict(torch.load('models/crop_type_detection_model.pth', map_location=device))
161
+ model_crop.to(device)
162
+ model_crop.eval()
163
+
164
+ def predict_crop(image_path):
165
+ if not os.path.exists(image_path):
166
+ return {"status": "error", "message": f"File not found: {image_path}"}
167
+
168
+ try:
169
+ image = Image.open(image_path).convert('RGB')
170
+ image_tensor = val_transforms_crop(image).unsqueeze(0).to(device)
171
+ with torch.no_grad():
172
+ outputs = model_crop(image_tensor)
173
+ probs = F.softmax(outputs, dim=1)
174
+ conf, pred = torch.max(probs, 1)
175
+ predicted_label = idx_to_class[pred.item()]
176
+ confidence = round(float(conf.item()) * 100, 2)
177
+ return {
178
+ "status": "success",
179
+ "predicted_class": predicted_label,
180
+ "confidence_percent": confidence
181
+ }
182
+ except Exception as e:
183
+ return {"status": "error", "message": str(e)}
184
+
185
+ # --- Crop Yield Prediction Utilities ---
186
+ def get_district_from_coordinates(lat, lon):
187
+ geolocator = Nominatim(user_agent="agrisure-ai")
188
+ try:
189
+ location = geolocator.reverse((lat, lon))
190
+ except GeocoderTimedOut:
191
+ return None, None, "Reverse geocoding service timed out."
192
+ except Exception as e:
193
+ return None, None, f"Geocoding error: {str(e)}"
194
+
195
+ if not location:
196
+ return None, None, "Could not get district from coordinates."
197
+
198
+ try:
199
+ address = location.raw.get('address', {}) # type: ignore
200
+ except (AttributeError, TypeError):
201
+ return None, None, "Could not parse location data."
202
+
203
+ if not address:
204
+ return None, None, "Could not get district from coordinates."
205
+ district = (
206
+ address.get('district') or
207
+ address.get('state_district') or
208
+ address.get('county')
209
+ )
210
+ if not district:
211
+ return None, None, "District not found in address data."
212
+ if 'district' in district.lower():
213
+ district = district.replace("District", "").strip()
214
+ place_name = district # Set place_name to district name
215
+ return district, place_name, None
216
+
217
+ def clean_district_name(district):
218
+ if not isinstance(district, str):
219
+ return district
220
+ district = re.sub(r"\s*[-\u2013]\s*(I{1,3}|IV|V|VI|VII|VIII|IX|X|\d+)$", "", district, flags=re.IGNORECASE)
221
+ district = district.replace("District", "").strip()
222
+ aliases = {
223
+ "Purba Bardhaman": "Burdwan",
224
+ "Paschim Bardhaman": "Burdwan",
225
+ "Bardhaman": "Burdwan",
226
+ "Kalna": "Burdwan",
227
+ "Kalyani": "Nadia",
228
+ "Raiganj": "Uttar Dinajpur",
229
+ "Kolkata": "North 24 Parganas"
230
+ }
231
+ return aliases.get(district, district)
232
+
233
+ def get_soil_category(score):
234
+ if score == 0:
235
+ return "No Soil Health Data"
236
+ elif score >= 4.5:
237
+ return "Very Excellent Soil Health"
238
+ elif score >= 4:
239
+ return "Excellent Soil Health"
240
+ elif score >= 3:
241
+ return "Good Soil Health"
242
+ elif score >= 2:
243
+ return "Poor Soil Health"
244
+ else:
245
+ return "Very Poor Soil Health"
246
+
247
+ def calculate_dynamic_climate_score(predicted_yield, soil_score, max_yield=8000, max_soil=5.0):
248
+ norm_yield = (predicted_yield / max_yield) ** 0.8
249
+ norm_soil = (soil_score / max_soil) ** 1.2
250
+ return round((0.6 * norm_yield + 0.4 * norm_soil) * 100, 2)
251
+
252
+ def forecast_yield(ts_data):
253
+ model = Prophet(yearly_seasonality='auto', growth='flat')
254
+ model.fit(ts_data)
255
+ forecast = model.predict(model.make_future_dataframe(periods=1, freq='YS'))
256
+ return max(forecast.iloc[-1]['yhat'], 0)
257
+
258
+ def forecast_yield_with_accuracy(ts_data):
259
+ model = Prophet(yearly_seasonality='auto', growth='flat')
260
+ model.fit(ts_data)
261
+ future = model.make_future_dataframe(periods=1, freq='YS')
262
+ forecast = model.predict(future)
263
+ predicted_yield = max(forecast.iloc[-1]['yhat'], 0)
264
+
265
+ try:
266
+ past = forecast[forecast['ds'] < ts_data['ds'].max()]
267
+ merged = ts_data.merge(past[['ds', 'yhat']], on='ds')
268
+ mae = mean_absolute_error(merged['y'], merged['yhat'])
269
+ mape = mean_absolute_percentage_error(merged['y'], merged['yhat']) * 100
270
+ except:
271
+ mae, mape = None, None
272
+
273
+ return predicted_yield, mae, mape
274
+
275
+ def get_crop_priority_list(district_yield, base_crop_names):
276
+ priority_list = []
277
+ for crop, column in base_crop_names.items():
278
+ crop_data = district_yield[['Year', column]].dropna()
279
+ crop_data.columns = ['ds', 'y']
280
+ crop_data['ds'] = pd.to_datetime(crop_data['ds'], format='%Y')
281
+ if len(crop_data) >= 5:
282
+ yield_pred = forecast_yield(crop_data)
283
+ priority_list.append((crop, yield_pred))
284
+ return sorted(priority_list, key=lambda x: x[1], reverse=True)
285
+
286
+ def get_weather_data(lat, lon):
287
+ try:
288
+ # Get weather API key from environment variables
289
+ weather_api_key = config.OPENWEATHER_API
290
+ if weather_api_key and weather_api_key != "your_openweather_api_key_here":
291
+ url = f"https://api.weatherapi.com/v1/current.json?key={weather_api_key}&q={lat},{lon}"
292
+ response = requests.get(url)
293
+ data = response.json()
294
+ return {
295
+ "temp_c": data['current']['temp_c'],
296
+ "humidity": data['current']['humidity'],
297
+ "condition": data['current']['condition']['text'],
298
+ "wind_kph": data['current']['wind_kph']
299
+ }
300
+ else:
301
+ return {"error": "Weather API key not configured or placeholder value"}
302
+ except Exception as e:
303
+ return {"error": "Weather fetch failed", "details": str(e)}
304
+
305
+ def predict_crop_yield_from_location(crop_input, lat, lon):
306
+ district, place_name, error = get_district_from_coordinates(lat, lon)
307
+ if error:
308
+ return {"error": error}
309
+
310
+ if district is None:
311
+ return {"error": "Could not determine district from coordinates"}
312
+
313
+ district_input = clean_district_name(district)
314
+
315
+ try:
316
+ data_dir = "data"
317
+ yield_df = pd.read_csv(os.path.join(data_dir, "ICRISAT-District_Level_Data_30_Years.csv"))
318
+ soil_df = pd.read_csv(os.path.join(data_dir, "SoilHealthScores_by_District_2.csv"))
319
+ except Exception as e:
320
+ return {"error": f"Failed to read data files: {str(e)}"}
321
+
322
+ soil_df['Soil_Category'] = soil_df['SoilHealthScore'].apply(get_soil_category)
323
+ yield_columns = [col for col in yield_df.columns if 'YIELD (Kg per ha)' in col]
324
+ base_crop_names = {col.split(' YIELD')[0]: col for col in yield_columns}
325
+
326
+ if crop_input not in base_crop_names:
327
+ return {"error": f"'{crop_input}' not found in crop list."}
328
+
329
+ yield_col = base_crop_names[crop_input]
330
+
331
+ # Ensure district_input is not None before using lower()
332
+ if district_input is None:
333
+ return {"error": "Could not determine district name"}
334
+
335
+ district_yield = yield_df[yield_df['Dist Name'].str.lower() == district_input.lower()]
336
+ district_soil = soil_df[soil_df['Dist Name'].str.lower() == district_input.lower()]
337
+
338
+ if district_yield.empty or district_soil.empty:
339
+ return {"error": f"Data for district '{district_input}' not found."}
340
+
341
+ ts_data = district_yield[['Year', yield_col]].dropna()
342
+ ts_data.columns = ['ds', 'y']
343
+ ts_data['ds'] = pd.to_datetime(ts_data['ds'], format='%Y')
344
+ ts_data['year'] = ts_data['ds'].dt.year
345
+
346
+ valid_data = ts_data[ts_data['y'] > 0]
347
+ if len(valid_data) < 6:
348
+ predicted_yield = ts_data['y'].mean()
349
+ mae, mape = None, None
350
+ else:
351
+ predicted_yield, mae, mape = forecast_yield_with_accuracy(valid_data)
352
+
353
+ if predicted_yield > 1000:
354
+ yield_cat = "Highly Recommended Crop"
355
+ elif predicted_yield > 500:
356
+ yield_cat = "Good Crop"
357
+ elif predicted_yield > 200:
358
+ yield_cat = "Poor Crop"
359
+ else:
360
+ yield_cat = "Very Poor Crop"
361
+
362
+ soil_score = district_soil['SoilHealthScore'].values[0]
363
+ soil_cat = district_soil['Soil_Category'].values[0]
364
+ climate_score = calculate_dynamic_climate_score(predicted_yield, soil_score)
365
+
366
+ sorted_crops = get_crop_priority_list(district_yield, base_crop_names)
367
+ best_crop = sorted_crops[0][0] if sorted_crops else None
368
+ best_yield = sorted_crops[0][1] if sorted_crops else None
369
+
370
+ weather_data = get_weather_data(lat, lon)
371
+
372
+ crop_priority_list = []
373
+ for c, y in sorted_crops:
374
+ if y > 1000:
375
+ yc = "Highly Recommended Crop"
376
+ elif y > 500:
377
+ yc = "Good Crop"
378
+ elif y > 200:
379
+ yc = "Poor Crop"
380
+ else:
381
+ yc = "Very Poor Crop"
382
+
383
+ score = calculate_dynamic_climate_score(y, soil_score)
384
+
385
+ crop_priority_list.append({
386
+ "crop": c,
387
+ "predicted_yield": {
388
+ "kg_per_ha": round(y, 2),
389
+ "kg_per_acre": round(y / 2.47105, 2)
390
+ },
391
+ "yield_category": yc,
392
+ "climate_score": score
393
+ })
394
+
395
+ return {
396
+ "location": {
397
+ "input_coordinates": {"lat": lat, "lon": lon},
398
+ "place_name": place_name,
399
+ "detected_district": district,
400
+ },
401
+ "input_crop_analysis": {
402
+ "crop": crop_input,
403
+ "predicted_yield": {
404
+ "kg_per_ha": round(predicted_yield, 2),
405
+ "kg_per_acre": round(predicted_yield / 2.47105, 2)
406
+ },
407
+ "yield_category": yield_cat,
408
+ "prediction_accuracy": {
409
+ "mae": round(mae, 2) if mae is not None else "Not enough data",
410
+ "mape_percent": round(mape, 2) if mape is not None else "Not enough data",
411
+ "accuracy_score": round(100 - mape, 2) if mape is not None else "Not enough data"
412
+ }
413
+ },
414
+ "soil_health": {
415
+ "score": soil_score,
416
+ "category": soil_cat
417
+ },
418
+ "climate_score": climate_score,
419
+ "weather_now": weather_data,
420
+ "best_crop": {
421
+ "name": best_crop,
422
+ "predicted_yield": {
423
+ "kg_per_ha": round(best_yield, 2) if best_crop and best_yield is not None else None,
424
+ "kg_per_acre": round(best_yield / 2.47105, 2) if best_crop and best_yield is not None else None,
425
+ }
426
+ },
427
+ "crop_priority_list": crop_priority_list
428
+ }