ganti api ke laravel
This commit is contained in:
parent
0372164658
commit
8af733ff12
262
api_client.py
262
api_client.py
|
|
@ -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)
|
||||
|
|
@ -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",
|
||||
" 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",
|
||||
" 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",
|
||||
" # Load images with progress bar\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",
|
||||
" 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\"Validation set: {X_val.shape[0]} images\")\n",
|
||||
"print(f\"Test set: {X_test.shape[0]} images\")\n",
|
||||
"print(\"-\" * 50)"
|
||||
"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",
|
||||
"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",
|
||||
" # 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",
|
||||
" base_model,\n",
|
||||
" layers.GlobalAveragePooling2D(),\n",
|
||||
" \n",
|
||||
" # Dense layers\n",
|
||||
" layers.Dense(512, activation='relu'),\n",
|
||||
" layers.Dropout(0.4),\n",
|
||||
" layers.Dense(128, activation='relu', kernel_regularizer=regularizers.l2(1e-4)),\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.Dropout(0.4),\n",
|
||||
" layers.Dense(len(classes), activation='softmax')\n",
|
||||
"])\n",
|
||||
"\n",
|
||||
" return model\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,9 +975,10 @@
|
|||
"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",
|
||||
" self.total_epochs = total_epochs\n",
|
||||
"\n",
|
||||
" def on_train_begin(self, logs=None):\n",
|
||||
" self.start_time = time.time()\n",
|
||||
|
|
@ -981,8 +990,8 @@
|
|||
" 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",
|
||||
" 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",
|
||||
|
|
@ -990,8 +999,8 @@
|
|||
" 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",
|
||||
" 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",
|
||||
|
|
@ -1002,43 +1011,90 @@
|
|||
" 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,
|
||||
|
|
|
|||
|
|
@ -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
11
setup.bat
11
setup.bat
|
|
@ -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
|
||||
|
|
|
|||
15
setup.sh
15
setup.sh
|
|
@ -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 ""
|
||||
|
|
|
|||
51
test_api.py
51
test_api.py
|
|
@ -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")
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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}"
|
||||
|
|
|
|||
|
|
@ -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,27 +34,27 @@ 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
|
||||
$savedToDatabase = true;
|
||||
$persistenceWarning = null;
|
||||
|
||||
try {
|
||||
Classification::create([
|
||||
'filename' => $file->getClientOriginalName(),
|
||||
'predicted_class' => $result['predicted_class'],
|
||||
|
|
@ -60,6 +64,14 @@ public function classify(Request $request)
|
|||
'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);
|
||||
|
|
@ -109,26 +123,28 @@ 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
|
||||
$savedToDatabase = true;
|
||||
$persistenceWarning = null;
|
||||
|
||||
try {
|
||||
Classification::create([
|
||||
'image_path' => $storagePath,
|
||||
'filename' => $file->getClientOriginalName(),
|
||||
|
|
@ -139,6 +155,15 @@ public function classifyAndSave(Request $request)
|
|||
'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();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -15,12 +15,17 @@
|
|||
// 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']);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"floatx": "float32",
|
||||
"epsilon": 1e-07,
|
||||
"backend": "tensorflow",
|
||||
"image_data_format": "channels_last"
|
||||
}
|
||||
Loading…
Reference in New Issue