upload data project klasifikasi tomat

This commit is contained in:
ramzdhani11 2026-04-15 01:26:26 +07:00
commit 4b8bd51b9c
490 changed files with 1972 additions and 0 deletions

185
API_README.md Normal file
View File

@ -0,0 +1,185 @@
# Tomat Classification API
API Flask untuk klasifikasi tingkat kematangan tomat menggunakan Random Forest dan Color Histogram RGB.
## Fitur
- **Preprocessing**: Resize gambar ke 256x256
- **Ekstraksi Fitur**: Color Histogram RGB (8x8x8 bins)
- **Klasifikasi**: Random Forest dengan 3 kelas
- **Format Output**: JSON dengan probabilitas dan confidence score
## Kelas Output
- `matang` - Tomat sudah matang
- `mentah` - Tomat masih mentah
- `setengah_matang` - Tomat setengah matang
## Endpoint
### 1. Health Check
```
GET /health
```
Response:
```json
{
"status": "healthy",
"model_loaded": true,
"service": "Tomat Classification API"
}
```
### 2. Prediksi
```
POST /predict
Content-Type: multipart/form-data
```
**Request**: Upload file gambar dengan key `image`
**Response**:
```json
{
"success": true,
"prediction": {
"class": "matang",
"confidence": 0.85,
"confidence_percentage": 85.0,
"probabilities": {
"matang": {"probability": 0.85, "percentage": 85.0},
"mentah": {"probability": 0.10, "percentage": 10.0},
"setengah_matang": {"probability": 0.05, "percentage": 5.0}
}
},
"metadata": {
"model_type": "RandomForest",
"features_used": 24,
"image_processed": "tomat.jpg"
}
}
```
### 3. Informasi Model
```
GET /info
```
Response:
```json
{
"success": true,
"model_info": {
"type": "RandomForestClassifier",
"classes": ["matang", "mentah", "setengah_matang"],
"n_features": 24,
"n_estimators": 100
},
"api_info": {
"version": "1.0.0",
"endpoints": {
"health": "/health",
"predict": "/predict (POST)",
"info": "/info"
},
"supported_formats": ["PNG", "JPG", "JPEG"],
"max_file_size": "16MB"
}
}
```
## Cara Menjalankan
### 1. Install Dependencies
```bash
pip install flask opencv-python numpy scikit-learn joblib
```
### 2. Training Model (Opsional)
```bash
python main.py
```
Ini akan membuat folder `models/` dengan file model yang sudah trained.
### 3. Jalankan API
```bash
python app.py
```
Server akan berjalan di: `http://127.0.0.1:5000`
## Cara Testing API
### Menggunakan curl
```bash
# Health check
curl http://127.0.0.1:5000/health
# Prediksi gambar
curl -X POST -F "image=@path/to/gambar.jpg" http://127.0.0.1:5000/predict
# Info model
curl http://127.0.0.1:5000/info
```
### Menggunakan Python
```python
import requests
# Health check
response = requests.get('http://127.0.0.1:5000/health')
print(response.json())
# Prediksi gambar
with open('gambar_tomat.jpg', 'rb') as f:
files = {'image': f}
response = requests.post('http://127.0.0.1:5000/predict', files=files)
print(response.json())
```
## Struktur File
```
data_tomat/
app.py # Flask API
main.py # Training model
models/ # Folder model
tomat_classifier.pkl # Model Random Forest
label_encoder.pkl # Label encoder
metadata.pkl # Metadata model
matang/ # Folder gambar matang
mentah/ # Folder gambar mentah
setengah_matang/ # Folder gambar setengah matang
```
## Error Handling
API mengembalikan error response dengan format:
```json
{
"success": false,
"error": "Error type",
"message": "Detailed error message"
}
```
### Common Errors
- **400**: File tidak ada, format tidak didukung, processing gagal
- **413**: File terlalu besar (>16MB)
- **500**: Internal server error
## Notes
- API akan otomatis membuat model dummy jika file model belum ada
- Untuk hasil prediksi yang akurat, jalankan `python main.py` terlebih dahulu
- File temporary akan otomatis dihapus setelah processing
- Gambar akan di-resize ke 256x256 sebelum ekstraksi fitur
## Dependencies
- Flask 2.0+
- OpenCV 4.0+
- NumPy 1.19+
- Scikit-learn 1.0+
- Joblib 1.0+

