File size: 20,073 Bytes
6029ea5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
fdd6eb3
6029ea5
 
 
fdd6eb3
6029ea5
 
fdd6eb3
6029ea5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
from flask import Flask, request, jsonify
from flask_cors import CORS
import numpy as np
from tensorflow.keras.preprocessing.image import load_img, img_to_array
import tensorflow as tf
import os
from PIL import Image
import io
import base64
import cv2

app = Flask(__name__)

# CORS Configuration - Lebih spesifik
CORS(app, resources={
    r"/*": {
        "origins": ["http://localhost", "http://127.0.0.1", "http://localhost:8000", "http://127.0.0.1:8000"],
        "methods": ["GET", "POST", "OPTIONS"],
        "allow_headers": ["Content-Type", "Authorization"]
    }
})

# Configuration
# UPLOAD_FOLDER = 'uploads'
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif'}
MAX_CONTENT_LENGTH = 16 * 1024 * 1024

# app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
app.config['MAX_CONTENT_LENGTH'] = MAX_CONTENT_LENGTH

# os.makedirs(UPLOAD_FOLDER, exist_ok=True)

# Load model
try:
    model = tf.keras.models.load_model('model.h5')
    print("Model loaded successfully!")
    
    # Print model details for debugging
    print(f" Model input shape: {model.input_shape}")
    print(f" Model output shape: {model.output_shape}")
    print(f" Number of classes: {model.output_shape[-1]}")
    
except Exception as e:
    print(f"Error loading model: {e}")
    model = None

# Class names - pastikan urutan sama dengan training
class_names =[
 'Bercak_bakteri',
 'Bercak_daun_Septoria',
 'Bercak_Target',
 'Bercak_daun_awal',
 'Busuk_daun_lanjut',
 'Embun_tepung',
 'Jamur_daun',
 'Sehat',
 'Tungau_dua_bercak',
 'Virus_keriting_daun_kuning',
 'Virus_mosaik_tomat',
]

def validate_tomato_leaf_image(image):
    """
    Validasi apakah gambar adalah daun tomat menggunakan beberapa metode
    Returns: (is_valid, reason, confidence)
    """
    try:
        # Convert PIL to OpenCV format
        img_cv = cv2.cvtColor(np.array(image), cv2.COLOR_RGB2BGR)
        
        # 1. Color Analysis - Cek dominasi warna hijau
        hsv = cv2.cvtColor(img_cv, cv2.COLOR_BGR2HSV)
        
        # Define green color range in HSV
        lower_green1 = np.array([35, 40, 40])   # Light green
        upper_green1 = np.array([85, 255, 255]) # Dark green
        
        # Create mask for green colors
        green_mask = cv2.inRange(hsv, lower_green1, upper_green1)
        green_ratio = np.sum(green_mask > 0) / (green_mask.shape[0] * green_mask.shape[1])
        
        print(f"Green color ratio: {green_ratio:.3f}")
        
        # 2. Edge Detection - Cek apakah ada struktur daun
        gray = cv2.cvtColor(img_cv, cv2.COLOR_BGR2GRAY)
        edges = cv2.Canny(gray, 50, 150)
        edge_ratio = np.sum(edges > 0) / (edges.shape[0] * edges.shape[1])
        
        print(f"Edge ratio: {edge_ratio:.3f}")
        
        # 3. Aspect Ratio - Daun biasanya tidak terlalu ekstrem
        height, width = image.size[1], image.size[0]
        aspect_ratio = max(width, height) / min(width, height)
        
        print(f"Aspect ratio: {aspect_ratio:.2f}")
        
        # 4. Brightness and Contrast Analysis
        gray_array = np.array(gray)
        brightness = np.mean(gray_array)
        contrast = np.std(gray_array)
        
        print(f"Brightness: {brightness:.2f}, Contrast: {contrast:.2f}")
        
        # Validation Rules - Lebih permisif
        reasons = []
        
        # Rule 1: Must have sufficient green color (at least 10% - lebih permisif)
        if green_ratio < 0.10:
            reasons.append(f"Kurang dominasi warna hijau ({green_ratio*100:.1f}%)")
        
        # Rule 2: Must have reasonable edge structure (0.01-0.4 - lebih permisif)
        if edge_ratio < 0.01:
            reasons.append("Struktur gambar terlalu sederhana")
        elif edge_ratio > 0.4:
            reasons.append("Struktur gambar terlalu kompleks")
        
        # Rule 3: Aspect ratio shouldn't be too extreme (lebih permisif)
        if aspect_ratio > 10:
            reasons.append(f"Rasio aspek terlalu ekstrem ({aspect_ratio:.1f}:1)")
        
        # Rule 4: Brightness should be reasonable (lebih permisif)
        if brightness < 20:
            reasons.append("Gambar terlalu gelap")
        elif brightness > 220:
            reasons.append("Gambar terlalu terang")
        
        # Rule 5: Should have reasonable contrast (lebih permisif)
        if contrast < 15:
            reasons.append("Kontras gambar terlalu rendah")
        
        # Calculate confidence based on how well it matches leaf characteristics
        confidence = 0
        confidence += min(green_ratio * 2.5, 0.4)  # Max 40% for green ratio
        confidence += min(edge_ratio * 4, 0.3)     # Max 30% for edge structure
        confidence += max(0, 0.2 - (aspect_ratio - 1) * 0.02)  # Max 20% for aspect ratio
        confidence += min((brightness - 30) / 120 * 0.1, 0.1)  # Max 10% for brightness
        
        # Lebih permisif untuk confidence threshold
        is_valid = len(reasons) == 0 and confidence > 0.2
        
        return is_valid, reasons, confidence
        
    except Exception as e:
        print(f"Validation error: {e}")
        return True, [], 0.5  # Lebih permisif jika ada error validasi

