kodetr commited on
Commit
3f5df17
Β·
verified Β·
1 Parent(s): 769e0d4

Update app-gradio.py

Browse files
Files changed (1) hide show
  1. app-gradio.py +506 -192
app-gradio.py CHANGED
@@ -3,196 +3,417 @@ import gradio as gr
3
  import numpy as np
4
  import os
5
  import warnings
 
 
 
6
  from PIL import Image
 
7
 
8
  warnings.filterwarnings("ignore")
9
 
10
- CUSTOM_CSS = """
11
- :root {
12
- color-scheme: light !important;
13
- }
14
- body, .gradio-container {
15
- background-color: #ffffff !important;
16
- color: #000000 !important;
17
- }
18
- .dark .gradio-container * {
19
- background-color: #ffffff !important;
20
- color: #000000 !important;
21
- }
22
- """
23
-
24
  # ============================================================
25
- # 1. LOAD MODEL
26
  # ============================================================
27
  print("=" * 60)
28
- print("LOADING MODEL")
29
  print("=" * 60)
30
 
31
- MODEL_PATH = "model.keras"
32
-
33
- try:
34
- best_model = tf.keras.models.load_model(
35
- MODEL_PATH,
36
- custom_objects={"quantization_config": None},
37
- compile=False
38
- )
39
- print("βœ… Model berhasil dimuat")
40
- except Exception as e:
41
- print("❌ Pemuatan model gagal, membuat model dummy:", e)
42
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
  from tensorflow.keras import layers, Model
44
  inputs = layers.Input(shape=(224, 224, 3))
45
  x = layers.GlobalAveragePooling2D()(inputs)
46
- dr_output = layers.Dense(5, name="dr_head")(x) # logits
47
- dme_output = layers.Dense(3, name="dme_head")(x) # logits
48
  best_model = Model(inputs, {"dr_head": dr_output, "dme_head": dme_output})
49
  best_model.compile(optimizer="adam", loss="categorical_crossentropy")
 
50
 
51
- best_model.summary()
 
 
 
 
52
 
53
  # ============================================================
54
  # 2. CONFIG
55
  # ============================================================
56
  IMG_SIZE = 224
 
 
57
 
58
  # ============================================================
59
- # 3. PREPROCESSING
60
  # ============================================================
61
- def preprocess_image(img_path):
62
- img = tf.io.read_file(img_path)
63
- img = tf.image.decode_jpeg(img, channels=3)
64
- img = tf.image.resize(img, (IMG_SIZE, IMG_SIZE))
65
- img = tf.cast(img, tf.float32) / 255.0
66
- return tf.expand_dims(img, 0)
 
 
 
 
 
 
 
 
67
 
68
  # ============================================================
69
- # 4. CLASS MAPPING
70
- # ============================================================
71
- DR_CLASSES = ["No DR", "Mild", "Moderate", "Severe", "Proliferative DR"]
72
- DME_CLASSES = ["No DME", "Low Risk", "High Risk"]
73
-
74
- # ============================================================
75
- # 5. SOFTMAX SAFETY
76
  # ============================================================
77
  def ensure_probability(x):
78
  x = np.asarray(x, dtype=np.float32)
 
79
  if x.min() < 0 or x.max() > 1.0 or abs(x.sum() - 1.0) > 1e-3:
80
  x = tf.nn.softmax(x).numpy()
81
  return x
82
 
83
  # ============================================================
84
- # 6. PREDICTION β†’ TABLE ONLY
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
85
  # ============================================================