283
app.py Normal file
View File

@ -0,0 +1,283 @@
from flask import Flask, request, jsonify
import cv2
import numpy as np
import joblib
import os
import logging
import time
# Konfigurasi logging - hanya WARNING ke atas agar tidak lambat
logging.basicConfig(level=logging.WARNING)
logger = logging.getLogger(__name__)
app = Flask(__name__)
# Konfigurasi
app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 # 16MB max file size
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif'}
# Path ke model
MODEL_PATH = "model_tomat.pkl"
# Global variables untuk model (di-load sekali saja)
model = None
class_names = ['matang', 'mentah', 'setengah_matang']
def allowed_file(filename):
"""Check if file has allowed extension"""
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
def load_model():
"""Load model sekali saja saat startup"""
global model
try:
if os.path.exists(MODEL_PATH):
model = joblib.load(MODEL_PATH)
print("✅ Model berhasil dimuat")
return True
else:
print(f"❌ ERROR: File model tidak ditemukan: {MODEL_PATH}")
return False
except Exception as e:
print(f"❌ ERROR: Gagal memuat model: {e}")
return False
def extract_color_histogram(image, bins=(8, 8, 8)):
"""
Ekstraksi fitur Color Histogram HSV dari gambar
Langsung dari array BGR tanpa konversi ulang yang tidak perlu
"""
try:
# Konversi dari BGR ke HSV
image_hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)
# Hitung dan normalisasi histogram untuk setiap channel HSV
features = []
for i in range(3):
hist = cv2.calcHist([image_hsv], [i], None, [bins[i]], [0, 256])
hist = cv2.normalize(hist, hist).flatten()
features.append(hist)
return np.concatenate(features)
except Exception as e:
logger.error(f"Error ekstraksi fitur: {e}")
return None
def preprocess_image_from_memory(file_stream):
"""
Preprocessing gambar dari memory buffer.
Laravel sudah kirim gambar 256x256, cukup decode + ekstrak fitur saja.
"""
try:
# Baca file stream ke memory sekaligus
file_bytes = np.frombuffer(file_stream.read(), np.uint8)
# Decode gambar
image = cv2.imdecode(file_bytes, cv2.IMREAD_COLOR)
if image is None:
logger.error("Tidak dapat decode gambar dari memory")
return None
# Resize hanya jika gambar BUKAN 256x256
# (jika Laravel sudah resize, skip langkah ini)
h, w = image.shape[:2]
if h != 256 or w != 256:
image = cv2.resize(image, (256, 256), interpolation=cv2.INTER_LINEAR)
# Ekstraksi fitur histogram HSV
return extract_color_histogram(image)
except Exception as e:
logger.error(f"Error preprocessing from memory: {e}")
return None
@app.route('/', methods=['GET'])
def home():
"""Home endpoint"""
return jsonify({
'success': True,
'message': '🍅 Tomat Classification API is running!',
'endpoints': {
'health': '/health',
'predict': '/predict (POST)',
'info': '/info'
},
'model_loaded': model is not None,
'service': 'Tomat Classification API v1.0.0',
'classes': class_names
})
@app.route('/health', methods=['GET'])
def health_check():
"""Health check endpoint"""
return jsonify({
'success': True,
'status': 'healthy',
'model_loaded': model is not None,
'service': 'Tomat Classification API'
})
@app.route('/predict', methods=['POST'])
def predict():
"""
Endpoint prediksi kematangan tomat - Full memory processing, no disk I/O
Expected: multipart/form-data dengan file 'image'
"""
start_time = time.time()
try:
if model is None:
return jsonify({
'success': False,
'error': 'Model belum dimuat',
'message': 'Server tidak siap untuk prediksi'
}), 500
if 'image' not in request.files:
return jsonify({
'success': False,
'error': 'No file uploaded',
'message': 'Harap upload file gambar'
}), 400
file = request.files['image']
if file.filename == '':
return jsonify({
'success': False,
'error': 'No file selected',
'message': 'Harap pilih file gambar'
}), 400
if not allowed_file(file.filename):
return jsonify({
'success': False,
'error': 'Invalid file type',
'message': 'Hanya file PNG, JPG, JPEG yang diperbolehkan'
}), 400
# Preprocessing langsung dari memory (tanpa file temporary)
features = preprocess_image_from_memory(file)
if features is None:
return jsonify({
'success': False,
'error': 'Processing failed',
'message': 'Gagal memproses gambar dari memory'
}), 400
# Reshape + prediksi dalam satu langkah
features_reshaped = features.reshape(1, -1)
prediction = model.predict(features_reshaped)[0]
prediction_proba = model.predict_proba(features_reshaped)[0]
predicted_class = class_names[prediction]
confidence = float(np.max(prediction_proba))
# Format probabilitas
probabilities = {
class_names[i]: {
'probability': float(p),
'percentage': float(p * 100)
}
for i, p in enumerate(prediction_proba)
}
processing_time = time.time() - start_time
return jsonify({
'success': True,
'prediction': {
'class': predicted_class,
'confidence': confidence,
'confidence_percentage': confidence * 100,
'probabilities': probabilities
},
'metadata': {
'model_type': 'RandomForest',
'features_used': int(features.shape[0]),
'preprocessing': 'Memory Processing: Resize 256x256 + HSV Histogram (8x8x8)',
'processing_time_seconds': round(processing_time, 3),
'performance': 'Optimized (no temporary files)'
}
})
except Exception as e:
logger.error(f"Error dalam prediksi: {e}")
return jsonify({
'success': False,
'error': 'Internal server error',
'message': f'Terjadi kesalahan: {str(e)}'
}), 500
@app.route('/info', methods=['GET'])
def model_info():
"""Endpoint untuk informasi model"""
try:
if model is None:
return jsonify({'success': False, 'error': 'Model not loaded'}), 500
return jsonify({
'success': True,
'model_info': {
'type': type(model).__name__,
'classes': class_names,
'n_features': getattr(model, 'n_features_in_', None),
'n_estimators': getattr(model, 'n_estimators', None)
},
'api_info': {
'version': '1.0.0',
'supported_formats': ['PNG', 'JPG', 'JPEG'],
'max_file_size': '16MB',
'preprocessing': 'Memory Processing: Resize 256x256 + HSV Histogram (8x8x8)',
'performance': 'Optimized (no temporary files)'
}
})
except Exception as e:
return jsonify({'success': False, 'error': str(e)}), 500
@app.errorhandler(413)
def too_large(e):
return jsonify({'success': False, 'error': 'File too large', 'message': 'Ukuran file maksimal 16MB'}), 413
@app.errorhandler(404)
def not_found(e):
return jsonify({
'success': False,
'error': 'Endpoint not found',
'available_endpoints': ['/health', '/predict (POST)', '/info', '/']
}), 404
@app.errorhandler(500)
def internal_error(e):
return jsonify({'success': False, 'error': 'Internal server error'}), 500
if __name__ == '__main__':
print("=" * 60)
print("🍅 TOMAT CLASSIFICATION API")
print("=" * 60)
if load_model():
print("🚀 API siap digunakan")
print(f"📊 Model: {type(model).__name__}")
print(f"🎯 Kelas: {class_names}")
else:
print("❌ ERROR: Model gagal dimuat!")
exit(1)
print("\n📡 Endpoints:")
print(" GET / - Home/API Info")
print(" GET /health - Health check")
print(" POST /predict - Prediksi kematangan tomat")
print(" GET /info - Informasi model")
print("\n⚡ Processing: In-memory (no temporary files)")
print("🌐 Starting server on http://127.0.0.1:5000")
print("=" * 60)
# ✅ FIX UTAMA: debug=False agar tidak ada overhead auto-reload
# use_reloader=False mencegah model di-load 2x saat startup
app.run(host='127.0.0.1', port=5000, debug=False, use_reloader=False, threaded=True)

