File size: 8,221 Bytes
0def9a2
 
 
b0828b5
 
 
79939d7
b0828b5
 
0def9a2
 
 
 
 
 
7b65de5
 
 
0def9a2
 
 
 
7b65de5
0def9a2
 
7b65de5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b0828b5
 
7b65de5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0def9a2
b0828b5
 
 
 
 
 
 
0def9a2
7b65de5
 
 
0def9a2
7b65de5
b0828b5
 
7b65de5
b0828b5
7b65de5
 
 
 
 
 
 
 
 
0def9a2
 
 
 
 
 
7b65de5
 
 
 
0def9a2
 
7b65de5
 
0def9a2
 
7b65de5
 
 
 
 
 
 
0def9a2
 
7b65de5
 
 
0def9a2
7b65de5
 
 
 
 
0def9a2
 
 
 
7b65de5
 
0def9a2
 
 
 
 
 
 
7b65de5
 
 
0def9a2
7b65de5
 
0def9a2
7b65de5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79939d7
7b65de5
 
 
 
 
 
 
0def9a2
7b65de5
0def9a2
 
 
7b65de5
 
0def9a2
 
 
7b65de5
0def9a2
 
 
 
7b65de5
 
 
 
0def9a2
 
 
7b65de5
 
 
 
 
 
 
 
 
 
0def9a2
 
7b65de5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0def9a2
 
7b65de5
 
0def9a2
7b65de5
 
 
 
 
 
 
 
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
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
import os
import numpy as np
import pandas as pd
import torch
import timm
import torch.nn as nn
from flask import Flask, request, jsonify, render_template_string
from torchvision import transforms
from PIL import Image
from math import radians, cos, sin, sqrt, atan2
import joblib
import requests
from dotenv import load_dotenv
from werkzeug.utils import secure_filename

# =========================================================
# ENV & APP CONFIG
# =========================================================
load_dotenv()
OPENWEATHER_API_KEY = os.getenv("OPENWEATHER_API_KEY")

app = Flask(__name__)
UPLOAD_FOLDER = "/tmp/uploads"
os.makedirs(UPLOAD_FOLDER, exist_ok=True)

# =========================================================
# GLOBAL PLACEHOLDERS (LAZY LOADING)
# =========================================================
soil_model = None
crop_model = None
crop_model_label = None
soil_df = None
agri_df = None

class_labels = [
    "Alluvial Soil",
    "Black Soil",
    "Clay Soil",
    "Non Soil",
    "Red Soil"
]
num_classes = len(class_labels)

# =========================================================
# MODEL & DATA LOADERS (LAZY)
# =========================================================
def load_soil_model():
    global soil_model
    if soil_model is None:
        model = timm.create_model(
            "vit_base_patch16_224",
            pretrained=False,
            num_classes=num_classes
        )
        model.head = nn.Linear(model.head.in_features, num_classes)
        state = torch.load(
            "models/best_vit_model.pth",
            map_location=torch.device("cpu")
        )
        model.load_state_dict(state)
        model.eval()
        soil_model = model
    return soil_model


def load_crop_model():
    global crop_model, crop_model_label
    if crop_model is None:
        crop_model = joblib.load("models/model_random_forest.joblib")
        crop_model_label = joblib.load("models/label_encoder.joblib")
    return crop_model, crop_model_label


def load_dataframes():
    global soil_df, agri_df
    if soil_df is None:
        soil_df = pd.read_csv("data/soil_data.csv")
    if agri_df is None:
        agri_df = pd.read_csv("data/tips_menanam_dan_manfaat_tanaman.csv")

# =========================================================
# IMAGE PREPROCESS
# =========================================================
def preprocess_image(img_path):
    transform = transforms.Compose([
        transforms.Resize((224, 224)),
        transforms.ToTensor(),
        transforms.Normalize(mean=[0.5]*3, std=[0.5]*3)
    ])
    img = Image.open(img_path).convert("RGB")
    return transform(img).unsqueeze(0)

# =========================================================
# SOIL PREDICTION
# =========================================================
def predict_soil_type(img_path):
    model = load_soil_model()
    img_tensor = preprocess_image(img_path)
    with torch.no_grad():
        outputs = model(img_tensor)
        probabilities = torch.softmax(outputs, dim=1).numpy()[0]
    idx = int(np.argmax(probabilities))
    return class_labels[idx], float(probabilities[idx])