86
- def predict_and_format_text(image_path):
87
- img_tensor = preprocess_image(image_path)
88
- preds = best_model.predict(img_tensor, verbose=0)
89
-
90
- # ---- handle output model ----
91
- if isinstance(preds, dict):
92
- dr_key, dme_key = None, None
93
- for k in preds.keys():
94
- lk = k.lower()
95
- if "dr" in lk:
96
- dr_key = k
97
- if "dme" in lk:
98
- dme_key = k
99
- dr_pred = preds[dr_key][0]
100
- dme_pred = preds[dme_key][0]
101
-
102
- elif isinstance(preds, (list, tuple)):
103
- dr_pred = preds[0][0]
104
- dme_pred = preds[1][0]
105
-
106
- elif isinstance(preds, np.ndarray):
107
- dr_pred = preds[0][:5]
108
- dme_pred = preds[0][5:]
109
-
110
- else:
111
- raise ValueError("Format output model tidak dikenali")
112
-
113
- # ---- softmax ----
114
- dr_pred = ensure_probability(dr_pred)
115
- dme_pred = ensure_probability(dme_pred)
116
-
117
- # ---- result ----
118
- dr_idx = int(np.argmax(dr_pred))
119
- dme_idx = int(np.argmax(dme_pred))
120
-
121
- dr_name = DR_CLASSES[dr_idx]
122
- dme_name = DME_CLASSES[dme_idx]
123
-
124
- dr_conf = dr_pred[dr_idx] * 100
125
- dme_conf = dme_pred[dme_idx] * 100
126
-
127
- # ======================================================
128
- # πŸ”₯ JUDUL UTAMA
129
- # ======================================================
130
- main_result = f"""
131
- <div style="text-align:center; font-size:30px; font-weight:800; margin-bottom:20px;">
132
- HASIL PREDIKSI
133
- </div>
134
- """
135
-
136
- # ======================================================
137
- # πŸ“Š TABEL HASIL
138
- # ======================================================
139
- table_result = f"""
140
- <table style="width:100%; border-collapse:collapse; font-size:16px;">
141
- <thead>
142
- <tr style="background-color:#f2f2f2;">
143
- <th style="border:1px solid #ccc; padding:8px;">Aspek</th>
144
- <th style="border:1px solid #ccc; padding:8px;">Klasifikasi</th>
145
- <th style="border:1px solid #ccc; padding:8px;">Confidence (%)</th>
146
- </tr>
147
- </thead>
148
- <tbody>
149
- <tr>
150
- <td style="border:1px solid #ccc; padding:8px;">Diabetic Retinopathy (DR)</td>
151
- <td style="border:1px solid #ccc; padding:8px;"><b>{dr_name}</b></td>
152
- <td style="border:1px solid #ccc; padding:8px;">{dr_conf:.2f}%</td>
153
- </tr>
154
- <tr>
155
- <td style="border:1px solid #ccc; padding:8px;">Diabetic Macular Edema (DME)</td>
156
- <td style="border:1px solid #ccc; padding:8px;"><b>{dme_name}</b></td>
157
- <td style="border:1px solid #ccc; padding:8px;">{dme_conf:.2f}%</td>
158
- </tr>
159
- </tbody>
160
- </table>
161
- """
162
-
163
- # ======================================================
164
- # 🩺 REKOMENDASI
165
- # ======================================================
166
- # Rekomendasi DR
167
- if dr_name in ["No DR"]:
168
- rec_dr = "Lanjutkan pola hidup sehat dan lakukan pemeriksaan mata rutin minimal 1 tahun sekali."
169
- elif dr_name in ["Mild", "Moderate"]:
170
- rec_dr = "Disarankan kontrol gula darah secara ketat dan pemeriksaan mata berkala setiap 6 bulan."
171
- else: # Severe / Proliferative
172
- rec_dr = "Disarankan segera konsultasi ke dokter spesialis mata untuk evaluasi dan penanganan lebih lanjut."
173
-
174
- # Rekomendasi DME
175
- if dme_name == "No DME":
176
- rec_dme = "Belum ditemukan tanda edema makula diabetik, lanjutkan pemantauan rutin."
177
- elif dme_name == "Low Risk":
178
- rec_dme = "Perlu observasi ketat dan pemeriksaan lanjutan untuk mencegah progresivitas."
179
- else: # High Risk
180
- rec_dme = "Disarankan segera mendapatkan evaluasi klinis dan terapi oleh dokter spesialis mata."
181
-
182
- recommendation = f"""
183
- <div style="margin-top:20px; padding:15px; border-left:5px solid #007bff; background-color:#f9f9f9;">
184
- <b>🩺 Rekomendasi Klinis:</b><br/><br/>
185
- <b>β€’ Diabetic Retinopathy (DR):</b><br/>
186
- {rec_dr}<br/><br/>
187
- <b>β€’ Diabetic Macular Edema (DME):</b><br/>
188
- {rec_dme}
189
- </div>
190
- """
191
-
192
- return main_result + table_result + recommendation
193
 
194
  # ============================================================