BIN
confusion_matrix.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

BIN
confusion_matrix_eval.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

BIN
confusion_matrix_tomat.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

View File

@ -0,0 +1,161 @@
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.metrics import confusion_matrix
def plot_confusion_matrix(y_true, y_pred, class_names, save_path='confusion_matrix.png'):
"""
Menampilkan confusion matrix dalam bentuk heatmap dan menyimpannya sebagai file PNG
Parameters:
y_true: label aktual (array-like)
y_pred: label prediksi (array-like)
class_names: list nama kelas (contoh: ['matang', 'mentah', 'setengah_matang'])
save_path: path untuk menyimpan file gambar (default: 'confusion_matrix.png')
Returns:
cm: confusion matrix array
"""
# Hitung confusion matrix
cm = confusion_matrix(y_true, y_pred)
# Buat figure dengan ukuran yang lebih besar
plt.figure(figsize=(10, 8))
# Buat heatmap dengan seaborn
sns.heatmap(cm, annot=True, fmt='d', cmap='Blues',
xticklabels=class_names, yticklabels=class_names,
cbar_kws={'label': 'Jumlah Sampel'},
square=True,
linewidths=0.5,
annot_kws={'size': 12, 'weight': 'bold'})
# Set title dan labels dengan formatting yang lebih baik
plt.title('Confusion Matrix - Klasifikasi Tingkat Kematangan Tomat',
fontsize=16, fontweight='bold', pad=20)
plt.xlabel('Kelas Prediksi', fontsize=12, fontweight='bold')
plt.ylabel('Kelas Aktual', fontsize=12, fontweight='bold')
# Rotate labels untuk better readability
plt.xticks(rotation=45, ha='right')
plt.yticks(rotation=0)
# Add text summary dengan akurasi
total_samples = len(y_true)
accuracy = np.mean(y_true == y_pred) * 100
plt.figtext(0.5, 0.02, f'Total Sampel: {total_samples} | Akurasi: {accuracy:.2f}%',
ha='center', fontsize=11, style='italic')
# Adjust layout untuk prevent label overlap
plt.tight_layout()
# Save sebagai PNG dengan high quality
plt.savefig(save_path, dpi=300, bbox_inches='tight', facecolor='white')
# Tampilkan plot
plt.show()
# Print summary
print(f"\n{'='*50}")
print("CONFUSION MATRIX VISUALIZATION")
print(f"{'='*50}")
print(f"File disimpan sebagai: {save_path}")
print(f"Ukuran gambar: 300 DPI")
print(f"Resolusi: Tinggi")
print(f"Format: PNG")
print(f"{'='*50}")
return cm
def plot_confusion_matrix_detailed(y_true, y_pred, class_names, save_path='confusion_matrix_detailed.png'):
"""
Menampilkan confusion matrix dengan informasi detail (precision, recall, f1-score)
Parameters:
y_true: label aktual
y_pred: label prediksi
class_names: list nama kelas
save_path: path untuk menyimpan file gambar
"""
from sklearn.metrics import classification_report, precision_score, recall_score, f1_score
# Hitung confusion matrix
cm = confusion_matrix(y_true, y_pred)
# Hitung metrics
precision = precision_score(y_true, y_pred, average=None)
recall = recall_score(y_true, y_pred, average=None)
f1 = f1_score(y_true, y_pred, average=None)
# Buat figure dengan 2x2 subplot
fig, ((ax1, ax2), (ax3, ax4)) = plt.subplots(2, 2, figsize=(15, 12))
# 1. Confusion Matrix
sns.heatmap(cm, annot=True, fmt='d', cmap='Blues',
xticklabels=class_names, yticklabels=class_names,
ax=ax1, cbar_kws={'label': 'Jumlah Sampel'})
ax1.set_title('Confusion Matrix', fontweight='bold')
ax1.set_xlabel('Kelas Prediksi')
ax1.set_ylabel('Kelas Aktual')
# 2. Precision per kelas
bars = ax2.bar(class_names, precision, color='skyblue', alpha=0.8)
ax2.set_title('Precision per Kelas', fontweight='bold')
ax2.set_ylabel('Precision')
ax2.set_ylim(0, 1)
for bar, value in zip(bars, precision):
ax2.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.01,
f'{value:.3f}', ha='center', va='bottom')
# 3. Recall per kelas
bars = ax3.bar(class_names, recall, color='lightgreen', alpha=0.8)
ax3.set_title('Recall per Kelas', fontweight='bold')
ax3.set_ylabel('Recall')
ax3.set_ylim(0, 1)
for bar, value in zip(bars, recall):
ax3.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.01,
f'{value:.3f}', ha='center', va='bottom')
# 4. F1-Score per kelas
bars = ax4.bar(class_names, f1, color='salmon', alpha=0.8)
ax4.set_title('F1-Score per Kelas', fontweight='bold')
ax4.set_ylabel('F1-Score')
ax4.set_ylim(0, 1)
for bar, value in zip(bars, f1):
ax4.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.01,
f'{value:.3f}', ha='center', va='bottom')
# Overall title
fig.suptitle('Confusion Matrix & Metrics Detail - Klasifikasi Tomat',
fontsize=16, fontweight='bold')
# Adjust layout
plt.tight_layout()
# Save dengan high quality
plt.savefig(save_path, dpi=300, bbox_inches='tight', facecolor='white')
plt.show()
# Print classification report
print(f"\nClassification Report:")
print(classification_report(y_true, y_pred, target_names=class_names))
print(f"\nDetailed confusion matrix disimpan sebagai: {save_path}")
return cm, precision, recall, f1
# Contoh penggunaan
if __name__ == "__main__":
# Contoh data
y_true = [0, 1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 2]
y_pred = [0, 1, 2, 0, 2, 2, 0, 1, 1, 0, 1, 2]
class_names = ['matang', 'mentah', 'setengah_matang']
# Plot confusion matrix sederhana
cm = plot_confusion_matrix(y_true, y_pred, class_names, 'example_confusion_matrix.png')
# Plot confusion matrix detail
cm_detail, precision, recall, f1 = plot_confusion_matrix_detailed(
y_true, y_pred, class_names, 'example_confusion_matrix_detailed.png'
)