def validate_with_model_confidence(prediction, confidence_threshold=0.4):  # Threshold lebih rendah
    """
    Validasi tambahan berdasarkan confidence model
    Jika confidence terlalu rendah, kemungkinan bukan daun tomat
    """
    max_confidence = np.max(prediction)
    
    if max_confidence < confidence_threshold:
        # Cek apakah prediksi terdistribusi merata (sign of uncertainty)
        sorted_probs = np.sort(prediction[0])[::-1]
        top_diff = sorted_probs[0] - sorted_probs[1]
        
        if top_diff < 0.15:  # Lebih permisif
            return False, f"Model tidak yakin dengan prediksi (confidence: {max_confidence*100:.1f}%)"
    
    return True, None

def preprocess_image(image, target_size=(224, 224)):
    from tensorflow.keras.applications.resnet50 import preprocess_input
    
    if image.mode != 'RGB':
        image = image.convert('RGB')
    
    image = image.resize(target_size)
    img_array = img_to_array(image)
    img_array = np.expand_dims(img_array, axis=0)
    
    # Use the same preprocessing as during training
    img_array = preprocess_input(img_array)
    
    return img_array

def allowed_file(filename):
    return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS

def is_healthy_plant(class_name):
    """Determine if the predicted class represents a healthy plant"""
    healthy_classes = ['Sehat', 'healthy', 'Tanaman_Sehat']
    return class_name in healthy_classes

