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)