15
create_dummy_model.py Normal file
View File

@ -0,0 +1,15 @@
import joblib
import numpy as np
from sklearn.ensemble import RandomForestClassifier
# Buat model Random Forest dummy
model = RandomForestClassifier(n_estimators=10, random_state=42)
# Training dengan data dummy (24 fitur untuk histogram RGB 8x8x8)
X_dummy = np.random.rand(100, 24)
y_dummy = np.random.choice([0, 1, 2], 100)
model.fit(X_dummy, y_dummy)
# Simpan model
joblib.dump(model, 'model_tomat.pkl')
print('Model dummy berhasil dibuat dan disimpan sebagai model_tomat.pkl')

22
create_model.py Normal file
View File

@ -0,0 +1,22 @@
import joblib
import numpy as np
from sklearn.ensemble import RandomForestClassifier
print("Membuat model Random Forest untuk testing...")
# Buat model Random Forest
model = RandomForestClassifier(n_estimators=10, random_state=42)
# Training dengan data dummy (24 fitur untuk histogram RGB 8x8x8)
X_dummy = np.random.rand(100, 24)
y_dummy = np.random.choice([0, 1, 2], 100)
model.fit(X_dummy, y_dummy)
# Simpan model
joblib.dump(model, 'model_tomat.pkl')
print("Model berhasil dibuat dan disimpan sebagai 'model_tomat.pkl'")
print(f"Tipe model: {type(model).__name__}")
print(f"Jumlah fitur: {model.n_features_in_}")
print(f"Jumlah kelas: {len(model.classes_)}")
print(f"Kelas: {model.classes_}")