def get_disease_info(disease_name):
    """Get disease information"""
    info = {
        'Bercak_bakteri': {
            'name': 'Bercak Bakteri',
            'symptoms': 'Bercak coklat kecil dengan tepi kuning pada daun, buah, dan batang',
            'causes': 'Bakteri Xanthomonas campestris',
            'prevention': 'Gunakan benih bebas penyakit, hindari penyiraman dari atas, rotasi tanaman',
            'treatment': 'Gunakan bakterisida yang mengandung tembaga, praktikkan rotasi tanaman',
            'severity': 'sedang'
        },
        'Bercak_daun_Septoria': {
            'name': 'Bercak Daun Septoria',
            'symptoms': 'Bercak bulat kecil dengan pusat abu-abu dan tepi coklat pada daun',
            'causes': 'Jamur Septoria lycopersici',
            'prevention': 'Hindari penyiraman dari atas, mulsa tanah, rotasi tanaman',
            'treatment': 'Hapus daun yang terinfeksi dan gunakan fungisida yang mengandung tembaga',
            'severity': 'sedang'
        },
        'Bercak_Target': {
            'name': 'Bercak Target',
            'symptoms': 'Lesi coklat dengan pola cincin target pada daun dan buah',
            'causes': 'Jamur Corynespora cassiicola',
            'prevention': 'Jaga sirkulasi udara, hindari penanaman terlalu rapat',
            'treatment': 'Gunakan fungisida dan hindari penanaman rapat',
            'severity': 'sedang'
        },
        'Bercak_daun_awal': {
            'name': 'Bercak Daun Awal',
            'symptoms': 'Lesi coklat dengan cincin konsentris pada daun, dimulai dari daun bawah',
            'causes': 'Jamur Alternaria solani',
            'prevention': 'Jaga drainase yang baik, hindari stres pada tanaman, mulsa tanah',
            'treatment': 'Gunakan fungisida yang mengandung chlorothalonil, buang daun yang terinfeksi',
            'severity': 'sedang'
        },
        'Busuk_daun_lanjut': {
            'name': 'Busuk Daun Lanjut',
            'symptoms': 'Bercak berair yang menjadi coklat pada daun dan batang, bulu putih di bawah daun',
            'causes': 'Oomycete Phytophthora infestans',
            'prevention': 'Hindari kelembaban tinggi, sirkulasi udara yang baik, tanam varietas tahan',
            'treatment': 'Gunakan fungisida sistemik seperti metalaxyl, hancurkan tanaman yang terinfeksi',
            'severity': 'tinggi'
        },
        'Embun_tepung': {
            'name': 'Embun Tepung',
            'symptoms': 'Lapisan putih seperti tepung pada permukaan daun',
            'causes': 'Jamur Leveillula atau Oidium',
            'prevention': 'Jaga sirkulasi udara, hindari kelembaban',
            'treatment': 'Gunakan fungisida sulfur atau potassium bicarbonate',
            'severity': 'sedang'
        },
        'Jamur_daun': {
            'name': 'Jamur Daun',
            'symptoms': 'Bercak kuning pada permukaan atas daun, lapisan fuzzy hijau-abu di bawah daun',
            'causes': 'Jamur Passalora fulva',
            'prevention': 'Tingkatkan sirkulasi udara, kurangi kelembaban, jaga jarak tanam',
            'treatment': 'Tingkatkan sirkulasi udara dan gunakan fungisida yang sesuai',
            'severity': 'sedang'
        },
        'Sehat': {
            'name': 'Tanaman Sehat',
            'symptoms': 'Daun hijau segar tanpa bercak',
            'causes': 'Tidak ada penyakit',
            'prevention': 'Pertahankan kondisi optimal',
            'treatment': 'Tanaman sehat, lanjutkan perawatan optimal',
            'severity': 'tidak ada'
        },
        'Tungau_dua_bercak': {
            'name': 'Tungau Dua Bercak',
            'symptoms': 'Daun menguning, bintik putih kecil, jaring laba-laba halus',
            'causes': 'Tungau Tetranychus urticae',
            'prevention': 'Jaga kelembaban udara, hindari stres kekeringan',
            'treatment': 'Gunakan mitisida atau sabun insektisida',
            'severity': 'sedang'
        },
        'Virus_keriting_daun_kuning': {
            'name': 'Virus Keriting Daun Kuning',
            'symptoms': 'Daun menguning, menggulung ke atas, pertumbuhan terhambat',
            'causes': 'Virus TYLCV oleh kutu kebul',
            'prevention': 'Kendalikan kutu kebul, gunakan mulsa reflektif',
            'treatment': 'Tanam varietas tahan, kendalikan kutu kebul',
            'severity': 'tinggi'
        },
        'Virus_mosaik_tomat': {
            'name': 'Virus Mosaik Tomat',
            'symptoms': 'Pola mosaik hijau terang dan gelap pada daun, daun keriting',
            'causes': 'Virus TMV yang menular',
            'prevention': 'Benih bebas virus, sterilisasi alat',
            'treatment': 'Hancurkan tanaman terinfeksi, sterilisasi alat',
            'severity': 'tinggi'
        }
    }
    
    return info.get(disease_name, {
        'name': disease_name,
        'symptoms': 'Informasi tidak tersedia',
        'causes': 'Tidak diketahui',
        'prevention': 'Konsultasikan dengan ahli pertanian',
        'treatment': 'Konsultasikan dengan ahli setempat',
        'severity': 'unknown'
    })

