Spaces:
Paused
Paused
| 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 | |
| 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 | |
| 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) | |
| } | |
| }) | |
| 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 | |
| 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 | |
| } | |
| }) | |
| 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) |