414
evaluasi_model.ipynb Normal file

File diff suppressed because one or more lines are too long

305
main.py Normal file
View File

@ -0,0 +1,305 @@
import cv2
import numpy as np
import os
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import classification_report, confusion_matrix
import matplotlib.pyplot as plt
import seaborn as sns
import joblib
from sklearn.preprocessing import LabelEncoder
def extract_color_histogram(image, bins=(8, 8, 8)):
"""
Ekstraksi fitur Color Histogram HSV dari gambar
Parameters:
image: gambar dalam format BGR (OpenCV)
bins: jumlah bin untuk setiap channel HSV
Returns:
features: array fitur histogram yang telah dinormalisasi
"""
# Konversi dari BGR ke HSV
image_hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)
# Hitung dan normalisasi histogram untuk setiap channel HSV
features = []
for i in range(3):
hist = cv2.calcHist([image_hsv], [i], None, [bins[i]], [0, 256])
hist = cv2.normalize(hist, hist).flatten()
features.append(hist)
# Gabungkan semua histogram
features = np.concatenate(features)
return features
def plot_confusion_matrix(y_true, y_pred, class_names, save_path='confusion_matrix.png'):
"""
Menampilkan confusion matrix dalam bentuk heatmap dan menyimpannya sebagai file PNG
Parameters:
y_true: label aktual
y_pred: label prediksi
class_names: list nama kelas
save_path: path untuk menyimpan file gambar
"""
# Hitung confusion matrix
cm = confusion_matrix(y_true, y_pred)
# Buat figure
plt.figure(figsize=(10, 8))
# Buat heatmap dengan seaborn
sns.heatmap(cm, annot=True, fmt='d', cmap='Blues',
xticklabels=class_names, yticklabels=class_names,
cbar_kws={'label': 'Jumlah Sampel'},
square=True,
linewidths=0.5,
annot_kws={'size': 12, 'weight': 'bold'})
# Set title dan labels
plt.title('Confusion Matrix - Klasifikasi Tingkat Kematangan Tomat',
fontsize=16, fontweight='bold', pad=20)
plt.xlabel('Kelas Prediksi', fontsize=12, fontweight='bold')
plt.ylabel('Kelas Aktual', fontsize=12, fontweight='bold')
# Rotate labels untuk better readability
plt.xticks(rotation=45, ha='right')
plt.yticks(rotation=0)
# Add text summary
total_samples = len(y_true)
accuracy = np.mean(y_true == y_pred) * 100
plt.figtext(0.5, 0.02, f'Total Sampel: {total_samples} | Akurasi: {accuracy:.2f}%',
ha='center', fontsize=11, style='italic')
# Adjust layout
plt.tight_layout()
# Save sebagai PNG dengan high quality
plt.savefig(save_path, dpi=300, bbox_inches='tight', facecolor='white')
# Tampilkan plot
plt.show()
# Print summary
print(f"\nConfusion Matrix telah disimpan sebagai: {save_path}")
print(f"Ukuran gambar: 300 DPI")
return cm
def load_dataset(dataset_path):
"""
Membaca seluruh gambar dari folder dataset dan melakukan ekstraksi fitur
Parameters:
dataset_path: path ke folder dataset
Returns:
features: array fitur dari semua gambar
labels: array label dari semua gambar
class_names: nama kelas
"""
features = []
labels = []
class_names = []
# Dapatkan semua folder kelas
classes = [d for d in os.listdir(dataset_path)
if os.path.isdir(os.path.join(dataset_path, d))]
classes.sort() # Urutkan untuk konsistensi
print(f"Ditemukan {len(classes)} kelas: {classes}")
for class_idx, class_name in enumerate(classes):
class_path = os.path.join(dataset_path, class_name)
class_names.append(class_name)
# Dapatkan semua file gambar dalam folder kelas
image_files = [f for f in os.listdir(class_path)
if f.lower().endswith(('.png', '.jpg', '.jpeg'))]
print(f"Memproses kelas '{class_name}': {len(image_files)} gambar")
for image_file in image_files:
image_path = os.path.join(class_path, image_file)
try:
# Baca gambar
image = cv2.imread(image_path)
if image is None:
print(f"Warning: Tidak dapat membaca gambar {image_path}")
continue
# Ekstraksi fitur
feature = extract_color_histogram(image)
features.append(feature)
labels.append(class_idx)
except Exception as e:
print(f"Error memproses {image_path}: {e}")
continue
return np.array(features), np.array(labels), class_names
def main():
"""
Fungsi utama untuk menjalankan proses klasifikasi tingkat kematangan tomat
"""
print("=" * 60)
print("KLASIFIKASI TINGKAT KEMATANGAN TOMAT")
print("Menggunakan Random Forest dan Color Histogram HSV")
print("=" * 60)
# Path ke dataset
dataset_path = "."
# Cek apakah folder dataset ada
if not os.path.exists(dataset_path):
print(f"Error: Folder '{dataset_path}' tidak ditemukan!")
print("Pastikan folder dataset ada dengan struktur:")
print("./")
print(" matang/")
print(" mentah/")
print(" setengah_matang/")
return
# Load dataset dan ekstraksi fitur
print("\n1. MEMBACA DATASET DAN EKSTRAKSI FITUR")
print("-" * 40)
features, labels, class_names = load_dataset(dataset_path)
print(f"\nTotal gambar yang berhasil diproses: {len(features)}")
print(f"Dimensi fitur per gambar: {features.shape[1]}")
# Split dataset menjadi training dan testing (80:20)
print("\n2. SPLIT DATASET")
print("-" * 40)
X_train, X_test, y_train, y_test = train_test_split(
features, labels,
test_size=0.2,
random_state=42,
stratify=labels
)
# Tampilkan jumlah data training dan testing
print(f"Jumlah data training: {len(X_train)}")
print(f"Jumlah data testing: {len(X_test)}")
print(f"Rasio training:testing = {len(X_train)/(len(X_train)+len(X_test)):.1f}:{len(X_test)/(len(X_train)+len(X_test)):.1f}")
# Tampilkan distribusi kelas
print("\nDistribusi kelas pada data training:")
for i, class_name in enumerate(class_names):
count = np.sum(y_train == i)
print(f" {class_name}: {count} gambar")
print("\nDistribusi kelas pada data testing:")
for i, class_name in enumerate(class_names):
count = np.sum(y_test == i)
print(f" {class_name}: {count} gambar")
# Training model Random Forest
print("\n3. TRAINING MODEL RANDOM FOREST")
print("-" * 40)
# Inisialisasi model
rf_model = RandomForestClassifier(
n_estimators=100,
random_state=42,
max_depth=10
)
# Training model
rf_model.fit(X_train, y_train)
print("Model Random Forest telah selesai training!")
# Evaluasi model
print("\n4. EVALUASI MODEL")
print("-" * 40)
# Prediksi pada data testing
y_pred = rf_model.predict(X_test)
# Tampilkan classification report
print("\nClassification Report:")
print(classification_report(y_test, y_pred, target_names=class_names))
# Tampilkan confusion matrix dengan visualisasi yang lebih baik
print("\nConfusion Matrix:")
cm = plot_confusion_matrix(y_test, y_pred, class_names, 'confusion_matrix_tomat.png')
print(cm)
# Tampilkan akurasi
accuracy = np.mean(y_pred == y_test)
print(f"\nAkurasi model: {accuracy:.4f} ({accuracy*100:.2f}%)")
# Feature importance
print("\n5. FEATURE IMPORTANCE")
print("-" * 40)
# Dapatkan feature importance
importances = rf_model.feature_importances_
# Kelompokkan berdasarkan channel RGB
bins_per_channel = len(importances) // 3
r_importance = np.sum(importances[:bins_per_channel])
g_importance = np.sum(importances[bins_per_channel:2*bins_per_channel])
b_importance = np.sum(importances[2*bins_per_channel:])
print(f"Importance channel R: {r_importance:.4f} ({r_importance*100:.2f}%)")
print(f"Importance channel G: {g_importance:.4f} ({g_importance*100:.2f}%)")
print(f"Importance channel B: {b_importance:.4f} ({b_importance*100:.2f}%)")
# Simpan model
print("\n6. MENYIMPAN MODEL")
print("-" * 40)
# Buat folder models jika belum ada
models_folder = "models"
if not os.path.exists(models_folder):
os.makedirs(models_folder)
print(f"Folder '{models_folder}' dibuat")
# Buat label encoder
label_encoder = LabelEncoder()
label_encoder.fit(class_names)
# Simpan model
try:
model_file = os.path.join(models_folder, "tomat_classifier.pkl")
joblib.dump(rf_model, model_file)
print(f"Model berhasil disimpan: {model_file}")
# Simpan label encoder
encoder_file = os.path.join(models_folder, "label_encoder.pkl")
joblib.dump(label_encoder, encoder_file)
print(f"Label encoder berhasil disimpan: {encoder_file}")
# Simpan metadata
metadata = {
'model_type': 'RandomForestClassifier',
'n_estimators': 100,
'max_depth': 10,
'n_features': features.shape[1],
'classes': class_names,
'accuracy': accuracy,
'histogram_bins': (8, 8, 8)
}
metadata_file = os.path.join(models_folder, "metadata.pkl")
joblib.dump(metadata, metadata_file)
print(f"Metadata berhasil disimpan: {metadata_file}")
except Exception as e:
print(f"Error menyimpan model: {e}")
print("\n" + "=" * 60)
print("PROSES KLASIFIKASI SELESAI!")
print("=" * 60)
if __name__ == "__main__":
main()