# Add OPTIONS handler for preflight requests
@app.before_request
def handle_preflight():
    if request.method == "OPTIONS":
        response = jsonify({})
        response.headers.add("Access-Control-Allow-Origin", "*")
        response.headers.add('Access-Control-Allow-Headers', "*")
        response.headers.add('Access-Control-Allow-Methods', "*")
        return response

@app.route('/health', methods=['GET'])
def health_check():
    """Check API and model status"""
    return jsonify({
        'success': True,
        'message': 'API is running',
        'model_loaded': model is not None,
        'status': 'healthy' if model else 'model_not_loaded',
        'model_info': {
            'input_shape': str(model.input_shape) if model else None,
            'output_shape': str(model.output_shape) if model else None,
            'num_classes': len(class_names)
        }
    })

@app.route('/predict', methods=['POST'])
def predict():
    """Classify disease from uploaded image with validation"""
    print("Predict endpoint called")
    print(f"Files in request: {list(request.files.keys())}")
    
    if model is None:
        print("Model not loaded")
        return jsonify({'success': False, 'error': 'Model not loaded'}), 500

    if 'image' not in request.files:
        print("No 'image' key in request.files")
        return jsonify({'success': False, 'error': 'No image provided'}), 400

    file = request.files['image']
    print(f"File received: {file.filename}")
    
    if file.filename == '':
        print("Empty filename")
        return jsonify({'success': False, 'error': 'No image selected'}), 400

    if not allowed_file(file.filename):
        print(f"Invalid file type: {file.filename}")
        return jsonify({'success': False, 'error': 'Invalid file type'}), 400

    try:
        print("Processing image...")
        image_bytes = file.read()
        print(f" Image bytes length: {len(image_bytes)}")
        
        # Open and validate image
        image = Image.open(io.BytesIO(image_bytes))
        print(f"Original image - Mode: {image.mode}, Size: {image.size}")
        
        # STEP 1: Pre-validation - Check if image looks like a tomato leaf (lebih permisif)
        print("Validating if image is a tomato leaf...")
        is_valid_leaf, validation_reasons, leaf_confidence = validate_tomato_leaf_image(image)
        
        if not is_valid_leaf:
            print(f"Image validation failed: {validation_reasons}")
            return jsonify({
                'success': False, 
                'error': 'Gambar yang diupload bukan daun tomat',
                'details': {
                    'reasons': validation_reasons,
                    'confidence': leaf_confidence,
                    'suggestion': 'Silakan upload gambar daun tomat yang jelas dengan latar belakang yang kontras'
                }
            }), 400
        
        print(f"Image validation passed with confidence: {leaf_confidence:.3f}")
        
        # STEP 2: Preprocess image for model
        img_array = preprocess_image(image)
        print(f" Preprocessed array shape: {img_array.shape}")
        print(f" Array min/max: {img_array.min():.3f}/{img_array.max():.3f}")

        # STEP 3: Make prediction
        print("Making prediction...")
        prediction = model.predict(img_array, verbose=0)
        print(f" Raw prediction shape: {prediction.shape}")
        print(f" Raw prediction: {prediction[0]}")
        
        # STEP 4: Post-validation - Check model confidence (lebih permisif)
        model_valid, model_reason = validate_with_model_confidence(prediction, confidence_threshold=0.3)
        
        if not model_valid:
            print(f"Model validation failed: {model_reason}")
            return jsonify({
                'success': False,
                'error': 'Model tidak dapat mengidentifikasi gambar sebagai daun tomat',
                'details': {
                    'reason': model_reason,
                    'suggestion': 'Pastikan gambar adalah daun tomat yang jelas dan berkualitas baik'
                }
            }), 400
        
        # STEP 5: Extract results
        predicted_index = np.argmax(prediction)
        predicted_class = class_names[predicted_index]
        confidence = float(np.max(prediction))
        confidence_percentage = round(confidence * 100, 2)

        print(f" Predicted index: {predicted_index}")
        print(f" Predicted class: {predicted_class}")
        print(f" Confidence: {confidence_percentage}%")
        
        # Get top 3 predictions for debugging
        top_indices = np.argsort(prediction[0])[::-1][:3]
        print(" Top 3 predictions:")
        for i, idx in enumerate(top_indices):
            print(f"   {i+1}. {class_names[idx]}: {prediction[0][idx]*100:.2f}%")

        # Determine if plant is healthy
        is_plant_healthy = is_healthy_plant(predicted_class)
        print(f" Is healthy: {is_plant_healthy}")

        # Get disease information
        disease_info = get_disease_info(predicted_class)
        
        # Convert image to base64 for response
        image_base64 = base64.b64encode(image_bytes).decode('utf-8')
        
        print("Prediction successful")
        return jsonify({
            'success': True,
            'data': {
                'classification': {
                    'class': predicted_class,
                    'class_name': disease_info['name'],
                    'confidence': confidence,
                    'confidence_percentage': confidence_percentage,
                    'is_healthy': is_plant_healthy,
                    'predicted_index': int(predicted_index)
                },
                'disease_info': disease_info,
                'validation_info': {
                    'leaf_confidence': leaf_confidence,
                    'passed_pre_validation': True,
                    'passed_model_validation': True
                },
                'debug_info': {
                    'top_predictions': [
                        {
                            'class': class_names[idx],
                            'confidence': float(prediction[0][idx]),
                            'percentage': round(float(prediction[0][idx]) * 100, 2)
                        }
                        for idx in top_indices
                    ],
                    'model_input_shape': str(model.input_shape),
                    'preprocessing_applied': 'resnet50_preprocess'
                },
                'image_base64': image_base64
            }
        })

    except Exception as e:
        print(f"Prediction error: {str(e)}")
        import traceback
        traceback.print_exc()
        return jsonify({'success': False, 'error': f'Prediction failed: {str(e)}'}), 500

