ganti api ke laravel

This commit is contained in:
rijalhabibullah 2026-04-19 04:17:06 +07:00
parent 0372164658
commit 8af733ff12
13 changed files with 1088 additions and 1301 deletions

View File

@ -1,262 +0,0 @@
"""
API Client Library - Python
Untuk mengakses API Classification dari Python
"""
import requests
import base64
from pathlib import Path
from typing import Dict, List, Optional, Tuple
import json
class RiceLeafClassificationAPI:
"""Client untuk Rice Leaf Disease Classification API"""
def __init__(self, base_url: str = "http://127.0.0.1:5000"):
"""
Initialize API client
Args:
base_url: Base URL untuk API endpoint (default: Flask API on port 5000)
"""
self.base_url = base_url
self.session = requests.Session()
self.timeout = 30
def test_connection(self) -> Tuple[bool, str]:
"""
Test koneksi ke API
Returns:
Tuple (success, message)
"""
try:
response = self.session.get(f"{self.base_url}/health", timeout=10)
if response.status_code == 200:
data = response.json()
if 'status' in data: # Flask API response
return data['status'] == 'ok', data.get('message', 'Connection OK')
else:
return data.get('success', True), data.get('message', 'Connection OK')
else:
return False, f"HTTP {response.status_code}"
except Exception as e:
return False, str(e)
def classify_image(
self,
image_path: str,
save: bool = False,
notes: Optional[str] = None
) -> Optional[Dict]:
"""
Klasifikasi gambar
Args:
image_path: Path ke file gambar
save: Jika True, simpan gambar ke server
notes: Catatan tambahan (hanya jika save=True)
Returns:
Dict dengan hasil klasifikasi atau None jika error
"""
# Validasi file
image_file = Path(image_path)
if not image_file.exists():
print(f"Error: File tidak ditemukan: {image_path}")
return None
if not image_file.suffix.lower() in ['.jpg', '.jpeg', '.png', '.gif']:
print(f"Error: Format file tidak didukung: {image_file.suffix}")
return None
try:
# Tentukan endpoint
endpoint = "classify" if not save else "classify"
url = f"{self.base_url}/{endpoint}"
# Baca dan kirim file
with open(image_file, 'rb') as f:
files = {'image': f}
data = {}
if save and notes:
data['notes'] = notes
response = self.session.post(
url,
files=files,
data=data,
timeout=self.timeout
)
if response.status_code == 200:
response_data = response.json()
if response_data.get('success'):
return response_data.get('data')
else:
print(f"Error: {response_data.get('message')}")
return None
else:
print(f"Error: HTTP {response.status_code}")
return None
except Exception as e:
print(f"Error during classification: {str(e)}")
return None
def classify_from_base64(
self,
base64_image: str,
filename: str = "image.jpg"
) -> Optional[Dict]:
"""
Klasifikasi dari base64 string
Args:
base64_image: Base64 encoded image string
filename: Nama file (opsional)
Returns:
Dict dengan hasil klasifikasi
"""
try:
url = f"{self.base_url}/classify"
payload = {
"image": base64_image,
"filename": filename
}
response = self.session.post(
url,
json=payload,
timeout=self.timeout
)
if response.status_code == 200:
response_data = response.json()
if response_data.get('success'):
return response_data.get('data')
else:
print(f"Error: {response_data.get('message')}")
return None
else:
print(f"Error: HTTP {response.status_code}")
return None
except Exception as e:
print(f"Error: {str(e)}")
return None
def batch_classify(
self,
image_paths: List[str],
save: bool = False
) -> List[Dict]:
"""
Klasifikasi multiple gambar sekaligus
Args:
image_paths: List path-ke-gambar
save: Simpan ke server
Returns:
List hasil klasifikasi
"""
results = []
total = len(image_paths)
for idx, path in enumerate(image_paths, 1):
print(f"Processing {idx}/{total}: {Path(path).name}...", end=" ")
result = self.classify_image(path, save=save)
if result:
print("")
results.append({
'image': path,
'result': result
})
else:
print("")
results.append({
'image': path,
'result': None
})
return results
def print_result(self, result: Dict):
"""Print hasil klasifikasi dalam format yang dapat dibaca"""
print("\n" + "="*60)
print("KLASIFIKASI HASIL")
print("="*60)
print(f"\n🎯 DIAGNOSIS: {result['disease_info']['name']}")
print(f" Confidence: {result['confidence']}")
print(f" Severity: {result['disease_info']['severity']}")
print(f"\n📊 PREDIKSI SEMUA KELAS:")
for class_name, score in result['all_predictions'].items():
percentage = f"{score*100:.2f}%"
bar = "" * int(score * 20)
print(f" {class_name:20} {percentage:>8} {bar}")
print(f"\n🔬 GEJALA:")
for symptom in result['disease_info']['symptoms']:
print(f"{symptom}")
print(f"\n💊 PENANGANAN:")
for treatment in result['disease_info']['treatment']:
print(f"{treatment}")
print("\n" + "="*60 + "\n")
# ============================================================================
# CONTOH PENGGUNAAN
# ============================================================================
if __name__ == "__main__":
# Initialize client
api = RiceLeafClassificationAPI()
# Test connection
print("Testing API connection...")
success, message = api.test_connection()
if success:
print(f"{message}\n")
else:
print(f"✗ Failed: {message}\n")
exit(1)
# Contoh 1: Klasifikasi single image
print("Example 1: Klasifikasi single image")
print("-" * 60)
image_path = "path/to/rice_leaf.jpg" # Ganti dengan path asli
result = api.classify_image(image_path, save=True, notes="Test dari script")
if result:
api.print_result(result)
# Contoh 2: Batch classification
print("\nExample 2: Batch classification")
print("-" * 60)
image_list = [
"path/to/image1.jpg",
"path/to/image2.jpg",
"path/to/image3.jpg",
]
results = api.batch_classify(image_list, save=True)
# Summary
successful = sum(1 for r in results if r['result'] is not None)
print(f"\nBatch Summary: {successful}/{len(results)} berhasil")
# Contoh 3: Klasifikasi dari base64
print("\nExample 3: Klasifikasi dari base64")
print("-" * 60)
with open("image.jpg", "rb") as f:
base64_image = base64.b64encode(f.read()).decode('utf-8')
result = api.classify_from_base64(base64_image)
if result:
api.print_result(result)

View File