BIN
matang/Matang1.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 482 KiB

BIN
matang/Matang10.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 534 KiB

BIN
matang/Matang100.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 766 KiB

BIN
matang/Matang101.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 799 KiB

BIN
matang/Matang102.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 835 KiB

BIN
matang/Matang103.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 833 KiB

BIN
matang/Matang104.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 885 KiB

BIN
matang/Matang105.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 808 KiB

BIN
matang/Matang106.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 883 KiB

BIN
matang/Matang107.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 908 KiB

BIN
matang/Matang108.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 863 KiB

BIN
matang/Matang109.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 792 KiB

BIN
matang/Matang11.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 563 KiB

BIN
matang/Matang110.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 856 KiB

BIN
matang/Matang111.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 898 KiB

BIN
matang/Matang112.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 838 KiB

BIN
matang/Matang113.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 802 KiB

BIN
matang/Matang114.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 787 KiB

BIN
matang/Matang115.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 866 KiB

BIN
matang/Matang116.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 KiB

BIN
matang/Matang117.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 848 KiB

BIN
matang/Matang118.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 836 KiB

BIN
matang/Matang119.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 786 KiB

BIN
matang/Matang12.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 700 KiB

BIN
matang/Matang120.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 842 KiB

BIN
matang/Matang121.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 764 KiB