@app.route('/test-classes', methods=['GET'])
def test_classes():
    """Endpoint untuk testing urutan class names"""
    return jsonify({
        'success': True,
        'data': {
            'class_names': class_names,
            'num_classes': len(class_names),
            'model_output_shape': str(model.output_shape) if model else None
        }
    })

@app.route('/diseases', methods=['GET'])
def get_diseases_info():
    """Return list of all known diseases and their descriptions"""
    try:
        data = []
        for class_name in class_names:
            data.append({
                'class': class_name,
                'info': get_disease_info(class_name)
            })
        return jsonify({'success': True, 'data': data})
    except Exception as e:
        return jsonify({'success': False, 'error': str(e)}), 500

if __name__ == '__main__':
    print("Starting Enhanced Tomato Disease Classification API...")
    print(f"Model loaded: {'Yes' if model is not None else 'No'}")
    if model:
        print(f" Model input shape: {model.input_shape}")
        print(f" Model output classes: {len(class_names)}")
    print("Endpoints:")
    print("- GET  /health")
    print("- POST /predict (with image validation)")
    print("- GET  /diseases")
    print("- GET  /test-classes")
    print("Image validation features:")
    print("- Color analysis (green dominance)")
    print("- Edge structure detection")
    print("- Aspect ratio validation")
    print("- Brightness/contrast checks")
    print("- Model confidence validation")
    print("Server starting on http://0.0.0.0:7860")
    app.run(host='0.0.0.0', port=7860, debug=True)