195
- # 7. MULTI TEST IMAGES
196
  # ============================================================
197
  TEST_IMAGES = [
198
  "IDRiD_001test.jpg",
@@ -209,50 +430,143 @@ TEST_IMAGES = [
209
 
210
  TEST_IMAGES = [[p] for p in TEST_IMAGES if os.path.exists(p)]
211
 
212
- # ============================================================
213
- # 8. GRADIO WRAPPER
214
- # ============================================================
215
- def gradio_predict(image):
216
- if image is None:
217
- return "❌ Silakan unggah gambar"
218
-
219
- temp_path = "uploaded.jpg"
220
- image.save(temp_path)
221
-
222
- return predict_and_format_text(temp_path)
223
 
224
  # ============================================================
225
- # 9. GRADIO UI
226
  # ============================================================
227
- with gr.Blocks(title="DR & DME Detection",
228
- css=CUSTOM_CSS
229
- ) as demo:
230
-
 
 
 
231
  gr.Markdown("""
232
- # 🩺 SISTEM DETEKSI RETINOPATHY DIABETIC DAN DME BERBASIS CITRA
 
 
 
 
 
233
  """)
234
-
235
- with gr.Row():
236
- with gr.Column():
237
- input_img = gr.Image(type="pil", label="Upload Gambar Retina")
238
- btn = gr.Button("πŸ” Prediksi")
239
-
240
- with gr.Column():
241
- out_text = gr.Markdown()
242
-
243
- gr.Markdown("### πŸ§ͺ Data Testing")
244
- gr.Examples(
245
- examples=TEST_IMAGES,
246
- inputs=input_img
247
- )
248
-
249
- btn.click(
250
- fn=gradio_predict,
251
- inputs=input_img,
252
- outputs=out_text
253
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
254
 
255
  # ============================================================
256
- # 10. LAUNCH
257
  # ============================================================
258
- demo.launch(share=True)
 
 
 
 
 
 
 
 
3
  import numpy as np
4
  import os
5
  import warnings
6
+ import io
7
+ import json
8
+ import base64
9
  from PIL import Image
10
+ import tempfile
11
 
12
  warnings.filterwarnings("ignore")
13
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
  # ============================================================
15
+ # 1. LOAD MODEL (with Hugging Face compatibility)
16
  # ============================================================
17
  print("=" * 60)
18
+ print("πŸš€ LOADING MODEL FOR HUGGING FACE SPACES")
19
  print("=" * 60)
20
 
21
+ # Cek apakah model ada di root atau folder
22
+ MODEL_PATHS = [
23
+ "model.keras",
24
+ "./model.keras",
25
+ "/tmp/model.keras"
26
+ ]
 
 
 
 
 
27
 
28
+ best_model = None
29
+ for model_path in MODEL_PATHS:
30
+ if os.path.exists(model_path):
31
+ try:
32
+ print(f"πŸ“‚ Trying to load model from: {model_path}")
33
+ best_model = tf.keras.models.load_model(
34
+ model_path,
35
+ compile=False,
36
+ safe_mode=False # Important for compatibility
37
+ )
38
+ print(f"βœ… Model loaded successfully from {model_path}")
39
+ break
40
+ except Exception as e:
41
+ print(f"❌ Failed to load from {model_path}: {e}")
42
+
43
+ # Jika model tidak ditemukan, buat dummy model
44
+ if best_model is None:
45
+ print("⚠️ No model file found. Creating dummy model for demo...")
46
  from tensorflow.keras import layers, Model
47
  inputs = layers.Input(shape=(224, 224, 3))
48
  x = layers.GlobalAveragePooling2D()(inputs)
49
+ dr_output = layers.Dense(5, name="dr_head")(x)
50
+ dme_output = layers.Dense(3, name="dme_head")(x)
51
  best_model = Model(inputs, {"dr_head": dr_output, "dme_head": dme_output})
52
  best_model.compile(optimizer="adam", loss="categorical_crossentropy")
53
+ print("βœ… Dummy model created")
54
 
55
+ # Summary model (debug info)
56
+ try:
57
+ best_model.summary()
58
+ except:
59
+ print("ℹ️ Model loaded, summary not available")
60
 
61
  # ============================================================
62
  # 2. CONFIG
63
  # ============================================================
64
  IMG_SIZE = 224
65
+ DR_CLASSES = ["No DR", "Mild", "Moderate", "Severe", "Proliferative DR"]
66
+ DME_CLASSES = ["No DME", "Low Risk", "High Risk"]
67
 
68
  # ============================================================
69
+ # 3. PREPROCESSING FUNCTIONS
70
  # ============================================================
71
+ def preprocess_pil_image(img):
72
+ """Preprocess PIL Image for prediction"""
73
+ # Convert to RGB if needed
74
+ if img.mode != 'RGB':
75
+ img = img.convert('RGB')
76
+
77
+ # Resize
78
+ img = img.resize((IMG_SIZE, IMG_SIZE))
79
+
80
+ # Convert to numpy and normalize
81
+ arr = np.array(img, dtype=np.float32) / 255.0
82
+
83
+ # Add batch dimension
84
+ return np.expand_dims(arr, 0)
85
 
86
  # ============================================================
87
+ # 4. SOFTMAX SAFETY
 
 
 
 
 
 
88
  # ============================================================
89
  def ensure_probability(x):
90
  x = np.asarray(x, dtype=np.float32)
91
+ # If values don't look like probabilities, apply softmax
92
  if x.min() < 0 or x.max() > 1.0 or abs(x.sum() - 1.0) > 1e-3:
93
  x = tf.nn.softmax(x).numpy()
94
  return x
95
 
96
  # ============================================================
97
+ # 5. CORE PREDICTION FUNCTION
98
+ # ============================================================
99
+ def predict_image(image):
100
+ """Core prediction function that returns structured data"""
101
+ try:
102
+ # Preprocess
103
+ img_tensor = preprocess_pil_image(image)
104
+
105
+ # Predict (disable verbose for cleaner output)
106
+ preds = best_model.predict(img_tensor, verbose=0)
107
+
108
+ # ---- Handle different model output formats ----
109
+ dr_pred = None
110
+ dme_pred = None
111
+
112
+ if isinstance(preds, dict):
113
+ # Cari key untuk DR dan DME
114
+ dr_keys = [k for k in preds.keys() if 'dr' in k.lower()]
115
+ dme_keys = [k for k in preds.keys() if 'dme' in k.lower()]
116
+
117
+ if dr_keys:
118
+ dr_pred = preds[dr_keys[0]]
119
+ if dme_keys:
120
+ dme_pred = preds[dme_keys[0]]
121
+
122
+ # Jika tidak ketemu, ambil 2 output pertama
123
+ if dr_pred is None and len(preds) >= 2:
124
+ keys = list(preds.keys())
125
+ dr_pred = preds[keys[0]]
126
+ dme_pred = preds[keys[1]]
127
+
128
+ elif isinstance(preds, (list, tuple)):
129
+ if len(preds) >= 2:
130
+ dr_pred = preds[0]
131
+ dme_pred = preds[1]
132
+ else:
133
+ dr_pred = preds[0][:, :5] if len(preds[0].shape) > 1 else preds[0][:5]
134
+ dme_pred = preds[0][:, 5:8] if len(preds[0].shape) > 1 else preds[0][5:8]
135
+
136
+ elif isinstance(preds, np.ndarray):
137
+ if len(preds.shape) == 2:
138
+ dr_pred = preds[:, :5]
139
+ dme_pred = preds[:, 5:8]
140
+ else:
141
+ dr_pred = preds[:5]
142
+ dme_pred = preds[5:8]
143
+
144
+ # Ambil batch pertama jika ada batch dimension
145
+ if dr_pred is not None and len(dr_pred.shape) > 1:
146
+ dr_pred = dr_pred[0]
147
+ if dme_pred is not None and len(dme_pred.shape) > 1:
148
+ dme_pred = dme_pred[0]
149
+
150
+ # Jika masih None, beri nilai default
151
+ if dr_pred is None:
152
+ dr_pred = np.zeros(5)
153
+ if dme_pred is None:
154
+ dme_pred = np.zeros(3)
155
+
156
+ # ---- Apply softmax ----
157
+ dr_probs = ensure_probability(dr_pred)
158
+ dme_probs = ensure_probability(dme_pred)
159
+
160
+ # ---- Get results ----
161
+ dr_idx = int(np.argmax(dr_probs))
162
+ dme_idx = int(np.argmax(dme_probs))
163
+
164
+ dr_name = DR_CLASSES[dr_idx]
165
+ dme_name = DME_CLASSES[dme_idx]
166
+
167
+ dr_conf = float(dr_probs[dr_idx] * 100)
168
+ dme_conf = float(dme_probs[dme_idx] * 100)
169
+
170
+ # ---- Generate recommendations ----
171
+ if dr_name in ["No DR"]:
172
+ rec_dr = "Lanjutkan pola hidup sehat dan lakukan pemeriksaan mata rutin minimal 1 tahun sekali."
173
+ elif dr_name in ["Mild", "Moderate"]:
174
+ rec_dr = "Disarankan kontrol gula darah secara ketat dan pemeriksaan mata berkala setiap 6 bulan."
175
+ else: # Severe / Proliferative
176
+ rec_dr = "Disarankan segera konsultasi ke dokter spesialis mata untuk evaluasi dan penanganan lebih lanjut."
177
+
178
+ if dme_name == "No DME":
179
+ rec_dme = "Belum ditemukan tanda edema makula diabetik, lanjutkan pemantauan rutin."
180
+ elif dme_name == "Low Risk":
181
+ rec_dme = "Perlu observasi ketat dan pemeriksaan lanjutan untuk mencegah progresivitas."
182
+ else: # High Risk
183
+ rec_dme = "Disarankan segera mendapatkan evaluasi klinis dan terapi oleh dokter spesialis mata."
184
+
185
+ # Return both structured data and HTML
186
+ return {
187
+ "success": True,
188
+ "predictions": {
189
+ "diabetic_retinopathy": {
190
+ "classification": dr_name,
191
+ "confidence": dr_conf,
192
+ "index": dr_idx,
193
+ "probabilities": dr_probs.tolist(),
194
+ "recommendation": rec_dr
195
+ },
196
+ "diabetic_macular_edema": {
197
+ "classification": dme_name,
198
+ "confidence": dme_conf,
199
+ "index": dme_idx,
200
+ "probabilities": dme_probs.tolist(),
201
+ "recommendation": rec_dme
202
+ }
203
+ }
204
+ }
205
+
206
+ except Exception as e:
207
+ return {
208
+ "success": False,
209
+ "error": str(e)
210
+ }
211
+
212
+ # ============================================================
213
+ # 6. API PREDICT FUNCTION UNTUK /run/predict
214
+ # ============================================================
215
+ def api_predict(image):
216
+ """
217
+ Function untuk API endpoint /run/predict
218
+ Gradio akan secara otomatis mengkonversi input ke format yang tepat
219
+ """
220
+ try:
221
+ if image is None:
222
+ return {"error": "No image provided"}
223
+
224
+ # Jika input adalah dictionary (dari JSON API call)
225
+ if isinstance(image, dict):
226
+ if "data" in image:
227
+ # Handle base64 dari JSON API
228
+ return handle_api_json_input(image)
229
+
230
+ # Jika input adalah file/bytes (dari form-data)
231
+ # Gradio sudah otomatis konversi ke PIL Image
232
+ result = predict_image(image)
233
+ return result
234
+
235
+ except Exception as e:
236
+ return {
237
+ "success": False,
238
+ "error": f"API processing error: {str(e)}"
239
+ }
240
+
241
+ def handle_api_json_input(image_data):
242
+ """Handle JSON input dengan base64"""
243
+ try:
244
+ img_data = image_data["data"]
245
+ if isinstance(img_data, list):
246
+ img_data = img_data[0]
247
+
248
+ # Decode base64
249
+ if img_data.startswith("data:image"):
250
+ img_data = img_data.split(",")[1]
251
+
252
+ # Convert base64 to PIL Image
253
+ img_bytes = base64.b64decode(img_data)
254
+ img = Image.open(io.BytesIO(img_bytes)).convert("RGB")
255
+
256
+ # Get prediction
257
+ return predict_image(img)
258
+
259
+ except Exception as e:
260
+ return {
261
+ "success": False,
262
+ "error": f"Base64 processing error: {str(e)}"
263
+ }
264
+
265
+ # ============================================================
266
+ # 7. FORMAT OUTPUT FOR GRADIO UI
267
+ # ============================================================
268
+ def format_prediction_html(result):
269
+ """Format prediction result as HTML for Gradio"""
270
+ if not result["success"]:
271
+ return f"""
272
+ <div style="color: red; padding: 20px; border: 2px solid red; border-radius: 10px;">
273
+ <h3>❌ Error</h3>
274
+ <p>{result['error']}</p>
275
+ </div>
276
+ """
277
+
278
+ preds = result["predictions"]
279
+ dr = preds["diabetic_retinopathy"]
280
+ dme = preds["diabetic_macular_edema"]
281
+
282
+ # Warna berdasarkan severity
283
+ dr_color = {
284
+ "No DR": "#28a745",
285
+ "Mild": "#ffc107",
286
+ "Moderate": "#fd7e14",
287
+ "Severe": "#dc3545",
288
+ "Proliferative DR": "#6f42c1"
289
+ }.get(dr["classification"], "#000000")
290
+
291
+ dme_color = {
292
+ "No DME": "#28a745",
293
+ "Low Risk": "#ffc107",
294
+ "High Risk": "#dc3545"
295
+ }.get(dme["classification"], "#000000")
296
+
297
+ html = f"""
298
+ <div style="font-family: Arial, sans-serif; max-width: 800px; margin: 0 auto;">
299
+
300
+ <!-- Header -->
301
+ <div style="text-align: center; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
302
+ color: white; padding: 25px; border-radius: 15px 15px 0 0; margin-bottom: 20px;">
303
+ <h1 style="margin: 0; font-size: 32px;">πŸ”¬ HASIL DETEKSI</h1>
304
+ <p style="margin: 5px 0 0 0; font-size: 16px; opacity: 0.9;">AI-Powered Retina Analysis</p>
305
+ </div>
306
+
307
+ <!-- Results Table -->
308
+ <div style="background: white; border-radius: 10px; box-shadow: 0 4px 12px rgba(0,0,0,0.1); overflow: hidden;">
309
+ <table style="width: 100%; border-collapse: collapse;">
310
+ <thead>
311
+ <tr style="background-color: #f8f9fa;">
312
+ <th style="padding: 16px; text-align: left; border-bottom: 2px solid #dee2e6; font-size: 18px;">Kondisi</th>
313
+ <th style="padding: 16px; text-align: left; border-bottom: 2px solid #dee2e6; font-size: 18px;">Klasifikasi</th>
314
+ <th style="padding: 16px; text-align: left; border-bottom: 2px solid #dee2e6; font-size: 18px;">Tingkat Kepercayaan</th>
315
+ </tr>
316
+ </thead>
317
+ <tbody>
318
+ <tr>
319
+ <td style="padding: 16px; border-bottom: 1px solid #dee2e6; font-weight: bold;">Diabetic Retinopathy (DR)</td>
320
+ <td style="padding: 16px; border-bottom: 1px solid #dee2e6;">
321
+ <span style="color: {dr_color}; font-weight: bold; font-size: 18px;">{dr['classification']}</span>
322
+ </td>
323
+ <td style="padding: 16px; border-bottom: 1px solid #dee2e6;">
324
+ <div style="display: flex; align-items: center; gap: 10px;">
325
+ <div style="flex-grow: 1; background: #e9ecef; height: 20px; border-radius: 10px; overflow: hidden;">
326
+ <div style="width: {dr['confidence']}%; background: {dr_color}; height: 100%;"></div>
327
+ </div>
328
+ <span style="font-weight: bold; min-width: 60px;">{dr['confidence']:.1f}%</span>
329
+ </div>
330
+ </td>
331
+ </tr>
332
+ <tr>
333
+ <td style="padding: 16px; border-bottom: 1px solid #dee2e6; font-weight: bold;">Diabetic Macular Edema (DME)</td>
334
+ <td style="padding: 16px; border-bottom: 1px solid #dee2e6;">
335
+ <span style="color: {dme_color}; font-weight: bold; font-size: 18px;">{dme['classification']}</span>
336
+ </td>
337
+ <td style="padding: 16px; border-bottom: 1px solid #dee2e6;">
338
+ <div style="display: flex; align-items: center; gap: 10px;">
339
+ <div style="flex-grow: 1; background: #e9ecef; height: 20px; border-radius: 10px; overflow: hidden;">
340
+ <div style="width: {dme['confidence']}%; background: {dme_color}; height: 100%;"></div>
341
+ </div>
342
+ <span style="font-weight: bold; min-width: 60px;">{dme['confidence']:.1f}%</span>
343
+ </div>
344
+ </td>
345
+ </tr>
346
+ </tbody>
347
+ </table>
348
+ </div>
349
+
350
+ <!-- Recommendations -->
351
+ <div style="margin-top: 25px; background: white; border-radius: 10px; box-shadow: 0 4px 12px rgba(0,0,0,0.1); overflow: hidden;">
352
+ <div style="background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); color: white; padding: 15px;">
353
+ <h3 style="margin: 0; font-size: 22px;">🩺 REKOMENDASI KLINIS</h3>
354
+ </div>
355
+ <div style="padding: 20px;">
356
+ <div style="margin-bottom: 15px;">
357
+ <h4 style="color: #333; margin-bottom: 8px;">β€’ Diabetic Retinopathy (DR):</h4>
358
+ <p style="margin: 0; color: #555; line-height: 1.6;">{dr['recommendation']}</p>
359
+ </div>
360
+ <div>
361
+ <h4 style="color: #333; margin-bottom: 8px;">β€’ Diabetic Macular Edema (DME):</h4>
362
+ <p style="margin: 0; color: #555; line-height: 1.6;">{dme['recommendation']}</p>
363
+ </div>
364
+ </div>
365
+ </div>
366
+
367
+ <!-- Disclaimer -->
368
+ <div style="margin-top: 20px; padding: 15px; background: #fff3cd; border: 1px solid #ffeaa7; border-radius: 8px; font-size: 14px;">
369
+ <strong>⚠️ Disclaimer:</strong> Hasil ini merupakan prediksi AI dan bukan diagnosis medis. Konsultasikan dengan dokter spesialis mata untuk diagnosis yang akurat.
370
+ </div>
371
+
372
+ </div>
373
+ """
374
+
375
+ return html
376
+
377
+ # ============================================================
378
+ # 8. GRADIO UI FUNCTION
379
+ # ============================================================
380
+ def gradio_predict(image):
381
+ """Main function for Gradio UI"""
382
+ if image is None:
383
+ return "❌ Silakan unggah gambar fundus retina"
384
+
385
+ # Get prediction
386
+ result = predict_image(image)
387
+
388
+ # Format as HTML
389
+ return format_prediction_html(result)
390
+
391
+ # ============================================================
392
+ # 9. CREATE GRADIO INTERFACES
393
  # ============================================================
394
+
395
+ # Interface untuk Web UI
396
+ web_interface = gr.Interface(
397
+ fn=gradio_predict,
398
+ inputs=gr.Image(type="pil", label="πŸ“€ Upload Gambar Retina"),
399
+ outputs=gr.HTML(label="πŸ“Š Hasil Analisis"),
400
+ title="🩺 DETEKSI DIABETIC RETINOPATHY & DME",
401
+ description="Sistem AI untuk Analisis Citra Fundus Retina",
402
+ allow_flagging="never"
403
+ )
404
+
405
+ # Interface untuk API (akan digunakan oleh /run/predict)
406
+ api_interface = gr.Interface(
407
+ fn=api_predict,
408
+ inputs=gr.Image(type="pil"),
409
+ outputs=gr.JSON(),
410
+ title="API Endpoint",
411
+ description="Use this endpoint for API calls",
412
+ allow_flagging="never"
413
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
414
 
415
  # ============================================================
416
+ # 10. MULTI TEST IMAGES
417
  # ============================================================
418
  TEST_IMAGES = [
419
  "IDRiD_001test.jpg",
 
430
 
431
  TEST_IMAGES = [[p] for p in TEST_IMAGES if os.path.exists(p)]
432
 
 
 
 
 
 
 
 
 
 
 
 
433
 
434
  # ============================================================
435
+ # 11. CREATE GRADIO APP WITH BLOCKS
436
  # ============================================================
437
+ with gr.Blocks(
438
+ title="DR & DME Detection",
439
+ # css=CUSTOM_CSS,
440
+ theme=gr.themes.Soft()
441
+ ) as demo:
442
+
443
+ # Header
444
  gr.Markdown("""
445
+ # 🩺 DETEKSI DIABETIC RETINOPATHY & DME
446
+ ### Sistem AI untuk Analisis Citra Fundus Retina
447
+
448
+ Upload gambar fundus retina untuk mendeteksi:
449
+ - **Diabetic Retinopathy (DR)**: Kerusakan retina akibat diabetes
450
+ - **Diabetic Macular Edema (DME)**: Pembengkakan di makula
451
  """)
452
+
453
+ # Create tabs for Web UI and API
454
+ with gr.Tabs():
455
+ # Tab 1: Web UI
456
+ with gr.TabItem("🌐 Web Interface"):
457
+ with gr.Row():
458
+ with gr.Column(scale=1):
459
+ # Upload section
460
+ image_input = gr.Image(
461
+ type="pil",
462
+ label="πŸ“€ Upload Gambar Retina",
463
+ height=300
464
+ )
465
+
466
+ upload_btn = gr.Button(
467
+ "πŸ” Analisis Gambar",
468
+ variant="primary",
469
+ size="lg"
470
+ )
471
+
472
+ gr.Markdown("""
473
+ **Format yang didukung:** JPG, PNG, JPEG
474
+ **Ukuran rekomendasi:** 224Γ—224 piksel
475
+ **Warna:** RGB (akan dikonversi otomatis)
476
+ """)
477
+
478
+ with gr.Column(scale=2):
479
+ # Results section
480
+ output_html = gr.HTML(
481
+ label="πŸ“Š Hasil Analisis",
482
+ value="<div style='text-align: center; padding: 50px; color: #666;'>Hasil analisis akan muncul di sini setelah mengupload gambar.</div>"
483
+ )
484
+
485
+
486
+ gr.Markdown("### πŸ§ͺ Data Testing")
487
+ gr.Examples(
488
+ examples=TEST_IMAGES,
489
+ inputs=image_input
490
+ )
491
+
492
+ # Connect button to function
493
+ upload_btn.click(
494
+ fn=gradio_predict,
495
+ inputs=image_input,
496
+ outputs=output_html
497
+ )
498
+
499
+ # Also trigger on image upload
500
+ image_input.change(
501
+ fn=gradio_predict,
502
+ inputs=image_input,
503
+ outputs=output_html
504
+ )
505
+
506
+ # Tab 2: API Interface
507
+ with gr.TabItem("πŸ”§ API Endpoint"):
508
+ gr.Markdown("""
509
+ ### API Endpoint untuk Mobile App
510
+
511
+ **URL:** `/run/predict`
512
+
513
+ **Method:** POST
514
+
515
+ **Content-Type:** `multipart/form-data` atau `application/json`
516
+ """)
517
+
518
+ with gr.Row():
519
+ with gr.Column():
520
+ api_image_input = gr.Image(
521
+ type="pil",
522
+ label="Test API dengan gambar"
523
+ )
524
+ api_test_btn = gr.Button("Test API", variant="secondary")
525
+
526
+ with gr.Column():
527
+ api_output = gr.JSON(
528
+ label="API Response",
529
+ value={"info": "API response akan muncul di sini"}
530
+ )
531
+
532
+ # Connect API test button
533
+ api_test_btn.click(
534
+ fn=api_predict,
535
+ inputs=api_image_input,
536
+ outputs=api_output
537
+ )
538
+
539
+ gr.Markdown("""
540
+ ### πŸ“‹ Contoh Penggunaan API
541
+
542
+ **cURL dengan file:**
543
+ ```bash
544
+ curl -X POST "https://[your-space].hf.space/run/predict" \\
545
+ -F "data=@retina_image.jpg"
546
+ ```
547
+
548
+ **Python:**
549
+ ```python
550
+ import requests
551
+
552
+ with open("retina_image.jpg", "rb") as f:
553
+ response = requests.post(
554
+ "https://[your-space].hf.space/run/predict",
555
+ files={"data": f}
556
+ )
557
+ print(response.json())
558
+ ```
559
+ """)
560
+
561
 
562
  # ============================================================
563
+ # 11. LAUNCH FOR HUGGING FACE
564
  # ============================================================
565
+ if __name__ == "__main__":
566
+ # Launch untuk Hugging Face Spaces
567
+ demo.launch(
568
+ server_name="0.0.0.0",
569
+ server_port=7860,
570
+ share=False,
571
+ debug=False
572
+ )