# =========================================================
# HAVERSINE & SOIL DATA
# =========================================================
def find_nearest_soil_data_weighted(soil_type, lat, lon):
    load_dataframes()
    filtered = soil_df[soil_df["Soil_Type"] == soil_type].copy()
    if filtered.empty:
        return None

    user_lat, user_lon = radians(lat), radians(lon)

    def haversine(row):
        lat2, lon2 = radians(row["Location_Latitude"]), radians(row["Location_Longitude"])
        dlat, dlon = lat2 - user_lat, lon2 - user_lon
        a = sin(dlat/2)**2 + cos(user_lat)*cos(lat2)*sin(dlon/2)**2
        c = 2 * atan2(sqrt(a), sqrt(1-a))
        return 6371 * c

    filtered["Distance_km"] = filtered.apply(haversine, axis=1)
    row = filtered.nsmallest(1, "Distance_km").iloc[0]

    return {
        "latitude": float(row["Location_Latitude"]),
        "longitude": float(row["Location_Longitude"]),
        "pH": float(row["pH"]),
        "N": float(row["Nitrogen_N_ppm"]),
        "P": float(row["Phosphorus_P_ppm"]),
        "K": float(row["Potassium_K_ppm"]),
        "distance_km": float(row["Distance_km"])
    }

# =========================================================
# WEATHER & GEO
# =========================================================
def get_weather_data(lat, lon):
    url = (
        f"https://api.openweathermap.org/data/2.5/weather"
        f"?lat={lat}&lon={lon}&appid={OPENWEATHER_API_KEY}&units=metric"
    )
    res = requests.get(url, timeout=10)
    if res.status_code != 200:
        return None
    data = res.json()
    return {
        "temperature": float(data["main"]["temp"]),
        "humidity": float(data["main"]["humidity"])
    }


def get_location_name(lat, lon):
    try:
        url = f"https://nominatim.openstreetmap.org/reverse?lat={lat}&lon={lon}&format=json"
        headers = {"User-Agent": "soil-api/1.0"}
        res = requests.get(url, headers=headers, timeout=10)
        if res.status_code == 200:
            return res.json().get("display_name", "Tidak ditemukan")
    except Exception:
        pass
    return "Tidak ditemukan"

# =========================================================
# FARMING TIPS
# =========================================================
def get_farming_tips(df, crop_name):
    match = df[df["Nama Tanaman"].str.lower() == str(crop_name).lower()]
    if not match.empty:
        row = match.iloc[0]
        return {
            "Nama Tanaman": row["Nama Tanaman"],
            "Tips Menanam": row.get("Tips Menanam", "Tidak tersedia"),
            "Manfaat": row.get("Manfaat", "Tidak tersedia")
        }
    return {"Tanaman": crop_name, "Pesan": "Data tidak tersedia"}

# =========================================================
# ROUTES
# =========================================================
@app.route("/")
def index():
    return render_template_string("""
    <h1>🌱 Soil & Crop Recommendation API</h1>
    <p>POST <code>/analyze</code> with image, lat, lon</p>
    """)


@app.route("/analyze", methods=["POST"])
def analyze():
    if "image" not in request.files:
        return jsonify({"error": "Gambar tidak ditemukan"}), 400

    try:
        lat = float(request.form.get("lat"))
        lon = float(request.form.get("lon"))
    except:
        return jsonify({"error": "Koordinat tidak valid"}), 400

    image_file = request.files["image"]
    filename = secure_filename(image_file.filename)
    image_path = os.path.join(UPLOAD_FOLDER, filename)
    image_file.save(image_path)

    soil_type, soil_acc = predict_soil_type(image_path)
    nearest = find_nearest_soil_data_weighted(soil_type, lat, lon)
    if not nearest:
        return jsonify({"error": "Data tanah tidak ditemukan"}), 404

    weather = get_weather_data(lat, lon)
    if not weather:
        return jsonify({"error": "Gagal mengambil cuaca"}), 500

    crop_model, crop_label = load_crop_model()
    input_df = pd.DataFrame([{
        "temperature": weather["temperature"],
        "humidity": weather["humidity"],
        "ph": nearest["pH"],
        "N": nearest["N"],
        "P": nearest["P"],
        "K": nearest["K"]
    }])

    proba = crop_model.predict_proba(input_df)[0]
    top_idx = np.argsort(proba)[::-1][:5]

    load_dataframes()
    crops = [
        crop_label.inverse_transform([crop_model.classes_[i]])[0]
        for i in top_idx
    ]
    percentages = [round(float(proba[i]) * 100, 2) for i in top_idx]

    tips = [get_farming_tips(agri_df, c) for c in crops]

    return jsonify({
        "soil_type": soil_type,
        "soil_accuracy": round(soil_acc * 100, 2),
        "location": get_location_name(nearest["latitude"], nearest["longitude"]),
        "weather": weather,
        "recommended_crops": [
            {"crop": c, "percentage": p}
            for c, p in zip(crops, percentages)
        ],
        "farming_tips": tips
    })

# =========================================================
# RUN SERVER (HF SPACES)
# =========================================================
if __name__ == "__main__":
    app.run(host="0.0.0.0", port=7860)