AtthalaricNero commited on
Commit
41de6ea
·
1 Parent(s): 45e641c
Files changed (6) hide show
  1. Dockerfile +19 -0
  2. app.py +142 -0
  3. pca_transformer.pkl +3 -0
  4. requirements.txt +8 -0
  5. svm_fruit_model.pkl +3 -0
  6. templates/index.html +402 -0
Dockerfile ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Gunakan Python 3.9 Slim
2
+ FROM python:3.9-slim
3
+
4
+ WORKDIR /app
5
+
6
+ RUN apt-get update && apt-get install -y \
7
+ libgl1-mesa-glx \
8
+ libglib2.0-0 \
9
+ && rm -rf /var/lib/apt/lists/*
10
+
11
+ COPY requirements.txt .
12
+ RUN pip install --no-cache-dir --upgrade pip && \
13
+ pip install --no-cache-dir -r requirements.txt
14
+
15
+ COPY . .
16
+
17
+ EXPOSE 7860
18
+
19
+ CMD ["gunicorn", "-b", "0.0.0.0:7860", "app:app", "--timeout", "120"]
app.py ADDED
@@ -0,0 +1,142 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+ import cv2
3
+ import joblib
4
+ import base64
5
+ import io
6
+ from flask import Flask, request, render_template
7
+ from PIL import Image
8
+ from skimage.feature import local_binary_pattern
9
+
10
+ app = Flask(__name__)
11
+
12
+ try:
13
+ model = joblib.load("svm_fruit_model.pkl")
14
+ pca = joblib.load("pca_transformer.pkl")
15
+ except Exception as e:
16
+ print(f"Error loading model: {e}")
17
+
18
+ CLASS_NAMES = [
19
+ "Avocado",
20
+ "Avocado ripe",
21
+ "Banana Lady",
22
+ "Banana Red",
23
+ "Banana Yellow",
24
+ "Carambula",
25
+ "Cherimoya",
26
+ "Dates",
27
+ "Fig",
28
+ "Guava",
29
+ "Kaki",
30
+ "Kiwi",
31
+ "Lychee",
32
+ "Mango",
33
+ "Mango Red",
34
+ "Mangostan",
35
+ "Papaya",
36
+ "Pineapple",
37
+ "Pineapple Mini",
38
+ "Pomegranate",
39
+ "Quince",
40
+ "Rambutan",
41
+ "Salak",
42
+ ]
43
+
44
+
45
+ def extract_color_histogram(img, bins=(8, 8, 8)):
46
+ hist = cv2.calcHist([img], [0, 1, 2], None, bins, [0, 256, 0, 256, 0, 256])
47
+ hist = cv2.normalize(hist, hist).flatten()
48
+
49
+ return hist
50
+
51
+
52
+ def extract_lbp_features(gray_img, P=8, R=1, method="uniform"):
53
+ lbp = local_binary_pattern(gray_img, P, R, method)
54
+ hist, _ = np.histogram(lbp.ravel(), bins=np.arange(0, P + 3), range=(0, P + 2))
55
+ hist = hist.astype("float")
56
+ hist /= hist.sum() + 1e-6
57
+ return hist
58
+
59
+
60
+ def preprocessing_pipeline(pil_img):
61
+ img = np.array(pil_img)
62
+
63
+ # ubah format dari RGBA menjadi RGB
64
+ if img.shape[-1] == 4:
65
+ img = img[:, :, :3]
66
+
67
+ img_float = img.astype(np.float32) / 255.0
68
+ img_uint8 = (img_float * 255).astype(np.uint8)
69
+
70
+ feat_color = extract_color_histogram(img_uint8)
71
+ img_gray = cv2.cvtColor(img_uint8, cv2.COLOR_RGB2GRAY)
72
+ feat_lbp = extract_lbp_features(img_gray)
73
+
74
+ combined = np.hstack([feat_color, feat_lbp])
75
+ combined = combined.reshape(1, -1)
76
+
77
+ final_features = pca.transform(combined)
78
+
79
+ return final_features
80
+
81
+
82
+ @app.route("/", methods=["GET", "POST"])
83
+ def index():
84
+ prediction_text = None
85
+ img_data = None
86
+ confidence = None
87
+ top_3_predictions = None
88
+
89
+ if request.method == "POST":
90
+ if "file" not in request.files:
91
+ return render_template("index.html", msg="Tidak ada file")
92
+
93
+ file = request.files["file"]
94
+
95
+ if file.filename == "":
96
+ return render_template("index.html", msg="Nama file kosong")
97
+
98
+ if file:
99
+ try:
100
+ image = Image.open(file.stream)
101
+ img_io = io.BytesIO()
102
+ image.save(img_io, "PNG")
103
+ encoded_img = base64.b64encode(img_io.getvalue()).decode("ascii")
104
+ img_data = f"data:image/png;base64, {encoded_img}"
105
+
106
+
107
+ features = preprocessing_pipeline(image)
108
+
109
+ # Prediksi dengan probabilitas
110
+ pred_index = model.predict(features)[0]
111
+ prediction_text = CLASS_NAMES[int(pred_index)]
112
+
113
+ # Dapatkan probabilitas untuk semua kelas
114
+ if hasattr(model, 'predict_proba'):
115
+ probabilities = model.predict_proba(features)[0]
116
+ confidence = float(probabilities[int(pred_index)]) * 100
117
+
118
+ # Dapatkan top 3 prediksi
119
+ top_3_indices = probabilities.argsort()[-3:][::-1]
120
+ top_3_predictions = [
121
+ {
122
+ 'name': CLASS_NAMES[idx],
123
+ 'probability': float(probabilities[idx]) * 100
124
+ }
125
+ for idx in top_3_indices
126
+ ]
127
+ else:
128
+ # Jika model tidak support predict_proba
129
+ confidence = None
130
+ top_3_predictions = None
131
+
132
+ except Exception as e:
133
+ prediction_text = f"Error: {str(e)}"
134
+ confidence = None
135
+ top_3_predictions = None
136
+
137
+ return render_template("index.html", prediction=prediction_text, img_data=img_data,
138
+ confidence=confidence, top_predictions=top_3_predictions)
139
+
140
+
141
+ if __name__ == "__main__":
142
+ app.run(debug=True, port=7860)
pca_transformer.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:d88792827adb898f287ad13c1aaf19f9144b6d3ca38c719fe7288d677852ce9b
3
+ size 122775
requirements.txt ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ flask
2
+ gunicorn
3
+ numpy
4
+ joblib
5
+ scikit-image
6
+ scikit-learn
7
+ pillow
8
+ opencv-python-headless
svm_fruit_model.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:9c8b141960eb9c6579a6ada492c2dd212af4b317d952965c3ad198e41a4c0b9a
3
+ size 332331
templates/index.html ADDED
@@ -0,0 +1,402 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="id">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>AI Fruit Classifier</title>
7
+
8
+ <!-- Bootstrap 5 CSS -->
9
+ <link
10
+ href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css"
11
+ rel="stylesheet"
12
+ />
13
+ <!-- Google Fonts (Poppins) -->
14
+ <link
15
+ href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600&display=swap"
16
+ rel="stylesheet"
17
+ />
18
+ <!-- Bootstrap Icons -->
19
+ <link
20
+ rel="stylesheet"
21
+ href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.5/font/bootstrap-icons.css"
22
+ />
23
+
24
+ <style>
25
+ body {
26
+ font-family: "Poppins", sans-serif;
27
+ background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
28
+ min-height: 100vh;
29
+ display: flex;
30
+ align-items: center;
31
+ padding: 20px 0;
32
+ }
33
+
34
+ .main-card {
35
+ border: none;
36
+ border-radius: 24px;
37
+ box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
38
+ background: #ffffff;
39
+ overflow: hidden;
40
+ }
41
+
42
+ .card-header-custom {
43
+ background: transparent;
44
+ padding: 30px 30px 10px 30px;
45
+ border-bottom: none;
46
+ text-align: center;
47
+ }
48
+
49
+ .card-header-custom h4 {
50
+ font-weight: 600;
51
+ color: #2d3436;
52
+ margin-bottom: 5px;
53
+ }
54
+
55
+ .card-header-custom p {
56
+ color: #636e72;
57
+ font-size: 0.9rem;
58
+ }
59
+
60
+ .card-body {
61
+ padding: 30px;
62
+ }
63
+
64
+ .upload-box {
65
+ border: 2px dashed #dfe6e9;
66
+ border-radius: 16px;
67
+ padding: 30px;
68
+ text-align: center;
69
+ transition: all 0.3s ease;
70
+ background-color: #fdfdfd;
71
+ cursor: pointer;
72
+ position: relative;
73
+ }
74
+
75
+ .upload-box:hover {
76
+ border-color: #74b9ff;
77
+ background-color: #f0f8ff;
78
+ }
79
+
80
+ .form-control[type="file"] {
81
+ position: absolute;
82
+ top: 0;
83
+ left: 0;
84
+ width: 100%;
85
+ height: 100%;
86
+ opacity: 0;
87
+ cursor: pointer;
88
+ }
89
+
90
+ .btn-predict {
91
+ background: linear-gradient(45deg, #6c5ce7, #a29bfe);
92
+ border: none;
93
+ border-radius: 12px;
94
+ padding: 12px 20px;
95
+ font-weight: 600;
96
+ letter-spacing: 0.5px;
97
+ transition: transform 0.2s;
98
+ box-shadow: 0 4px 15px rgba(108, 92, 231, 0.3);
99
+ }
100
+
101
+ .btn-predict:hover {
102
+ transform: translateY(-2px);
103
+ box-shadow: 0 6px 20px rgba(108, 92, 231, 0.4);
104
+ background: linear-gradient(45deg, #5f4dd0, #9189f0);
105
+ }
106
+
107
+ .result-container {
108
+ background-color: #f8f9fa;
109
+ border-radius: 16px;
110
+ padding: 20px;
111
+ margin-top: 30px;
112
+ animation: fadeIn 0.5s ease-in-out;
113
+ }
114
+
115
+ .prediction-badge {
116
+ background-color: #d1fae5;
117
+ color: #065f46;
118
+ padding: 8px 16px;
119
+ border-radius: 50px;
120
+ font-weight: 600;
121
+ font-size: 0.9rem;
122
+ display: inline-block;
123
+ margin-bottom: 10px;
124
+ }
125
+
126
+ .prediction-text {
127
+ font-size: 1.8rem;
128
+ font-weight: 700;
129
+ color: #2d3436;
130
+ margin: 0;
131
+ }
132
+
133
+ .confidence-container {
134
+ margin-top: 20px;
135
+ padding: 15px;
136
+ background-color: #ffffff;
137
+ border-radius: 12px;
138
+ border: 1px solid #e1e8ed;
139
+ }
140
+
141
+ .confidence-bar {
142
+ height: 8px;
143
+ border-radius: 10px;
144
+ background-color: #e9ecef;
145
+ overflow: hidden;
146
+ margin-top: 8px;
147
+ }
148
+
149
+ .confidence-fill {
150
+ height: 100%;
151
+ border-radius: 10px;
152
+ transition: width 0.8s ease-in-out;
153
+ background: linear-gradient(90deg, #10b981, #34d399);
154
+ }
155
+
156
+ .top-predictions {
157
+ margin-top: 15px;
158
+ }
159
+
160
+ .prediction-item {
161
+ display: flex;
162
+ justify-content: space-between;
163
+ align-items: center;
164
+ padding: 10px;
165
+ margin-bottom: 8px;
166
+ background-color: #f8f9fa;
167
+ border-radius: 8px;
168
+ font-size: 0.9rem;
169
+ }
170
+
171
+ .prediction-item .name {
172
+ font-weight: 500;
173
+ color: #2d3436;
174
+ }
175
+
176
+ .prediction-item .percentage {
177
+ font-weight: 600;
178
+ color: #6c5ce7;
179
+ }
180
+
181
+ .prediction-item .bar-small {
182
+ flex: 1;
183
+ height: 6px;
184
+ background-color: #e9ecef;
185
+ border-radius: 10px;
186
+ margin: 0 10px;
187
+ overflow: hidden;
188
+ }
189
+
190
+ .prediction-item .bar-fill {
191
+ height: 100%;
192
+ background: linear-gradient(90deg, #a29bfe, #6c5ce7);
193
+ border-radius: 10px;
194
+ }
195
+
196
+ .img-preview {
197
+ border-radius: 16px;
198
+ box-shadow: 0 8px 20px rgba(0, 0, 0, 0.1);
199
+ max-height: 250px;
200
+ object-fit: cover;
201
+ transition: transform 0.3s;
202
+ }
203
+
204
+ .img-preview:hover {
205
+ transform: scale(1.02);
206
+ }
207
+
208
+ @keyframes fadeIn {
209
+ from {
210
+ opacity: 0;
211
+ transform: translateY(10px);
212
+ }
213
+ to {
214
+ opacity: 1;
215
+ transform: translateY(0);
216
+ }
217
+ }
218
+ </style>
219
+ </head>
220
+ <body>
221
+ <div class="container">
222
+ <div class="row justify-content-center">
223
+ <div class="col-md-5">
224
+ <div class="card main-card">
225
+ <div class="card-header-custom">
226
+ <div class="mb-3">
227
+ <i
228
+ class="bi bi-flower1"
229
+ style="font-size: 3rem; color: #6c5ce7"
230
+ ></i>
231
+ </div>
232
+ <h4>Fruit Classifier</h4>
233
+ <p>Upload gambar buah untuk dideteksi oleh AI</p>
234
+ </div>
235
+
236
+ <div class="card-body">
237
+ <form method="post" enctype="multipart/form-data" id="uploadForm">
238
+ <div class="mb-4">
239
+ <div class="upload-box" id="uploadBox">
240
+ <i
241
+ class="bi bi-cloud-arrow-up"
242
+ style="font-size: 2rem; color: #b2bec3"
243
+ ></i>
244
+ <p class="mb-0 mt-2 text-muted">
245
+ Klik atau drag gambar di sini
246
+ </p>
247
+ <input
248
+ class="form-control"
249
+ type="file"
250
+ name="file"
251
+ id="fileInput"
252
+ accept="image/*"
253
+ required
254
+ />
255
+ </div>
256
+ <div
257
+ id="fileNameDisplay"
258
+ class="text-center mt-2 text-muted small"
259
+ ></div>
260
+ </div>
261
+ <button
262
+ type="submit"
263
+ class="btn btn-primary btn-predict w-100 text-white"
264
+ id="submitBtn"
265
+ >
266
+ <i class="bi bi-magic me-2"></i> Prediksi Gambar
267
+ </button>
268
+ </form>
269
+
270
+ {% if msg %}
271
+ <div
272
+ class="alert alert-warning mt-4 rounded-3 border-0 shadow-sm"
273
+ >
274
+ <i class="bi bi-exclamation-triangle me-2"></i> {{ msg }}
275
+ </div>
276
+ {% endif %}
277
+
278
+ {% if img_data and prediction %}
279
+ <div class="result-container text-center">
280
+ <div class="mb-3">
281
+ <img src="{{ img_data }}" class="img-fluid img-preview" alt="Uploaded fruit" />
282
+ </div>
283
+
284
+ <div class="prediction-badge">
285
+ <i class="bi bi-check-circle-fill me-1"></i> Hasil Prediksi
286
+ </div>
287
+ <h2 class="prediction-text">{{ prediction }}</h2>
288
+
289
+ {% if confidence %}
290
+ <div class="confidence-container">
291
+ <div class="d-flex justify-content-between align-items-center">
292
+ <span style="font-weight: 600; color: #636e72;">
293
+ <i class="bi bi-graph-up me-1"></i> Confidence Score
294
+ </span>
295
+ <span style="font-size: 1.3rem; font-weight: 700; color: #10b981;">
296
+ {{ "%.2f" | format(confidence) }}%
297
+ </span>
298
+ </div>
299
+ <div class="confidence-bar">
300
+ <div class="confidence-fill" style="width: {{ confidence }}%"></div>
301
+ </div>
302
+ </div>
303
+
304
+ {% if top_predictions %}
305
+ <div class="top-predictions text-start">
306
+ <h6 style="font-weight: 600; color: #636e72; margin-bottom: 12px;">
307
+ <i class="bi bi-trophy me-1"></i> Top 3 Prediksi
308
+ </h6>
309
+ {% for pred in top_predictions %}
310
+ <div class="prediction-item">
311
+ <span class="name">{{ loop.index }}. {{ pred.name }}</span>
312
+ <div class="bar-small">
313
+ <div class="bar-fill" style="width: {{ pred.probability }}%"></div>
314
+ </div>
315
+ <span class="percentage">{{ "%.1f" | format(pred.probability) }}%</span>
316
+ </div>
317
+ {% endfor %}
318
+ </div>
319
+ {% endif %}
320
+ {% endif %}
321
+ </div>
322
+ {% endif %}
323
+ </div>
324
+ </div>
325
+
326
+ <div class="text-center mt-4 text-muted small">
327
+ &copy; 2025 Fruit Classification Project
328
+ </div>
329
+ </div>
330
+ </div>
331
+ </div>
332
+
333
+ <script>
334
+ // Ambil elemen
335
+ let fileInput = document.getElementById('fileInput');
336
+ const uploadBox = document.getElementById('uploadBox');
337
+ const fileNameDisplay = document.getElementById('fileNameDisplay');
338
+ const uploadForm = document.getElementById('uploadForm');
339
+ const submitBtn = document.getElementById('submitBtn');
340
+
341
+ // Simpan file yang dipilih
342
+ let selectedFile = null;
343
+
344
+ // Fungsi untuk handle perubahan file
345
+ function handleFileChange(e) {
346
+ const input = e.target;
347
+ if (input.files && input.files[0]) {
348
+ selectedFile = input.files[0];
349
+ const fileName = selectedFile.name;
350
+
351
+ // Tampilkan nama file
352
+ fileNameDisplay.innerHTML = `<i class="bi bi-check-circle-fill text-success me-1"></i> File terpilih: <strong>${fileName}</strong>`;
353
+
354
+ // Buat preview gambar
355
+ const reader = new FileReader();
356
+ reader.onload = function(event) {
357
+ // Ganti konten upload box dengan preview gambar
358
+ uploadBox.innerHTML = `
359
+ <div style="position: relative;">
360
+ <img src="${event.target.result}" style="max-width: 100%; max-height: 200px; border-radius: 12px; object-fit: contain;" alt="Preview">
361
+ </div>
362
+ <p class="mb-0 mt-3 text-success">
363
+ <i class="bi bi-check-circle-fill me-1"></i>
364
+ <strong>${fileName}</strong>
365
+ </p>
366
+ <p class="mb-0 mt-2 text-muted small">Klik untuk mengganti gambar</p>
367
+ <input class="form-control" type="file" name="file" id="fileInput" accept="image/*">
368
+ `;
369
+
370
+ // Re-attach event listener untuk input file yang baru
371
+ fileInput = document.getElementById('fileInput');
372
+ fileInput.addEventListener('change', handleFileChange);
373
+
374
+ // Transfer file ke input baru menggunakan DataTransfer
375
+ const dataTransfer = new DataTransfer();
376
+ dataTransfer.items.add(selectedFile);
377
+ fileInput.files = dataTransfer.files;
378
+ };
379
+ reader.readAsDataURL(selectedFile);
380
+ }
381
+ }
382
+
383
+ // Event listener untuk file input pertama kali
384
+ fileInput.addEventListener('change', handleFileChange);
385
+
386
+ // Event listener untuk form submit
387
+ uploadForm.addEventListener('submit', function(e) {
388
+ // Cek apakah ada file yang dipilih
389
+ const currentFileInput = document.getElementById('fileInput');
390
+ if (!currentFileInput.files || currentFileInput.files.length === 0) {
391
+ e.preventDefault();
392
+ alert('Silakan pilih file gambar terlebih dahulu!');
393
+ return;
394
+ }
395
+
396
+ // Ubah tampilan tombol
397
+ submitBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Sedang menganalisis...';
398
+ submitBtn.disabled = true;
399
+ });
400
+ </script>
401
+ </body>
402
+ </html>