BIN
matang/Matang122.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 857 KiB

BIN
matang/Matang123.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 838 KiB

BIN
matang/Matang124.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 802 KiB

BIN
matang/Matang125.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 827 KiB

BIN
matang/Matang126.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 785 KiB

BIN
matang/Matang127.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 792 KiB

BIN
matang/Matang128.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 769 KiB

BIN
matang/Matang129.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 764 KiB

BIN
matang/Matang13.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 471 KiB

BIN
matang/Matang130.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 848 KiB

BIN
matang/Matang131.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 809 KiB

BIN
matang/Matang132.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 635 KiB

BIN
matang/Matang133.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 636 KiB

BIN
matang/Matang134.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 525 KiB

BIN
matang/Matang135.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 573 KiB

BIN
matang/Matang136.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 534 KiB

BIN
matang/Matang137.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 554 KiB

BIN
matang/Matang138.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 534 KiB

BIN
matang/Matang139.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 570 KiB

BIN
matang/Matang14.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 348 KiB

BIN
matang/Matang140.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 692 KiB

BIN
matang/Matang141.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 644 KiB

BIN
matang/Matang142.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 681 KiB

BIN
matang/Matang143.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 551 KiB

BIN
matang/Matang144.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 578 KiB

BIN
matang/Matang145.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 465 KiB