@ -2,16 +2,19 @@
"cells": [
{
"cell_type": "code",
"execution_count": 1,
"execution_count": 4,
"id": "5ca735e6",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"TensorFlow Version: 2.20.0\n",
"GPU Available: []\n"
"ename": "ModuleNotFoundError",
"evalue": "No module named 'pandas'",
"output_type": "error",
"traceback": [
"\u001b[1;31m---------------------------------------------------------------------------\u001b[0m",
"\u001b[1;31mModuleNotFoundError\u001b[0m Traceback (most recent call last)",
"Cell \u001b[1;32mIn[4], line 3\u001b[0m\n\u001b[0;32m 1\u001b[0m \u001b[38;5;28;01mimport\u001b[39;00m \u001b[38;5;21;01mos\u001b[39;00m\n\u001b[0;32m 2\u001b[0m \u001b[38;5;28;01mimport\u001b[39;00m \u001b[38;5;21;01mnumpy\u001b[39;00m \u001b[38;5;28;01mas\u001b[39;00m \u001b[38;5;21;01mnp\u001b[39;00m\n\u001b[1;32m----> 3\u001b[0m \u001b[38;5;28;01mimport\u001b[39;00m \u001b[38;5;21;01mpandas\u001b[39;00m \u001b[38;5;28;01mas\u001b[39;00m \u001b[38;5;21;01mpd\u001b[39;00m\n\u001b[0;32m 4\u001b[0m \u001b[38;5;28;01mimport\u001b[39;00m \u001b[38;5;21;01mmatplotlib\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mpyplot\u001b[39;00m \u001b[38;5;28;01mas\u001b[39;00m \u001b[38;5;21;01mplt\u001b[39;00m\n\u001b[0;32m 5\u001b[0m \u001b[38;5;28;01mimport\u001b[39;00m \u001b[38;5;21;01mseaborn\u001b[39;00m \u001b[38;5;28;01mas\u001b[39;00m \u001b[38;5;21;01msns\u001b[39;00m\n",
"\u001b[1;31mModuleNotFoundError\u001b[0m: No module named 'pandas'"
]
}
],
@ -69,7 +72,7 @@
},
{
"cell_type": "code",
"execution_count": 2,
"execution_count": null,
"id": "63695071",
"metadata": {},
"outputs": [
@ -234,6 +237,9 @@
],
"source": [
"# Set dataset path - handle both relative and absolute paths\n",
"from pathlib import Path\n",
"import re\n",
"\n",
"notebook_dir = Path('.').absolute()\n",
"print(f\"Notebook working directory: {notebook_dir}\\n\")\n",
"\n",
@ -258,47 +264,75 @@
"\n",
"classes = ['Bacterialblight', 'Brownspot', 'Leafsmut']\n",
"\n",
"# Load images and labels\n",
"# Heuristic filter to remove offline augmented/duplicate files.\n",
"def is_augmented_or_duplicate(filename: str) -> bool:\n",
" name = filename.lower()\n",
" patterns = [\n",
" r'rotated',\n",
" r'\\borig\\b',\n",
" r'_orig_',\n",
" r'\\(\\d+\\)',\n",
" r'copy',\n",
" ]\n",
" return any(re.search(p, name) for p in patterns)\n",
"\n",
"# Build group id so near-duplicate name variants do not leak across splits.\n",
"def build_group_id(class_name: str, filename: str) -> str:\n",
" stem = Path(filename).stem.lower()\n",
" stem = re.sub(r'\\(\\d+\\)', '', stem)\n",
" stem = re.sub(r'\\s+', '_', stem)\n",
" stem = re.sub(r'_+', '_', stem).strip('_')\n",
" return f\"{class_name}:{stem}\"\n",
"\n",
"# Load images, labels, and group ids\n",
"images = []\n",
"labels = []\n",
"groups = []\n",
"class_to_idx = {class_name: idx for idx, class_name in enumerate(classes)}\n",
"\n",
"print(\"Loading dataset...\")\n",
"print(\"Loading dataset (with anti-leakage filtering)...\")\n",
"print(\"-\" * 50)\n",
"\n",
"total_images = 0\n",
"total_filtered_out = 0\n",
"\n",
"for class_name in classes:\n",
" class_path = dataset_base_path / class_name\n",
" \n",
"\n",
" if class_path.exists():\n",
" # Count images first for progress bar\n",
" image_files = [f for f in class_path.glob('*') if f.suffix.lower() in ['.jpg', '.jpeg', '.png']]\n",
" \n",
" # Load images with progress bar\n",
" kept_files = [f for f in image_files if not is_augmented_or_duplicate(f.name)]\n",
" filtered_count = len(image_files) - len(kept_files)\n",
"\n",
" image_count = 0\n",
" with tqdm(image_files, desc=f\"Loading {class_name}\", position=classes.index(class_name), leave=True) as pbar:\n",
" with tqdm(kept_files, desc=f\"Loading {class_name}\", position=classes.index(class_name), leave=True) as pbar:\n",
" for img_file in pbar:\n",
" img = cv2.imread(str(img_file))\n",
" if img is not None:\n",
" img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)\n",
" images.append(img_rgb)\n",
" labels.append(class_to_idx[class_name])\n",
" groups.append(build_group_id(class_name, img_file.name))\n",
" image_count += 1\n",
" \n",
" print(f\" ✓ {class_name}: {image_count} images loaded\\n\")\n",
"\n",
" print(f\" ✓ {class_name}: {image_count} images loaded | filtered out: {filtered_count}\\n\")\n",
" total_images += image_count\n",
" total_filtered_out += filtered_count\n",
" else:\n",
" print(f\" ✗ {class_name} folder not found!\\n\")\n",
"\n",
"print(\"-\" * 50)\n",
"print(f\"Total images loaded: {total_images}\\n\")\n",
"print(f\"Total images loaded: {total_images}\")\n",
"print(f\"Total files filtered out: {total_filtered_out}\\n\")\n",
"\n",
"# Convert to numpy arrays with proper dtype\n",
"X = np.array(images)\n",
"y = np.array(labels, dtype=np.int32) # Convert to int32 for bincount\n",
"y = np.array(labels, dtype=np.int32)\n",
"group_ids = np.array(groups)\n",
"\n",
"print(f\"Dataset shape: {X.shape}\")\n",
"print(f\"Labels shape: {y.shape}\")\n",
"print(f\"Unique group ids: {len(np.unique(group_ids))}\")\n",
"if len(y) > 0:\n",
" print(f\"Class distribution: {np.bincount(y)}\")\n",
"else:\n",
@ -307,7 +341,7 @@
},
{
"cell_type": "code",
"execution_count": 3,
"execution_count": null,
"id": "a9a6d821",
"metadata": {},
"outputs": [
@ -364,7 +398,7 @@
},
{
"cell_type": "code",
"execution_count": 4,
"execution_count": null,
"id": "f8de0b07",
"metadata": {},
"outputs": [
@ -416,35 +450,61 @@
" resized_img = cv2.resize(img, (IMG_SIZE, IMG_SIZE))\n",
" X_resized.append(resized_img)\n",
"\n",
"X_resized = np.array(X_resized)\n",
"X_resized = np.array(X_resized).astype('float32')\n",
"\n",
"# Normalize pixel values to [0, 1]\n",
"print(\"Normalizing pixel values...\")\n",
"X_normalized = X_resized.astype('float32') / 255.0\n",
"# MobileNetV2-specific preprocessing ([-1, 1] range)\n",
"from tensorflow.keras.applications.mobilenet_v2 import preprocess_input\n",
"print(\"Applying MobileNetV2 preprocess_input...\")\n",
"X_preprocessed = preprocess_input(X_resized)\n",
"\n",
"print(f\"\\nPreprocessed image shape: {X_normalized.shape}\")\n",
"print(f\"Pixel value range: [{X_normalized.min()}, {X_normalized.max()}]\")\n",
"print(f\"\\nPreprocessed image shape: {X_preprocessed.shape}\")\n",
"print(f\"Pixel value range: [{X_preprocessed.min():.3f}, {X_preprocessed.max():.3f}]\")\n",
"\n",
"# Split dataset into train, validation, and test sets\n",
"print(\"\\nSplitting dataset...\")\n",
"X_train, X_temp, y_train, y_temp = train_test_split(\n",
" X_normalized, y, test_size=0.3, random_state=42, stratify=y\n",
")\n",
"# Split dataset into train, validation, and test sets (group-aware anti-leakage split)\n",
"print(\"\\nSplitting dataset with GROUP-AWARE strategy (anti data leakage)...\")\n",
"from sklearn.model_selection import GroupShuffleSplit\n",
"\n",
"X_val, X_test, y_val, y_test = train_test_split(\n",
" X_temp, y_temp, test_size=0.5, random_state=42, stratify=y_temp\n",
")\n",
"# 70% train, 30% temp (val+test)\n",
"gss_outer = GroupShuffleSplit(n_splits=1, test_size=0.30, random_state=42)\n",
"train_idx, temp_idx = next(gss_outer.split(X_preprocessed, y, groups=group_ids))\n",
"\n",
"X_train, X_temp = X_preprocessed[train_idx], X_preprocessed[temp_idx]\n",
"y_train, y_temp = y[train_idx], y[temp_idx]\n",
"groups_train, groups_temp = group_ids[train_idx], group_ids[temp_idx]\n",
"\n",
"# Split temp equally into val and test: 15% val, 15% test\n",
"gss_inner = GroupShuffleSplit(n_splits=1, test_size=0.50, random_state=42)\n",
"val_rel_idx, test_rel_idx = next(gss_inner.split(X_temp, y_temp, groups=groups_temp))\n",
"\n",
"X_val, X_test = X_temp[val_rel_idx], X_temp[test_rel_idx]\n",
"y_val, y_test = y_temp[val_rel_idx], y_temp[test_rel_idx]\n",
"groups_val, groups_test = groups_temp[val_rel_idx], groups_temp[test_rel_idx]\n",
"\n",
"# Leakage sanity check: these must all be zero\n",
"leak_train_val = len(set(groups_train) & set(groups_val))\n",
"leak_train_test = len(set(groups_train) & set(groups_test))\n",
"leak_val_test = len(set(groups_val) & set(groups_test))\n",
"\n",
"print(\"-\" * 50)\n",
"print(f\"Training set: {X_train.shape[0]} images\")\n",
"print(f\"Training set: {X_train.shape[0]} images\")\n",
"print(f\"Validation set: {X_val.shape[0]} images\")\n",
"print(f\"Test set: {X_test.shape[0]} images\")\n",
"print(\"-\" * 50)"
"print(f\"Test set: {X_test.shape[0]} images\")\n",
"print(\"-\" * 50)\n",
"print(\"Leakage check (should be 0):\")\n",
"print(f\" Train ∩ Val : {leak_train_val}\")\n",
"print(f\" Train ∩ Test: {leak_train_test}\")\n",
"print(f\" Val ∩ Test : {leak_val_test}\")\n",
"print(\"-\" * 50)\n",
"\n",
"# Show class distribution after split\n",
"for split_name, split_y in [('Train', y_train), ('Val', y_val), ('Test', y_test)]:\n",
" counts = np.bincount(split_y, minlength=len(classes))\n",
" print(f\"{split_name} distribution: {counts}\")"
]
},
{
"cell_type": "code",
"execution_count": 5,
"execution_count": null,
"id": "eba217e2",
"metadata": {},
"outputs": [
@ -465,22 +525,21 @@
}
],
"source": [
"# Data augmentation\n",
"# Data augmentation (moderate, realistic)\n",
"print(\"Configuring data augmentation...\")\n",
"print(\"-\" * 50)\n",
"\n",
"train_datagen = ImageDataGenerator(\n",
" rotation_range=20,\n",
" width_shift_range=0.2,\n",
" height_shift_range=0.2,\n",
" rotation_range=15,\n",
" width_shift_range=0.10,\n",
" height_shift_range=0.10,\n",
" zoom_range=0.10,\n",
" horizontal_flip=True,\n",
" vertical_flip=True,\n",
" zoom_range=0.2,\n",
" shear_range=0.2,\n",
" fill_mode='nearest'\n",
")\n",
"\n",
"val_datagen = ImageDataGenerator() # No augmentation for validation\n",
"# No augmentation for validation/test\n",
"val_datagen = ImageDataGenerator()\n",
"\n",
"# Convert to one-hot encoding\n",
"print(\"Converting labels to one-hot encoding...\")\n",
@ -506,7 +565,7 @@
},
{
"cell_type": "code",
"execution_count": 6,
"execution_count": null,
"id": "146d5d58",
"metadata": {},
"outputs": [
@ -756,69 +815,30 @@
}
],
"source": [
"# Option 1: Build custom CNN model from scratch\n",
"print(\"Building custom CNN model...\")\n",
"# Build Transfer Learning model (recommended to reduce overfitting)\n",
"print(\"Building MobileNetV2 transfer learning model...\")\n",
"print(\"-\" * 50)\n",
"print(\"Architecture:\")\n",
"print(\" - 4 Convolutional Blocks (32→64→128→256 filters)\")\n",
"print(\" - Batch Normalization & Dropout for regularization\")\n",
"print(\" - Global Average Pooling + Dense layers\")\n",
"print()\n",
"\n",
"def build_custom_cnn():\n",
" model = models.Sequential([\n",
" # Block 1\n",
" layers.Conv2D(32, (3, 3), activation='relu', padding='same', input_shape=(IMG_SIZE, IMG_SIZE, 3)),\n",
" layers.BatchNormalization(),\n",
" layers.Conv2D(32, (3, 3), activation='relu', padding='same'),\n",
" layers.BatchNormalization(),\n",
" layers.MaxPooling2D((2, 2)),\n",
" layers.Dropout(0.25),\n",
" \n",
" # Block 2\n",
" layers.Conv2D(64, (3, 3), activation='relu', padding='same'),\n",
" layers.BatchNormalization(),\n",
" layers.Conv2D(64, (3, 3), activation='relu', padding='same'),\n",
" layers.BatchNormalization(),\n",
" layers.MaxPooling2D((2, 2)),\n",
" layers.Dropout(0.25),\n",
" \n",
" # Block 3\n",
" layers.Conv2D(128, (3, 3), activation='relu', padding='same'),\n",
" layers.BatchNormalization(),\n",
" layers.Conv2D(128, (3, 3), activation='relu', padding='same'),\n",
" layers.BatchNormalization(),\n",
" layers.MaxPooling2D((2, 2)),\n",
" layers.Dropout(0.25),\n",
" \n",
" # Block 4\n",
" layers.Conv2D(256, (3, 3), activation='relu', padding='same'),\n",
" layers.BatchNormalization(),\n",
" layers.Conv2D(256, (3, 3), activation='relu', padding='same'),\n",
" layers.BatchNormalization(),\n",
" layers.MaxPooling2D((2, 2)),\n",
" layers.Dropout(0.25),\n",
" \n",
" # Global Average Pooling\n",
" layers.GlobalAveragePooling2D(),\n",
" \n",
" # Dense layers\n",
" layers.Dense(512, activation='relu'),\n",
" layers.BatchNormalization(),\n",
" layers.Dropout(0.5),\n",
" \n",
" layers.Dense(256, activation='relu'),\n",
" layers.BatchNormalization(),\n",
" layers.Dropout(0.5),\n",
" \n",
" # Output layer\n",
" layers.Dense(len(classes), activation='softmax')\n",
" ])\n",
" \n",
" return model\n",
"from tensorflow.keras.applications import MobileNetV2\n",
"from tensorflow.keras import regularizers\n",
"\n",
"base_model = MobileNetV2(\n",
" input_shape=(IMG_SIZE, IMG_SIZE, 3),\n",
" include_top=False,\n",
" weights='imagenet'\n",
")\n",
"base_model.trainable = False # freeze backbone at first stage\n",
"\n",
"model = models.Sequential([\n",
" base_model,\n",
" layers.GlobalAveragePooling2D(),\n",
" layers.Dropout(0.4),\n",
" layers.Dense(128, activation='relu', kernel_regularizer=regularizers.l2(1e-4)),\n",
" layers.BatchNormalization(),\n",
" layers.Dropout(0.4),\n",
" layers.Dense(len(classes), activation='softmax')\n",
"])\n",
"\n",
"print(\"Creating model...\")\n",
"model = build_custom_cnn()\n",
"print(\"✓ Model created!\\n\")\n",
"print(\"Model Summary:\")\n",
"print(\"-\" * 50)\n",
@ -828,7 +848,7 @@
},
{
"cell_type": "code",
"execution_count": 7,
"execution_count": null,
"id": "240316b9",
"metadata": {},
"outputs": [
@ -849,30 +869,18 @@
}
],
"source": [
"# Option 2: Transfer Learning with MobileNetV2 (uncomment to use)\n",
"# base_model = MobileNetV2(input_shape=(IMG_SIZE, IMG_SIZE, 3), include_top=False, weights='imagenet')\n",
"# base_model.trainable = False\n",
"# \n",
"# model = models.Sequential([\n",
"# base_model,\n",
"# layers.GlobalAveragePooling2D(),\n",
"# layers.Dense(256, activation='relu'),\n",
"# layers.Dropout(0.5),\n",
"# layers.Dense(len(classes), activation='softmax')\n",
"# ])\n",
"\n",
"# Compile the model\n",
"# Compile the model with stronger regularization\n",
"print(\"Compiling model...\")\n",
"print(\"-\" * 50)\n",
"model.compile(\n",
" optimizer=keras.optimizers.Adam(learning_rate=0.001),\n",
" loss='categorical_crossentropy',\n",
" optimizer=keras.optimizers.Adam(learning_rate=1e-4),\n",
" loss=keras.losses.CategoricalCrossentropy(label_smoothing=0.1),\n",
" metrics=['accuracy']\n",
")\n",
"\n",
"print(\"Configuration:\")\n",
"print(\" - Optimizer: Adam (lr=0.001)\")\n",
"print(\" - Loss: Categorical Crossentropy\")\n",
"print(\" - Optimizer: Adam (lr=0.0001)\")\n",
"print(\" - Loss: Categorical Crossentropy + Label Smoothing (0.1)\")\n",
"print(\" - Metrics: Accuracy\")\n",
"print(\"-\" * 50)\n",
"print(\"✓ Model compiled successfully!\\n\")"
@ -888,7 +896,7 @@
},
{
"cell_type": "code",
"execution_count": 8,
"execution_count": null,
"id": "26699eff",
"metadata": {},
"outputs": [
@ -967,78 +975,126 @@
"source": [
"# Define callbacks with custom callback for progress display\n",
"class ProgressCallback(keras.callbacks.Callback):\n",
" def __init__(self):\n",
" def __init__(self, total_epochs):\n",
" self.start_time = None\n",
" self.epoch_times = []\n",
" \n",
" self.total_epochs = total_epochs\n",
"\n",
" def on_train_begin(self, logs=None):\n",
" self.start_time = time.time()\n",
" self.epoch_times = []\n",
" print(\"\\n📊 Training Progress:\")\n",
" print(\"-\" * 80)\n",
" \n",
"\n",
" def on_epoch_end(self, epoch, logs=None):\n",
" epoch_time = time.time() - self.start_time\n",
" self.epoch_times.append(epoch_time)\n",
" avg_epoch_time = epoch_time / (epoch + 1)\n",
" remaining_epochs = EPOCHS - (epoch + 1)\n",
" eta_seconds = avg_epoch_time * remaining_epochs\n",
" \n",
" remaining_epochs = self.total_epochs - (epoch + 1)\n",
" eta_seconds = avg_epoch_time * max(remaining_epochs, 0)\n",
"\n",
" if logs:\n",
" loss = logs.get('loss', 0)\n",
" val_loss = logs.get('val_loss', 0)\n",
" acc = logs.get('accuracy', 0)\n",
" val_acc = logs.get('val_accuracy', 0)\n",
" \n",
" progress = \"█\" * (epoch + 1) + \"░\" * (EPOCHS - epoch - 1)\n",
" print(f\"[{progress}] Epoch {epoch + 1}/{EPOCHS} | \"\n",
"\n",
" progress = \"█\" * (epoch + 1) + \"░\" * (self.total_epochs - epoch - 1)\n",
" print(f\"[{progress}] Epoch {epoch + 1}/{self.total_epochs} | \"\n",
" f\"Loss: {loss:.4f} | Val Loss: {val_loss:.4f} | \"\n",
" f\"Acc: {acc:.4f} | Val Acc: {val_acc:.4f} | \"\n",
" f\"ETA: {int(eta_seconds)}s\")\n",
" \n",
"\n",
" def on_train_end(self, logs=None):\n",
" total_time = time.time() - self.start_time\n",
" print(\"-\" * 80)\n",
" print(f\"✓ Training completed in {int(total_time)}s\\n\")\n",
"\n",
"import time\n",
"import numpy as np\n",
"from sklearn.utils.class_weight import compute_class_weight\n",
"\n",
"# Balanced class weights to reduce bias to dominant class\n",
"class_weights = compute_class_weight(\n",
" class_weight='balanced',\n",
" classes=np.unique(y_train),\n",
" y=y_train\n",
")\n",
"class_weight_dict = {i: w for i, w in enumerate(class_weights)}\n",
"print(\"Class weights:\", class_weight_dict)\n",
"\n",
"# Stage 1 callbacks\n",
"early_stop = keras.callbacks.EarlyStopping(\n",
" monitor='val_loss',\n",
" patience=10,\n",
" patience=6,\n",
" restore_best_weights=True\n",
")\n",
"\n",
"reduce_lr = keras.callbacks.ReduceLROnPlateau(\n",
" monitor='val_loss',\n",
" factor=0.5,\n",
" patience=5,\n",
" min_lr=1e-7\n",
" patience=3,\n",
" min_lr=1e-7,\n",
" verbose=1\n",
")\n",
"\n",
"# Train the model\n",
"EPOCHS = 50\n",
"checkpoint = keras.callbacks.ModelCheckpoint(\n",
" 'best_rice_leaf_model.keras',\n",
" monitor='val_loss',\n",
" save_best_only=True,\n",
" verbose=1\n",
")\n",
"\n",
"# Stage 1: train head only\n",
"EPOCHS_STAGE1 = 20\n",
"BATCH_SIZE = 32\n",
"\n",
"print(\"=\" * 80)\n",
"print(\"🚀 STARTING MODEL TRAINING\")\n",
"print(\"🚀 STAGE 1 TRAINING (Frozen Backbone)\")\n",
"print(\"=\" * 80)\n",
"print(f\"Epochs: {EPOCHS} | Batch Size: {BATCH_SIZE}\")\n",
"print(f\"Epochs: {EPOCHS_STAGE1} | Batch Size: {BATCH_SIZE}\")\n",
"print(f\"Training samples: {len(X_train)} | Validation samples: {len(X_val)}\")\n",
"print(f\"Optimizer: Adam (lr=0.001)\")\n",
"print(f\"Loss Function: Categorical Crossentropy\")\n",
"print(f\"Optimizer: Adam (lr=0.0001)\")\n",
"print(\"=\" * 80)\n",
"\n",
"history = model.fit(\n",
"history_stage1 = model.fit(\n",
" train_datagen.flow(X_train, y_train_cat, batch_size=BATCH_SIZE),\n",
" epochs=EPOCHS,\n",
" batch_size=BATCH_SIZE,\n",
" epochs=EPOCHS_STAGE1,\n",
" validation_data=(X_val, y_val_cat),\n",
" callbacks=[early_stop, reduce_lr, ProgressCallback()],\n",
" verbose=0 # Suppress default verbose output\n",
" class_weight=class_weight_dict,\n",
" callbacks=[early_stop, reduce_lr, checkpoint, ProgressCallback(EPOCHS_STAGE1)],\n",
" verbose=0\n",
")\n",
"\n",
"print(\"✅ Model training and validation completed!\")"
"# Stage 2: fine-tune last layers of MobileNetV2\n",
"print(\"\\n\" + \"=\" * 80)\n",
"print(\"🔧 STAGE 2 FINE-TUNING (Unfreeze top layers)\")\n",
"print(\"=\" * 80)\n",
"\n",
"base_model.trainable = True\n",
"for layer in base_model.layers[:-40]:\n",
" layer.trainable = False\n",
"\n",
"model.compile(\n",
" optimizer=keras.optimizers.Adam(learning_rate=1e-5),\n",
" loss=keras.losses.CategoricalCrossentropy(label_smoothing=0.1),\n",
" metrics=['accuracy']\n",
")\n",
"\n",
"EPOCHS_STAGE2 = 10\n",
"print(f\"Fine-tuning epochs: {EPOCHS_STAGE2} | lr=1e-5\")\n",
"\n",
"history_stage2 = model.fit(\n",
" train_datagen.flow(X_train, y_train_cat, batch_size=BATCH_SIZE),\n",
" epochs=EPOCHS_STAGE2,\n",
" validation_data=(X_val, y_val_cat),\n",
" class_weight=class_weight_dict,\n",
" callbacks=[early_stop, reduce_lr, checkpoint, ProgressCallback(EPOCHS_STAGE2)],\n",
" verbose=0\n",
")\n",
"\n",
"print(\"✅ Training + fine-tuning completed!\")\n",
"print(\"Best model saved to: best_rice_leaf_model.keras\")"
]
},
{
@ -1051,7 +1107,7 @@
},
{
"cell_type": "code",
"execution_count": 9,
"execution_count": null,
"id": "0c34bf89",
"metadata": {},
"outputs": [
@ -1136,7 +1192,7 @@
},
{
"cell_type": "code",
"execution_count": 10,
"execution_count": null,
"id": "586213e5",
"metadata": {},
"outputs": [
@ -1232,7 +1288,7 @@
},
{
"cell_type": "code",
"execution_count": 11,
"execution_count": null,
"id": "a0bffc2c",
"metadata": {},
"outputs": [
@ -1347,7 +1403,7 @@
},
{
"cell_type": "code",
"execution_count": 12,
"execution_count": null,
"id": "ac7f14c3",
"metadata": {},
"outputs": [
@ -1483,7 +1539,7 @@
},
{
"cell_type": "code",
"execution_count": 15,
"execution_count": null,
"id": "783a7e43",
"metadata": {},
"outputs": [
@ -1674,7 +1730,7 @@
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"display_name": "base",
"language": "python",
"name": "python3"
},
@ -1688,7 +1744,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.5"
"version": "3.12.5"
}
},
"nbformat": 4,

View File

@ -1,450 +0,0 @@
"""
Flask API Server untuk Rice Leaf Disease Classification
Menjalankan model TensorFlow dan melayani request dari Laravel
"""
import os
import json
import base64
import numpy as np
from PIL import Image
from io import BytesIO
from pathlib import Path
import warnings
warnings.filterwarnings('ignore')
# TensorFlow & Keras
try:
import tensorflow as tf
from tensorflow import keras
from tensorflow.keras.preprocessing.image import img_to_array
TENSORFLOW_AVAILABLE = True
except ImportError as e:
print(f"⚠️ Warning: TensorFlow not available: {e}")
print(" Using mock predictions for testing")
TENSORFLOW_AVAILABLE = False
keras = None
# Flask
from flask import Flask, request, jsonify
from flask_cors import CORS
# Inisialisasi Flask
app = Flask(__name__)
CORS(app)
# Konfigurasi
MODEL_PATH = None # Akan diset saat startup
MODEL = None
IMG_SIZE = (224, 224)
CLASS_NAMES = ['Bacterialblight', 'Brownspot', 'Leafsmut']
def load_model():
"""Load model dari file yang tersedia"""
global MODEL_PATH, MODEL
if not TENSORFLOW_AVAILABLE:
print("⚠️ TensorFlow not available - Using mock mode for testing")
MODEL_PATH = "MOCK_MODEL"
return True
# Cari model file yang tersedia
possible_models = [
'rice_leaf_disease_model.keras',
'rice_leaf_disease_model.h5',
'rice_leaf_disease_model.json'
]
for model_name in possible_models:
if os.path.exists(model_name):
MODEL_PATH = model_name
print(f"✓ Model ditemukan: {MODEL_PATH}")
try:
if model_name.endswith('.keras'):
MODEL = keras.models.load_model(model_name)
elif model_name.endswith('.h5'):
MODEL = keras.models.load_model(model_name)
elif model_name.endswith('.json'):
# Load model dari JSON + weights
with open(model_name, 'r') as f:
model_json = f.read()
MODEL = keras.models.model_from_json(model_json)
# Cari weights file
weights_base = model_name.replace('.json', '')
weights_files = [
f"{weights_base}.h5",
f"{weights_base}_weights.h5"
]
for weights_file in weights_files:
if os.path.exists(weights_file):
MODEL.load_weights(weights_file)
print(f"✓ Weights dimuat: {weights_file}")
break
print(f"✓ Model berhasil dimuat!")
print(f" Model input shape: {MODEL.input_shape}")
print(f" Number of layers: {len(MODEL.layers)}")
return True
except Exception as e:
print(f"✗ Error loading model: {str(e)}")
return False
print("✗ Model tidak ditemukan!")
print(" Letakkan salah satu dari ini di folder yang sama dengan script ini:")
print(" - rice_leaf_disease_model.keras")
print(" - rice_leaf_disease_model.h5")
print(" - rice_leaf_disease_model.json (+ .h5 weights)")
return False
def preprocess_image(image_data):
"""
Preprocessing gambar dari base64 atau bytes
Args:
image_data: base64 string atau bytes
Returns:
Preprocessed image array atau None jika error
"""
try:
# Jika string base64, decode dulu
if isinstance(image_data, str):
image_bytes = base64.b64decode(image_data)
else:
image_bytes = image_data
# Convert bytes ke image menggunakan PIL
image = Image.open(BytesIO(image_bytes))
# Convert to RGB jika diperlukan
if image.mode != 'RGB':
image = image.convert('RGB')
# Resize ke ukuran yang diharapkan model
image = image.resize(IMG_SIZE, Image.Resampling.LANCZOS)
# Convert ke numpy array
img_array = np.array(image, dtype='float32')
# Normalize pixel values ke range [0, 1]
img_array = img_array / 255.0
# Add batch dimension
img_array = np.expand_dims(img_array, axis=0)
return img_array
except Exception as e:
print(f"Error preprocessing image: {str(e)}")
return None
def classify_image(image_data):
"""
Klasifikasi gambar menggunakan model
Args:
image_data: base64 string atau bytes
Returns:
Dict dengan hasil klasifikasi atau None jika error
"""
# Mock mode - jika TensorFlow tidak tersedia
if not TENSORFLOW_AVAILABLE:
import random
predictions = [random.uniform(0.1, 0.9) for _ in CLASS_NAMES]
max_pred = max(predictions)
idx = predictions.index(max_pred)
all_predictions = {}
for i, class_name in enumerate(CLASS_NAMES):
all_predictions[class_name] = round(predictions[i], 4)
return {
'predicted_class': CLASS_NAMES[idx],
'confidence': round(max_pred, 4),
'all_predictions': all_predictions
}
if MODEL is None:
return None
try:
# Preprocess image
img_array = preprocess_image(image_data)
if img_array is None:
return None
# Prediction
predictions = MODEL.predict(img_array, verbose=0)
# Get predicted class dan confidence
predicted_idx = np.argmax(predictions[0])
predicted_class = CLASS_NAMES[predicted_idx]
confidence = float(predictions[0][predicted_idx])
# Build all predictions
all_predictions = {}
for idx, class_name in enumerate(CLASS_NAMES):
all_predictions[class_name] = float(predictions[0][idx])
return {
'predicted_class': predicted_class,
'confidence': confidence,
'all_predictions': all_predictions
}
except Exception as e:
print(f"Error during classification: {str(e)}")
return None
# ============================================================================
# ROUTES
# ============================================================================
@app.route('/health', methods=['POST', 'GET'])
def health_check():
"""Check apakah API berjalan dan model tersedia"""
if not TENSORFLOW_AVAILABLE:
return jsonify({
'status': 'ok',
'message': 'API running in MOCK MODE (TensorFlow not available)',
'model_loaded': False,
'mock_mode': True,
'classes': CLASS_NAMES
}), 200
if MODEL is None:
return jsonify({
'status': 'error',
'message': 'Model not loaded',
'model_loaded': False
}), 503
return jsonify({
'status': 'ok',
'message': 'API is running',
'model_loaded': True,
'model_path': MODEL_PATH,
'classes': CLASS_NAMES,
'input_shape': str(MODEL.input_shape)
}), 200
@app.route('/classify', methods=['POST'])
def classify():
"""
API endpoint untuk klasifikasi gambar
Expected request:
{
"image": "base64_encoded_image_string",
"filename": "optional_filename.jpg"
}
Response:
{
"success": true,
"predicted_class": "Bacterialblight",
"confidence": 0.95,
"all_predictions": {
"Bacterialblight": 0.95,
"Brownspot": 0.04,
"Leafsmut": 0.01
}
}
"""
try:
data = request.get_json()
if data is None:
return jsonify({
'success': False,
'message': 'Request harus JSON'
}), 400
# Validasi input
if 'image' not in data:
return jsonify({
'success': False,
'message': 'Field "image" (base64) diperlukan'
}), 400
image_data = data['image']
filename = data.get('filename', 'unknown')
# Klasifikasi
result = classify_image(image_data)
if result is None:
return jsonify({
'success': False,
'message': 'Gagal memproses gambar'
}), 400
# Log hasil
print(f"✓ Classification done: {filename} -> {result['predicted_class']} ({result['confidence']:.2%})")
return jsonify({
'success': True,
'predicted_class': result['predicted_class'],
'confidence': result['confidence'],
'all_predictions': result['all_predictions'],
'filename': filename
}), 200
except Exception as e:
print(f"✗ Error in classify endpoint: {str(e)}")
return jsonify({
'success': False,
'message': f'Error: {str(e)}'
}), 500
@app.route('/classify-from-url', methods=['POST'])
def classify_from_url():
"""
Alternative endpoint untuk klasifikasi dari URL gambar
Expected request:
{
"image_url": "http://example.com/image.jpg"
}
"""
try:
data = request.get_json()
if data is None or 'image_url' not in data:
return jsonify({
'success': False,
'message': 'Field "image_url" diperlukan'
}), 400
image_url = data['image_url']
import requests
response = requests.get(image_url, timeout=10)
if response.status_code != 200:
return jsonify({
'success': False,
'message': f'Gagal download image dari URL'
}), 400
# Klasifikasi
result = classify_image(response.content)
if result is None:
return jsonify({
'success': False,
'message': 'Gagal memproses gambar'
}), 400
return jsonify({
'success': True,
'predicted_class': result['predicted_class'],
'confidence': result['confidence'],
'all_predictions': result['all_predictions'],
'url': image_url
}), 200
except Exception as e:
return jsonify({
'success': False,
'message': f'Error: {str(e)}'
}), 500
@app.route('/info', methods=['GET'])
def model_info():
"""Get informasi tentang model"""
if MODEL is None:
return jsonify({
'status': 'error',
'message': 'Model not loaded'
}), 503
return jsonify({
'model_loaded': True,
'model_path': MODEL_PATH,
'classes': CLASS_NAMES,
'number_of_classes': len(CLASS_NAMES),
'input_shape': str(MODEL.input_shape),
'number_of_layers': len(MODEL.layers),
'total_parameters': int(MODEL.count_params())
}), 200
@app.route('/', methods=['GET'])
def index():
"""Root endpoint dengan informasi API"""
return jsonify({
'name': 'Rice Leaf Disease Classification API',
'version': '1.0',
'description': 'API untuk klasifikasi penyakit daun padi menggunakan CNN',
'endpoints': {
'POST /classify': 'Klasifikasi gambar (base64)',
'POST /classify-from-url': 'Klasifikasi gambar dari URL',
'GET /health': 'Health check',
'GET /info': 'Informasi model',
'GET /': 'Info API ini'
},
'model_status': 'Loaded' if MODEL is not None else 'Not loaded',
'classes': CLASS_NAMES
}), 200
@app.errorhandler(404)
def not_found(error):
return jsonify({
'success': False,
'message': 'Endpoint tidak ditemukan'
}), 404
@app.errorhandler(500)
def server_error(error):
return jsonify({
'success': False,
'message': 'Internal server error'
}), 500
# ============================================================================
# STARTUP
# ============================================================================
if __name__ == '__main__':
print("\n" + "="*60)
print("Rice Leaf Disease Classification API Server")
print("="*60 + "\n")
# Pindah ke folder yang sama dengan script
script_dir = os.path.dirname(os.path.abspath(__file__))
os.chdir(script_dir)
print(f"Working directory: {os.getcwd()}\n")
# Load model
print("Loading model...")
if not load_model():
print("\n⚠️ WARNING: Model tidak dapat dimuat!")
print(" API akan berjalan tapi endpoint /classify akan gagal.\n")
print("\n" + "="*60)
print("Starting Flask API Server...")
print("="*60)
print("Server berjalan di http://127.0.0.1:5000/")
print("Tekan CTRL+C untuk menghentikan.\n")
# Run Flask app
app.run(
host='127.0.0.1',
port=5000,
debug=False, # Set ke True jika development
use_reloader=False
)

File diff suppressed because one or more lines are too long

View File

@ -44,15 +44,14 @@ echo ========================================
echo.
echo Langkah selanjutnya:
echo.
echo 1. Buka Terminal 1 dan jalankan Python API:
echo cd "rice leaf diseases dataset"
echo python api_server.py
echo.
echo 2. Buka Terminal 2 dan jalankan Laravel:
echo 1. Buka Terminal 1 dan jalankan Laravel:
echo cd "web_TA"
echo php artisan serve
echo.
echo 3. Test API:
echo 2. Test API:
echo python test_api.py
echo.
echo 3. Pastikan Python environment memiliki dependency inferensi:
echo pip install -r "rice leaf diseases dataset\requirements_api.txt"
echo.
pause

View File

@ -25,9 +25,9 @@ fi
# Check model files
echo ""
echo "Cek file model..."
if [ -f "rice_leaf_diseases dataset/rice_leaf_disease_model.keras" ]; then
if [ -f "rice leaf diseases dataset/rice_leaf_disease_model.keras" ]; then
echo "[OK] Model keras ditemukan"
elif [ -f "rice_leaf_diseases dataset/rice_leaf_disease_model.h5" ]; then
elif [ -f "rice leaf diseases dataset/rice_leaf_disease_model.h5" ]; then
echo "[OK] Model h5 ditemukan"
else
echo "[WARNING] File model tidak ditemukan"
@ -41,14 +41,13 @@ echo "========================================"
echo ""
echo "Langkah selanjutnya:"
echo ""
echo "1. Buka Terminal 1 dan jalankan Python API:"
echo " cd \"rice leaf diseases dataset\""
echo " python3 api_server.py"
echo ""
echo "2. Buka Terminal 2 dan jalankan Laravel:"
echo "1. Buka Terminal 1 dan jalankan Laravel:"
echo " cd web_TA"
echo " php artisan serve"
echo ""
echo "3. Test API:"
echo "2. Test API:"
echo " python3 test_api.py"
echo ""
echo "3. Pastikan dependency inferensi Python sudah terpasang:"
echo " pip3 install -r \"rice leaf diseases dataset/requirements_api.txt\""
echo ""

View File

@ -3,59 +3,60 @@ Simple test script untuk menguji API Classification
"""
import requests
import base64
import json
from pathlib import Path
# Konfigurasi
LARAVEL_API_BASE = "http://127.0.0.1:8000/api/classification"
PYTHON_API_BASE = "http://127.0.0.1:5000"
def test_python_api_health():
"""Test health check Python API"""
def test_laravel_api_health():
"""Test health check API klasifikasi di Laravel"""
print("\n" + "="*60)
print("TEST 1: Python API Health Check")
print("TEST 1: Laravel Classification Health Check")
print("="*60)
try:
response = requests.get(f"{PYTHON_API_BASE}/health", timeout=5)
response = requests.get(f"{LARAVEL_API_BASE}/health", timeout=15)
if response.status_code == 200:
data = response.json()
print("✅ Python API berhasil dihubungi!")
print(f" Status: {data['status']}")
print(f" Model Loaded: {data['model_loaded']}")
print(f" Classes: {', '.join(data['classes'])}")
print("✅ Laravel API berhasil dihubungi!")
print(f" Success: {data.get('success')}")
print(f" Message: {data.get('message')}")
model_info = data.get('model_info', {})
print(f" Model Loaded: {model_info.get('model_loaded')}")
print(f" Classes: {', '.join(model_info.get('classes', []))}")
return True
else:
print(f"❌ API responded with status {response.status_code}")
print(f" Response: {response.text}")
return False
except requests.exceptions.ConnectionError:
print("❌ Tidak dapat menghubungi Python API")
print(f" Pastikan server berjalan di {PYTHON_API_BASE}")
print("❌ Tidak dapat menghubungi Laravel API")
print(" Pastikan server berjalan di http://127.0.0.1:8000")
return False
except Exception as e:
print(f"❌ Error: {str(e)}")
return False
def test_laravel_api_connection():
"""Test connection via Laravel API"""
def test_laravel_api_info():
"""Test endpoint info model via Laravel API"""
print("\n" + "="*60)
print("TEST 2: Laravel API Connection Test")
print("TEST 2: Laravel Model Info")
print("="*60)
try:
response = requests.get(f"{LARAVEL_API_BASE}/test", timeout=5)
response = requests.get(f"{LARAVEL_API_BASE}/info", timeout=15)
if response.status_code == 200:
data = response.json()
print("Laravel API berhasil dihubungi!")
print("Endpoint info berhasil diakses!")
print(f" Success: {data['success']}")
print(f" Message: {data['message']}")
if 'model_info' in data:
print(f" Model Info: {json.dumps(data['model_info'], indent=2)}")
if 'data' in data:
print(f" Model Info: {json.dumps(data['data'], indent=2)}")
return True
else:
print(f"❌ API responded with status {response.status_code}")
@ -167,11 +168,9 @@ def main():
print("# Rice Leaf Disease Classification API - Test Suite")
print("#" * 60)
# Test Python API
python_ok = test_python_api_health()
# Test Laravel API
laravel_ok = test_laravel_api_connection()
# Test Laravel API health dan info
health_ok = test_laravel_api_health()
info_ok = test_laravel_api_info()
# Test dengan gambar contoh jika ada
test_image_paths = [
@ -202,8 +201,8 @@ def main():
print("\n" + "="*60)
print("TEST SUMMARY")
print("="*60)
print(f"Python API: {'✅ OK' if python_ok else '❌ FAILED'}")
print(f"Laravel API: {'✅ OK' if laravel_ok else '❌ FAILED'}")
print(f"Laravel Health: {'✅ OK' if health_ok else '❌ FAILED'}")
print(f"Laravel Info: {'✅ OK' if info_ok else '❌ FAILED'}")
print("\nUntuk hasil lengkap, sediakan file gambar test.\n")

View File

@ -62,4 +62,8 @@ AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=
AWS_USE_PATH_STYLE_ENDPOINT=false
PYTHON_EXECUTABLE=python
PYTHON_CLASSIFIER_SCRIPT=
RICE_MODEL_DIR=
VITE_APP_NAME="${APP_NAME}"

View File

@ -3,16 +3,20 @@
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Log;
use App\Models\Classification;
use App\Services\PythonClassificationService;
class ClassificationController extends Controller
{
private $pythonApiUrl = 'http://127.0.0.1:5000'; // URL Flask API
public function __construct(
private readonly PythonClassificationService $classificationService
) {
}
/**
* Menerima gambar dan mengirim ke Python API untuk klasifikasi
* Menerima gambar dan mengklasifikasi melalui service internal Laravel
* POST /api/classify
*/
public function classify(Request $request)
@ -30,36 +34,44 @@ public function classify(Request $request)
$imageContent = file_get_contents($file->getRealPath());
$base64Image = base64_encode($imageContent);
// Kirim request ke Flask API
$response = Http::timeout(30)->post($this->pythonApiUrl . '/classify', [
// Jalankan klasifikasi melalui service lokal (tanpa Flask API)
$result = $this->classificationService->classifyFromBase64([
'image' => $base64Image,
'filename' => $file->getClientOriginalName(),
]);
// Cek apakah request berhasil
if ($response->failed()) {
if (!($result['success'] ?? false)) {
return response()->json([
'success' => false,
'message' => 'Gagal menghubungi model classification',
'error' => $response->body()
'error' => $result['message'] ?? 'Unknown error',
], 500);
}
$result = $response->json();
// Tambahkan informasi detail tentang penyakit
$diseaseInfo = $this->getDiseaseInfo($result['predicted_class']);
// Save to database
Classification::create([
'filename' => $file->getClientOriginalName(),
'predicted_class' => $result['predicted_class'],
'confidence' => $result['confidence'],
'all_predictions' => $result['all_predictions'],
'disease_name' => $diseaseInfo['name'],
'severity' => $diseaseInfo['severity'],
'notes' => 'Classification without storage',
]);
$savedToDatabase = true;
$persistenceWarning = null;
try {
Classification::create([
'filename' => $file->getClientOriginalName(),
'predicted_class' => $result['predicted_class'],
'confidence' => $result['confidence'],
'all_predictions' => $result['all_predictions'],
'disease_name' => $diseaseInfo['name'],
'severity' => $diseaseInfo['severity'],
'notes' => 'Classification without storage',
]);
} catch (\Throwable $dbException) {
$savedToDatabase = false;
$persistenceWarning = 'Klasifikasi berhasil, tetapi gagal simpan ke database.';
Log::warning('Classification result not persisted', [
'filename' => $file->getClientOriginalName(),
'error' => $dbException->getMessage(),
]);
}
return response()->json([
'success' => true,
@ -70,6 +82,8 @@ public function classify(Request $request)
'confidence_value' => $result['confidence'],
'all_predictions' => $result['all_predictions'],
'disease_info' => $diseaseInfo,
'saved_to_database' => $savedToDatabase,
'persistence_warning' => $persistenceWarning,
'timestamp' => now(),
]
], 200);
@ -89,7 +103,7 @@ public function classify(Request $request)
}
/**
* Upload gambar dan simpan ke database, kemudian klasifikasi
* Upload gambar dan simpan ke database, kemudian klasifikasi
* POST /api/classify-and-save
*/
public function classifyAndSave(Request $request)
@ -109,36 +123,47 @@ public function classifyAndSave(Request $request)
$imageContent = file_get_contents($file->getRealPath());
$base64Image = base64_encode($imageContent);
// Kirim request ke Flask API
$response = Http::timeout(30)->post($this->pythonApiUrl . '/classify', [
// Jalankan klasifikasi melalui service lokal (tanpa Flask API)
$result = $this->classificationService->classifyFromBase64([
'image' => $base64Image,
'filename' => $file->getClientOriginalName(),
]);
if ($response->failed()) {
if (!($result['success'] ?? false)) {
// Hapus file yang sudah disimpan jika klasifikasi gagal
Storage::disk('public')->delete($storagePath);
return response()->json([
'success' => false,
'message' => 'Gagal menghubungi model classification',
'error' => $result['message'] ?? 'Unknown error',
], 500);
}
$result = $response->json();
$diseaseInfo = $this->getDiseaseInfo($result['predicted_class']);
// Save to database
Classification::create([
'image_path' => $storagePath,
'filename' => $file->getClientOriginalName(),
'predicted_class' => $result['predicted_class'],
'confidence' => $result['confidence'],
'all_predictions' => $result['all_predictions'],
'disease_name' => $diseaseInfo['name'],
'severity' => $diseaseInfo['severity'],
'notes' => $request->input('notes'),
]);
$savedToDatabase = true;
$persistenceWarning = null;
try {
Classification::create([
'image_path' => $storagePath,
'filename' => $file->getClientOriginalName(),
'predicted_class' => $result['predicted_class'],
'confidence' => $result['confidence'],
'all_predictions' => $result['all_predictions'],
'disease_name' => $diseaseInfo['name'],
'severity' => $diseaseInfo['severity'],
'notes' => $request->input('notes'),
]);
} catch (\Throwable $dbException) {
$savedToDatabase = false;
$persistenceWarning = 'Gambar berhasil diklasifikasi dan disimpan file, tetapi gagal simpan riwayat ke database.';
Log::warning('Classification file stored but DB persist failed', [
'filename' => $file->getClientOriginalName(),
'path' => $storagePath,
'error' => $dbException->getMessage(),
]);
}
return response()->json([
'success' => true,
@ -150,6 +175,8 @@ public function classifyAndSave(Request $request)
'confidence_value' => $result['confidence'],
'all_predictions' => $result['all_predictions'],
'disease_info' => $diseaseInfo,
'saved_to_database' => $savedToDatabase,
'persistence_warning' => $persistenceWarning,
'notes' => $request->input('notes'),
'timestamp' => now(),
]
@ -170,34 +197,143 @@ public function classifyAndSave(Request $request)
}
/**
* Test koneksi ke Python API
* Klasifikasi gambar dari URL
* POST /api/classification/classify-from-url
*/
public function classifyFromUrl(Request $request)
{
try {
$request->validate([
'image_url' => 'required|url|max:2048',
'notes' => 'nullable|string|max:500',
'save' => 'nullable|boolean',
]);
$imageUrl = $request->input('image_url');
$result = $this->classificationService->classifyFromUrl([
'image_url' => $imageUrl,
]);
if (!($result['success'] ?? false)) {
return response()->json([
'success' => false,
'message' => $result['message'] ?? 'Gagal memproses gambar dari URL',
], 400);
}
$diseaseInfo = $this->getDiseaseInfo($result['predicted_class']);
if ($request->boolean('save')) {
$urlPath = parse_url($imageUrl, PHP_URL_PATH);
$filename = is_string($urlPath) && $urlPath !== ''
? basename($urlPath)
: 'from_url_image';
Classification::create([
'filename' => $filename,
'predicted_class' => $result['predicted_class'],
'confidence' => $result['confidence'],
'all_predictions' => $result['all_predictions'],
'disease_name' => $diseaseInfo['name'],
'severity' => $diseaseInfo['severity'],
'notes' => $request->input('notes', 'Classification from URL'),
]);
}
return response()->json([
'success' => true,
'message' => 'Klasifikasi dari URL berhasil',
'data' => [
'url' => $imageUrl,
'predicted_class' => $result['predicted_class'],
'confidence' => round($result['confidence'] * 100, 2) . '%',
'confidence_value' => $result['confidence'],
'all_predictions' => $result['all_predictions'],
'disease_info' => $diseaseInfo,
'timestamp' => now(),
]
], 200);
} catch (\Illuminate\Validation\ValidationException $e) {
return response()->json([
'success' => false,
'message' => 'Validasi gagal',
'errors' => $e->errors()
], 422);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'Terjadi kesalahan: ' . $e->getMessage(),
], 500);
}
}
/**
* Health check klasifikasi service
* GET /api/classification/health
*/
public function health()
{
try {
$result = $this->classificationService->health();
if ($result['status'] !== 'ok') {
return response()->json([
'success' => false,
'message' => $result['message'] ?? 'Model tidak siap',
'model_info' => $result,
], 503);
}
return response()->json([
'success' => true,
'message' => 'Koneksi ke model classification berhasil',
'model_info' => $result,
], 200);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'Tidak dapat melakukan health check model: ' . $e->getMessage(),
], 500);
}
}
/**
* Informasi model klasifikasi
* GET /api/classification/info
*/
public function info()
{
try {
$result = $this->classificationService->info();
if (!($result['model_loaded'] ?? false)) {
return response()->json([
'success' => false,
'message' => $result['message'] ?? 'Model tidak tersedia',
'data' => $result,
], 503);
}
return response()->json([
'success' => true,
'message' => 'Informasi model berhasil diambil',
'data' => $result,
], 200);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'Gagal mengambil informasi model: ' . $e->getMessage(),
], 500);
}
}
/**
* Backward-compatible endpoint test (alias untuk health)
* GET /api/classification/test
*/
public function testConnection()
{
try {
$response = Http::timeout(10)->post($this->pythonApiUrl . '/health', []);
if ($response->successful()) {
return response()->json([
'success' => true,
'message' => 'Koneksi ke model API berhasil',
'model_info' => $response->json()
], 200);
}
return response()->json([
'success' => false,
'message' => 'Model API tidak merespons dengan benar'
], 500);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'Tidak dapat menghubungi model API: ' . $e->getMessage(),
'hint' => 'Pastikan server Python API sudah berjalan di ' . $this->pythonApiUrl
], 500);
}
return $this->health();
}
/**

View File

@ -0,0 +1,130 @@
<?php
namespace App\Services;
use RuntimeException;
use Symfony\Component\Process\Process;
class PythonClassificationService
{
private string $pythonExecutable;
private string $scriptPath;
private string $modelDirectory;
public function __construct()
{
$this->pythonExecutable = env('PYTHON_EXECUTABLE', 'python');
$this->scriptPath = env('PYTHON_CLASSIFIER_SCRIPT', base_path('scripts/rice_inference.py'));
$this->modelDirectory = env('RICE_MODEL_DIR', base_path('../rice leaf diseases dataset'));
}
public function classifyFromBase64(array $payload): array
{
return $this->runAction('classify', $payload, 120);
}
public function classifyFromUrl(array $payload): array
{
return $this->runAction('classify-from-url', $payload, 120);
}
public function health(): array
{
return $this->runAction('health');
}
public function info(): array
{
return $this->runAction('info');
}
private function runAction(string $action, array $payload = [], int $timeout = 60): array
{
if (!is_file($this->scriptPath)) {
throw new RuntimeException("Script classifier tidak ditemukan di {$this->scriptPath}");
}
$command = [
$this->pythonExecutable,
$this->scriptPath,
$action,
'--model-dir',
$this->modelDirectory,
];
$process = new Process($command, base_path(), $this->buildProcessEnvironment());
$process->setTimeout($timeout);
$process->setInput(json_encode($payload, JSON_UNESCAPED_SLASHES));
$process->run();
if (!$process->isSuccessful()) {
$stderr = trim($process->getErrorOutput());
$stdout = trim($process->getOutput());
throw new RuntimeException($stderr !== '' ? $stderr : ($stdout !== '' ? $stdout : 'Gagal menjalankan proses inferensi Python'));
}
$output = trim($process->getOutput());
if ($output === '') {
throw new RuntimeException('Proses inferensi Python tidak mengembalikan output.');
}
$decoded = json_decode($output, true);
if (!is_array($decoded)) {
throw new RuntimeException('Output inferensi Python bukan JSON yang valid.');
}
return $decoded;
}
private function buildProcessEnvironment(): array
{
$environment = array_merge($_SERVER, $_ENV);
// Prevent Python from using conflicting host-level overrides.
unset($environment['PYTHONHOME'], $environment['PYTHONPATH']);
$pythonDir = dirname($this->pythonExecutable);
$currentPath = getenv('PATH') ?: ($environment['PATH'] ?? '');
$environment['PATH'] = $pythonDir . PATH_SEPARATOR . $currentPath;
if (!isset($environment['SystemRoot']) || $environment['SystemRoot'] === '') {
$environment['SystemRoot'] = getenv('SystemRoot') ?: 'C:\\Windows';
}
if (!isset($environment['WINDIR']) || $environment['WINDIR'] === '') {
$environment['WINDIR'] = getenv('WINDIR') ?: 'C:\\Windows';
}
$fallbackUserProfile = getenv('USERPROFILE') ?: ('C:\\Users\\' . (getenv('USERNAME') ?: 'Public'));
if (!isset($environment['USERPROFILE']) || $environment['USERPROFILE'] === '') {
$environment['USERPROFILE'] = $fallbackUserProfile;
}
if (!isset($environment['HOMEDRIVE']) || $environment['HOMEDRIVE'] === '') {
$environment['HOMEDRIVE'] = getenv('HOMEDRIVE') ?: substr($fallbackUserProfile, 0, 2);
}
if (!isset($environment['HOMEPATH']) || $environment['HOMEPATH'] === '') {
$environment['HOMEPATH'] = getenv('HOMEPATH') ?: substr($fallbackUserProfile, 2);
}
if (!isset($environment['APPDATA']) || $environment['APPDATA'] === '') {
$environment['APPDATA'] = getenv('APPDATA') ?: ($fallbackUserProfile . '\\AppData\\Roaming');
}
if (!isset($environment['LOCALAPPDATA']) || $environment['LOCALAPPDATA'] === '') {
$environment['LOCALAPPDATA'] = getenv('LOCALAPPDATA') ?: ($fallbackUserProfile . '\\AppData\\Local');
}
if (!isset($environment['TEMP']) || $environment['TEMP'] === '') {
$environment['TEMP'] = getenv('TEMP') ?: ($fallbackUserProfile . '\\AppData\\Local\\Temp');
}
if (!isset($environment['TMP']) || $environment['TMP'] === '') {
$environment['TMP'] = getenv('TMP') ?: $environment['TEMP'];
}
return $environment;
}
}

View File

@ -15,11 +15,16 @@
// Classification endpoints
Route::prefix('classification')->group(function () {
// Test koneksi ke Python API
// Health check klasifikasi (alias test untuk backward compatibility)
Route::get('/health', [ClassificationController::class, 'health']);
Route::get('/info', [ClassificationController::class, 'info']);
Route::get('/test', [ClassificationController::class, 'testConnection']);
// Klasifikasi gambar (hanya analisis)
Route::post('/classify', [ClassificationController::class, 'classify']);
// Klasifikasi gambar dari URL
Route::post('/classify-from-url', [ClassificationController::class, 'classifyFromUrl']);
// Klasifikasi dan simpan gambar
Route::post('/classify-and-save', [ClassificationController::class, 'classifyAndSave']);

View File

@ -0,0 +1,293 @@
"""
Python CLI untuk inferensi model rice leaf disease.
Dipanggil langsung dari Laravel, tanpa Flask API.
"""
import argparse
import base64
import json
import os
import sys
from io import BytesIO
import numpy as np
import requests
from PIL import Image
CLASS_NAMES = ["Bacterialblight", "Brownspot", "Leafsmut"]
IMG_SIZE = (224, 224)
try:
from tensorflow import keras
TENSORFLOW_AVAILABLE = True
TENSORFLOW_IMPORT_ERROR = None
except Exception:
keras = None
TENSORFLOW_AVAILABLE = False
TENSORFLOW_IMPORT_ERROR = str(sys.exc_info()[1])
MODEL = None
MODEL_PATH = None
def _read_json_input() -> dict:
raw = sys.stdin.read().strip()
if not raw:
return {}
try:
data = json.loads(raw)
return data if isinstance(data, dict) else {}
except json.JSONDecodeError:
return {}
def _emit(data: dict, exit_code: int = 0) -> None:
print(json.dumps(data, ensure_ascii=True))
raise SystemExit(exit_code)
def _find_model_file(model_dir: str) -> str | None:
candidates = [
"rice_leaf_disease_model.keras",
"rice_leaf_disease_model.h5",
"rice_leaf_disease_model.json",
]
for file_name in candidates:
full_path = os.path.join(model_dir, file_name)
if os.path.isfile(full_path):
return full_path
return None
def _load_model(model_dir: str):
global MODEL, MODEL_PATH
if MODEL is not None:
return MODEL
if not TENSORFLOW_AVAILABLE:
raise RuntimeError("TensorFlow tidak tersedia pada environment Python ini.")
model_file = _find_model_file(model_dir)
if model_file is None:
raise RuntimeError(
"Model tidak ditemukan. Pastikan salah satu file ini ada: "
"rice_leaf_disease_model.keras, rice_leaf_disease_model.h5, rice_leaf_disease_model.json"
)
MODEL_PATH = model_file
if model_file.endswith(".keras") or model_file.endswith(".h5"):
MODEL = keras.models.load_model(model_file)
return MODEL
with open(model_file, "r", encoding="utf-8") as f:
model_json = f.read()
MODEL = keras.models.model_from_json(model_json)
weights_base = model_file.replace(".json", "")
weight_candidates = [
f"{weights_base}.h5",
f"{weights_base}_weights.h5",
]
for weights_file in weight_candidates:
if os.path.isfile(weights_file):
MODEL.load_weights(weights_file)
return MODEL
raise RuntimeError("Model JSON ditemukan, tetapi file weights tidak ditemukan.")
def _preprocess_image(image_bytes: bytes):
image = Image.open(BytesIO(image_bytes))
if image.mode != "RGB":
image = image.convert("RGB")
try:
resample_filter = Image.Resampling.LANCZOS
except AttributeError:
resample_filter = Image.LANCZOS
image = image.resize(IMG_SIZE, resample_filter)
img_array = np.array(image, dtype="float32") / 255.0
img_array = np.expand_dims(img_array, axis=0)
return img_array
def _predict(image_bytes: bytes, model_dir: str) -> dict:
model = _load_model(model_dir)
img_array = _preprocess_image(image_bytes)
predictions = model.predict(img_array, verbose=0)
predicted_idx = int(np.argmax(predictions[0]))
predicted_class = CLASS_NAMES[predicted_idx]
confidence = float(predictions[0][predicted_idx])
all_predictions = {
class_name: float(predictions[0][idx])
for idx, class_name in enumerate(CLASS_NAMES)
}
return {
"success": True,
"predicted_class": predicted_class,
"confidence": confidence,
"all_predictions": all_predictions,
"model_path": MODEL_PATH,
}
def action_classify(model_dir: str) -> None:
payload = _read_json_input()
image_base64 = payload.get("image")
if not image_base64 or not isinstance(image_base64, str):
_emit({"success": False, "message": "Field 'image' (base64) diperlukan"}, 0)
try:
image_bytes = base64.b64decode(image_base64)
result = _predict(image_bytes, model_dir)
_emit(result, 0)
except Exception as e:
_emit({"success": False, "message": str(e)}, 0)
def action_classify_from_url(model_dir: str) -> None:
payload = _read_json_input()
image_url = payload.get("image_url")
if not image_url or not isinstance(image_url, str):
_emit({"success": False, "message": "Field 'image_url' diperlukan"}, 0)
try:
response = requests.get(image_url, timeout=10)
if response.status_code != 200:
_emit({"success": False, "message": "Gagal download image dari URL"}, 0)
result = _predict(response.content, model_dir)
result["url"] = image_url
_emit(result, 0)
except Exception as e:
_emit({"success": False, "message": str(e)}, 0)
def action_health(model_dir: str) -> None:
if not TENSORFLOW_AVAILABLE:
message = "TensorFlow tidak tersedia"
if TENSORFLOW_IMPORT_ERROR:
message = f"TensorFlow tidak tersedia: {TENSORFLOW_IMPORT_ERROR}"
_emit(
{
"status": "error",
"message": message,
"model_loaded": False,
"python_executable": sys.executable,
"classes": CLASS_NAMES,
},
0,
)
try:
model = _load_model(model_dir)
_emit(
{
"status": "ok",
"message": "Model siap digunakan",
"model_loaded": True,
"model_path": MODEL_PATH,
"python_executable": sys.executable,
"classes": CLASS_NAMES,
"input_shape": str(model.input_shape),
},
0,
)
except Exception as e:
_emit(
{
"status": "error",
"message": str(e),
"model_loaded": False,
"python_executable": sys.executable,
"classes": CLASS_NAMES,
},
0,
)
def action_info(model_dir: str) -> None:
if not TENSORFLOW_AVAILABLE:
message = "TensorFlow tidak tersedia"
if TENSORFLOW_IMPORT_ERROR:
message = f"TensorFlow tidak tersedia: {TENSORFLOW_IMPORT_ERROR}"
_emit(
{
"model_loaded": False,
"message": message,
"python_executable": sys.executable,
"classes": CLASS_NAMES,
"number_of_classes": len(CLASS_NAMES),
},
0,
)
try:
model = _load_model(model_dir)
_emit(
{
"model_loaded": True,
"model_path": MODEL_PATH,
"python_executable": sys.executable,
"classes": CLASS_NAMES,
"number_of_classes": len(CLASS_NAMES),
"input_shape": str(model.input_shape),
"number_of_layers": len(model.layers),
"total_parameters": int(model.count_params()),
},
0,
)
except Exception as e:
_emit(
{
"model_loaded": False,
"message": str(e),
"python_executable": sys.executable,
"classes": CLASS_NAMES,
"number_of_classes": len(CLASS_NAMES),
},
0,
)
def main() -> None:
parser = argparse.ArgumentParser(description="Rice leaf disease inference CLI")
parser.add_argument(
"action",
choices=["classify", "classify-from-url", "health", "info"],
)
parser.add_argument(
"--model-dir",
required=True,
help="Direktori tempat file model berada",
)
args = parser.parse_args()
if args.action == "classify":
action_classify(args.model_dir)
if args.action == "classify-from-url":
action_classify_from_url(args.model_dir)
if args.action == "health":
action_health(args.model_dir)
if args.action == "info":
action_info(args.model_dir)
if __name__ == "__main__":
main()

View File

@ -0,0 +1,6 @@
{
"floatx": "float32",
"epsilon": 1e-07,
"backend": "tensorflow",
"image_data_format": "channels_last"
}