BIN
matang/Matang146.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 465 KiB

BIN
matang/Matang147.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 564 KiB

BIN
matang/Matang148.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 588 KiB

BIN
matang/Matang149.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 559 KiB

BIN
matang/Matang15.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 444 KiB

BIN
matang/Matang150.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 383 KiB

BIN
matang/Matang151.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 519 KiB

BIN
matang/Matang152.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 525 KiB

BIN
matang/Matang153.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 606 KiB

BIN
matang/Matang154.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 481 KiB

BIN
matang/Matang155.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 600 KiB

BIN
matang/Matang156.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 498 KiB

BIN
matang/Matang157.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 603 KiB

BIN
matang/Matang158.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 448 KiB

BIN
matang/Matang159.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 449 KiB

BIN
matang/Matang16.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 356 KiB

BIN
matang/Matang160.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 508 KiB

BIN
matang/Matang161.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 509 KiB

BIN
matang/Matang162.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 503 KiB

BIN
matang/Matang163.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 640 KiB

BIN
matang/Matang164.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 584 KiB

BIN
matang/Matang165.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 417 KiB

BIN
matang/Matang166.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 495 KiB

BIN
matang/Matang167.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 460 KiB

BIN
matang/Matang168.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 423 KiB

BIN
matang/Matang169.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 922 KiB

BIN
matang/Matang17.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 438 KiB

BIN
matang/Matang18.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 412 KiB

BIN
matang/Matang19.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 390 KiB

BIN
matang/Matang2.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 451 KiB

BIN
matang/Matang20.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 376 KiB

BIN
matang/Matang21.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 439 KiB

BIN
matang/Matang22.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 451 KiB

BIN
matang/Matang23.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 420 KiB

BIN
matang/Matang24.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 459 KiB

BIN
matang/Matang25.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 410 KiB

BIN
matang/Matang26.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 418 KiB

BIN
matang/Matang27.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 352 KiB

Some files were not shown because too many files have changed in this diff Show More