kodetr commited on
Commit
2d7ab3a
Β·
verified Β·
1 Parent(s): 02ad042

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +358 -389
app.py CHANGED
@@ -4,38 +4,63 @@ import numpy as np
4
  import os
5
  import warnings
6
  import io
 
7
  import base64
8
  from PIL import Image
 
 
 
 
 
9
 
10
  warnings.filterwarnings("ignore")
11
 
 
 
 
12
  print("=" * 60)
13
- print("πŸš€ DR & DME DETECTION - HUGGING FACE SPACES")
14
  print("=" * 60)
15
 
16
- # ============================================================
17
- # 1. LOAD MODEL
18
- # ============================================================
19
- MODEL_PATHS = ["model.keras", "./model.keras"]
 
 
20
 
21
  best_model = None
22
  for model_path in MODEL_PATHS:
23
  if os.path.exists(model_path):
24
  try:
25
- best_model = tf.keras.models.load_model(model_path, compile=False)
26
- print(f"βœ… Model loaded from {model_path}")
 
 
 
 
 
27
  break
28
  except Exception as e:
29
  print(f"❌ Failed to load from {model_path}: {e}")
30
 
 
31
  if best_model is None:
32
- print("⚠️ Creating dummy model for demo")
33
  from tensorflow.keras import layers, Model
34
  inputs = layers.Input(shape=(224, 224, 3))
35
  x = layers.GlobalAveragePooling2D()(inputs)
36
  dr_output = layers.Dense(5, name="dr_head")(x)
37
  dme_output = layers.Dense(3, name="dme_head")(x)
38
  best_model = Model(inputs, {"dr_head": dr_output, "dme_head": dme_output})
 
 
 
 
 
 
 
 
39
 
40
  # ============================================================
41
  # 2. CONFIG
@@ -45,209 +70,128 @@ DR_CLASSES = ["No DR", "Mild", "Moderate", "Severe", "Proliferative DR"]
45
  DME_CLASSES = ["No DME", "Low Risk", "High Risk"]
46
 
47
  # ============================================================
48
- # 3. CUSTOM CSS FOR HUGGING FACE
49
  # ============================================================
50
- CUSTOM_CSS = """
51
- /* Reset Hugging Face wrapper styles */
52
- #component-0, #component-1, #component-2, #component-3 {
53
- background: white !important;
54
- color: black !important;
55
- }
56
-
57
- .gradio-container {
58
- background: white !important;
59
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif !important;
60
- max-width: 1200px !important;
61
- margin: 0 auto !important;
62
- padding: 20px !important;
63
- }
64
-
65
- /* Fix for dark mode issues */
66
- .dark .gradio-container,
67
- .dark .gradio-container * {
68
- background: white !important;
69
- color: black !important;
70
- }
71
-
72
- /* Header styling */
73
- .header-container {
74
- text-align: center;
75
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
76
- color: white;
77
- padding: 30px;
78
- border-radius: 15px;
79
- margin-bottom: 30px;
80
- box-shadow: 0 4px 20px rgba(0,0,0,0.1);
81
- }
82
-
83
- .header-title {
84
- font-size: 2.5em;
85
- font-weight: 800;
86
- margin-bottom: 10px;
87
- }
88
-
89
- .header-subtitle {
90
- font-size: 1.2em;
91
- opacity: 0.9;
92
- }
93
-
94
- /* Upload section */
95
- .upload-section {
96
- background: #f8f9fa;
97
- padding: 25px;
98
- border-radius: 12px;
99
- border: 2px dashed #dee2e6;
100
- text-align: center;
101
- }
102
-
103
- /* Results section */
104
- .results-section {
105
- background: white;
106
- padding: 25px;
107
- border-radius: 12px;
108
- border: 1px solid #e9ecef;
109
- box-shadow: 0 2px 10px rgba(0,0,0,0.05);
110
- }
111
-
112
- /* Button styling */
113
- .primary-button {
114
- background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%) !important;
115
- color: white !important;
116
- border: none !important;
117
- padding: 12px 30px !important;
118
- font-size: 16px !important;
119
- border-radius: 8px !important;
120
- font-weight: 600 !important;
121
- margin-top: 15px !important;
122
- }
123
-
124
- .primary-button:hover {
125
- transform: translateY(-2px);
126
- box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2) !important;
127
- }
128
-
129
- /* Table styling */
130
- .result-table {
131
- width: 100%;
132
- border-collapse: collapse;
133
- margin: 20px 0;
134
- }
135
-
136
- .result-table th {
137
- background: #f8f9fa;
138
- padding: 15px;
139
- text-align: left;
140
- border-bottom: 2px solid #dee2e6;
141
- font-weight: 600;
142
- }
143
-
144
- .result-table td {
145
- padding: 15px;
146
- border-bottom: 1px solid #e9ecef;
147
- }
148
-
149
- /* Progress bar */
150
- .progress-bar {
151
- height: 20px;
152
- background: #e9ecef;
153
- border-radius: 10px;
154
- overflow: hidden;
155
- margin: 10px 0;
156
- }
157
-
158
- .progress-fill {
159
- height: 100%;
160
- border-radius: 10px;
161
- }
162
-
163
- /* Responsive design */
164
- @media (max-width: 768px) {
165
- .gradio-container {
166
- padding: 10px !important;
167
- }
168
-
169
- .header-title {
170
- font-size: 1.8em;
171
- }
172
-
173
- .header-subtitle {
174
- font-size: 1em;
175
- }
176
- }
177
- """
178
 
179
  # ============================================================
180
- # 4. PREDICTION FUNCTION
 
 
 
 
 
 
 
 
 
181
  # ============================================================
182
  def predict_image(image):
 
183
  try:
184
  # Preprocess
185
- if image.mode != 'RGB':
186
- image = image.convert('RGB')
187
- img = image.resize((IMG_SIZE, IMG_SIZE))
188
- arr = np.array(img, dtype=np.float32) / 255.0
189
- x = np.expand_dims(arr, 0)
190
 
191
  # Predict
192
- preds = best_model.predict(x, verbose=0)
 
 
 
 
193
 
194
- # Handle output
195
  if isinstance(preds, dict):
196
- dr = preds.get("dr_head", list(preds.values())[0])[0]
197
- dme = preds.get("dme_head", list(preds.values())[1])[0]
198
- else:
199
- dr = preds[0][0] if len(preds[0].shape) > 1 else preds[0]
200
- dme = preds[1][0] if len(preds[1].shape) > 1 else preds[1]
201
-
202
- # Softmax
203
- dr_probs = tf.nn.softmax(dr).numpy()
204
- dme_probs = tf.nn.softmax(dme).numpy()
205
-
206
- # Results
207
- dr_idx = np.argmax(dr_probs)
208
- dme_idx = np.argmax(dme_probs)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
209
 
 
 
 
 
 
 
 
 
 
 
 
 
 
210
  dr_name = DR_CLASSES[dr_idx]
211
  dme_name = DME_CLASSES[dme_idx]
212
-
213
  dr_conf = float(dr_probs[dr_idx] * 100)
214
  dme_conf = float(dme_probs[dme_idx] * 100)
215
-
216
- # Recommendations
217
- recommendations = {
218
- "No DR": "Lanjutkan pola hidup sehat dan lakukan pemeriksaan mata rutin minimal 1 tahun sekali.",
219
- "Mild": "Disarankan kontrol gula darah secara ketat dan pemeriksaan mata berkala setiap 6 bulan.",
220
- "Moderate": "Disarankan kontrol gula darah secara ketat dan pemeriksaan mata berkala setiap 6 bulan.",
221
- "Severe": "Disarankan segera konsultasi ke dokter spesialis mata untuk evaluasi dan penanganan lebih lanjut.",
222
- "Proliferative DR": "Disarankan segera konsultasi ke dokter spesialis mata untuk evaluasi dan penanganan lebih lanjut.",
223
- "No DME": "Belum ditemukan tanda edema makula diabetik, lanjutkan pemantauan rutin.",
224
- "Low Risk": "Perlu observasi ketat dan pemeriksaan lanjutan untuk mencegah progresivitas.",
225
- "High Risk": "Disarankan segera mendapatkan evaluasi klinis dan terapi oleh dokter spesialis mata."
226
- }
227
-
 
 
 
228
  return {
229
  "success": True,
230
- "dr": {
231
- "name": dr_name,
232
- "confidence": dr_conf,
233
- "color": {
234
- "No DR": "#28a745",
235
- "Mild": "#ffc107",
236
- "Moderate": "#fd7e14",
237
- "Severe": "#dc3545",
238
- "Proliferative DR": "#6f42c1"
239
- }.get(dr_name, "#000000"),
240
- "recommendation": recommendations.get(dr_name, "")
241
- },
242
- "dme": {
243
- "name": dme_name,
244
- "confidence": dme_conf,
245
- "color": {
246
- "No DME": "#28a745",
247
- "Low Risk": "#ffc107",
248
- "High Risk": "#dc3545"
249
- }.get(dme_name, "#000000"),
250
- "recommendation": recommendations.get(dme_name, "")
251
  }
252
  }
253
 
@@ -258,66 +202,95 @@ def predict_image(image):
258
  }
259
 
260
  # ============================================================
261
- # 5. GRADIO INTERFACE FUNCTION
262
  # ============================================================
263
- def gradio_predict(image):
264
- if image is None:
265
- return """
266
- <div class="results-section">
267
- <h3 style="color: #666; text-align: center;">
268
- ⬆️ Silakan upload gambar retina di sebelah kiri
269
- </h3>
270
- </div>
271
- """
272
-
273
- result = predict_image(image)
274
-
 
 
 
 
 
 
 
 
275
  if not result["success"]:
276
  return f"""
277
- <div class="results-section" style="border-color: #dc3545;">
278
- <h3 style="color: #dc3545;">❌ Error</h3>
279
  <p>{result['error']}</p>
280
  </div>
281
  """
282
 
283
- dr = result["dr"]
284
- dme = result["dme"]
 
285
 
286
- return f"""
287
- <div class="results-section">
288
- <div style="text-align: center; margin-bottom: 25px;">
289
- <h2 style="color: #333; margin-bottom: 10px;">πŸ”¬ HASIL ANALISIS</h2>
290
- <p style="color: #666;">AI-Powered Retina Analysis</p>
291
- </div>
292
-
293
- <table class="result-table">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
294
  <thead>
295
- <tr>
296
- <th>Kondisi</th>
297
- <th>Klasifikasi</th>
298
- <th>Tingkat Kepercayaan</th>
299
  </tr>
300
  </thead>
301
  <tbody>
302
  <tr>
303
- <td><strong>Diabetic Retinopathy (DR)</strong></td>
304
- <td><span style="color: {dr['color']}; font-weight: bold; font-size: 1.1em;">{dr['name']}</span></td>
305
- <td>
 
 
306
  <div style="display: flex; align-items: center; gap: 10px;">
307
- <div class="progress-bar">
308
- <div class="progress-fill" style="width: {dr['confidence']}%; background: {dr['color']};"></div>
309
  </div>
310
  <span style="font-weight: bold; min-width: 60px;">{dr['confidence']:.1f}%</span>
311
  </div>
312
  </td>
313
  </tr>
314
  <tr>
315
- <td><strong>Diabetic Macular Edema (DME)</strong></td>
316
- <td><span style="color: {dme['color']}; font-weight: bold; font-size: 1.1em;">{dme['name']}</span></td>
317
- <td>
 
 
318
  <div style="display: flex; align-items: center; gap: 10px;">
319
- <div class="progress-bar">
320
- <div class="progress-fill" style="width: {dme['confidence']}%; background: {dme['color']};"></div>
321
  </div>
322
  <span style="font-weight: bold; min-width: 60px;">{dme['confidence']:.1f}%</span>
323
  </div>
@@ -325,206 +298,202 @@ def gradio_predict(image):
325
  </tr>
326
  </tbody>
327
  </table>
328
-
329
- <div style="margin-top: 30px; padding: 20px; background: #f8f9fa; border-radius: 10px;">
330
- <h3 style="color: #333; margin-bottom: 15px;">🩺 REKOMENDASI KLINIS</h3>
331
-
 
 
 
332
  <div style="margin-bottom: 15px;">
333
- <h4 style="color: #555; margin-bottom: 8px;">β€’ Diabetic Retinopathy (DR):</h4>
334
- <p style="color: #666; margin: 0; line-height: 1.6;">{dr['recommendation']}</p>
335
  </div>
336
-
337
  <div>
338
- <h4 style="color: #555; margin-bottom: 8px;">β€’ Diabetic Macular Edema (DME):</h4>
339
- <p style="color: #666; margin: 0; line-height: 1.6;">{dme['recommendation']}</p>
340
  </div>
341
  </div>
342
-
343
- <div style="margin-top: 20px; padding: 15px; background: #fff3cd; border: 1px solid #ffeaa7; border-radius: 8px;">
344
- <strong>⚠️ Disclaimer:</strong> Hasil ini merupakan prediksi AI dan bukan diagnosis medis.
345
- Konsultasikan dengan dokter spesialis mata untuk diagnosis yang akurat.
346
- </div>
347
  </div>
348
  """
 
 
 
 
 
 
 
 
 
349
 
350
  # ============================================================
351
- # 6. CREATE GRADIO APP
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
352
  # ============================================================
353
  with gr.Blocks(
354
- css=CUSTOM_CSS,
355
- theme=gr.themes.Soft(
356
- primary_hue="blue",
357
- secondary_hue="gray",
358
- font=gr.themes.GoogleFont("Inter")
359
- ),
360
  title="DR & DME Detection",
361
- analytics_enabled=False # Disable Gradio analytics for faster load
362
  ) as demo:
363
 
364
- # Custom header
365
- gr.HTML("""
366
- <div class="header-container">
367
- <div class="header-title">🩺 DETEKSI DIABETIC RETINOPATHY & DME</div>
368
- <div class="header-subtitle">Sistem AI untuk Analisis Citra Fundus Retina</div>
369
- <div style="margin-top: 15px; font-size: 0.9em; opacity: 0.8;">
370
- Deteksi kerusakan retina akibat diabetes dan pembengkakan di makula
371
- </div>
372
- </div>
373
  """)
374
 
375
- with gr.Row(equal_height=True):
376
- # Left column - Upload
377
- with gr.Column(scale=1, min_width=400):
378
- gr.HTML("""
379
- <div class="upload-section">
380
- <h3 style="color: #333; margin-bottom: 15px;">πŸ“€ UPLOAD GAMBAR</h3>
381
- <p style="color: #666; margin-bottom: 10px;">
382
- Upload gambar fundus retina untuk analisis AI
383
- </p>
384
- </div>
385
- """)
386
-
387
  image_input = gr.Image(
388
  type="pil",
389
- label=" ",
390
- height=300,
391
- elem_id="upload-image"
392
  )
393
 
394
- predict_btn = gr.Button(
395
- "πŸ” ANALISIS GAMBAR",
396
- elem_classes="primary-button",
397
- scale=1
398
  )
399
 
400
- gr.HTML("""
401
- <div style="margin-top: 20px; padding: 15px; background: #e8f4fd; border-radius: 8px;">
402
- <h4 style="color: #0d6efd; margin-bottom: 10px;">πŸ“‹ Panduan Upload:</h4>
403
- <ul style="color: #666; margin: 0; padding-left: 20px;">
404
- <li><strong>Format:</strong> JPG, PNG, JPEG</li>
405
- <li><strong>Ukuran:</strong> 224Γ—224 piksel (otomatis resize)</li>
406
- <li><strong>Warna:</strong> RGB (otomatis konversi)</li>
407
- </ul>
408
- </div>
409
  """)
410
 
411
- # Right column - Results
412
- with gr.Column(scale=2, min_width=600):
413
  output_html = gr.HTML(
414
- value="""
415
- <div class="results-section" style="text-align: center; padding: 50px;">
416
- <div style="color: #666; margin-bottom: 20px;">
417
- <h3>πŸ‘οΈ Siap untuk Analisis</h3>
418
- <p>Upload gambar retina di sebelah kiri untuk memulai deteksi</p>
419
- </div>
420
- <div style="font-size: 3em; color: #dee2e6;">
421
- ⬅️
422
- </div>
423
- </div>
424
- """,
425
- elem_id="results-container"
426
  )
 
 
 
 
 
 
427
 
428
- # API info section
429
- with gr.Accordion("πŸ“± AKSES API UNTUK MOBILE APP", open=False):
 
430
  gr.Markdown("""
431
- ### API Endpoint untuk Mobile:
432
-
433
- **URL:** `https://kodetr-idrid.hf.space/run/predict`
434
-
435
- **Method:** `POST`
436
-
437
- **Content-Type:** `multipart/form-data`
438
-
439
- **Body parameter:** `data` (file gambar)
440
 
441
- ### Contoh cURL:
442
- ```bash
443
- curl -X POST "https://kodetr-idrid.hf.space/run/predict" \\
444
- -F "data=@retina_image.jpg"
445
- ```
446
 
447
- ### Contoh Python:
448
- ```python
449
- import requests
450
 
451
- with open("retina.jpg", "rb") as f:
452
- response = requests.post(
453
- "https://kodetr-idrid.hf.space/run/predict",
454
- files={"data": f}
455
- )
456
- print(response.json())
457
- ```
458
-
459
- ### Response Format (JSON):
460
- ```json
461
- {{
462
- "success": true,
463
- "dr": {{
464
- "name": "No DR",
465
- "confidence": 85.5,
466
- "recommendation": "..."
467
- }},
468
- "dme": {{
469
- "name": "No DME",
470
- "confidence": 92.3,
471
- "recommendation": "..."
472
- }}
473
- }}
474
- ```
475
  """)
476
 
477
- # Connect button
478
- predict_btn.click(
479
- fn=gradio_predict,
480
- inputs=image_input,
481
- outputs=output_html
482
- )
483
-
484
- # Auto-trigger on image upload
485
- image_input.change(
486
  fn=gradio_predict,
487
  inputs=image_input,
488
  outputs=output_html
489
  )
490
 
491
  # ============================================================
492
- # 7. API FUNCTION FOR /run/predict
493
  # ============================================================
494
- def api_predict(image):
495
- """Function for /run/predict endpoint"""
496
- if image is None:
497
- return {"error": "No image provided"}
498
-
499
- result = predict_image(image)
500
- return result
501
-
502
- # Create separate API interface
503
- api_interface = gr.Interface(
504
- fn=api_predict,
505
- inputs=gr.Image(type="pil"),
506
- outputs=gr.JSON(),
507
- title="API Endpoint",
508
- description="Use this endpoint for API calls",
509
- allow_flagging="never"
510
- )
511
 
512
- # Combine interfaces
513
- final_app = gr.TabbedInterface(
514
- [demo, api_interface],
515
- ["🌐 Web Interface", "πŸ”§ API Endpoint"],
516
- title="DR & DME Detection"
517
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
518
 
519
  # ============================================================
520
- # 8. LAUNCH FOR HUGGING FACE
 
 
 
 
 
 
521
  # ============================================================
522
  if __name__ == "__main__":
523
- final_app.launch(
524
- server_name="0.0.0.0",
525
- server_port=7860,
526
- share=False,
527
- debug=False,
528
- show_error=True,
529
- favicon_path=None # Disable favicon for faster load
 
 
 
 
 
 
 
 
530
  )
 
4
  import os
5
  import warnings
6
  import io
7
+ import json
8
  import base64
9
  from PIL import Image
10
+ import tempfile
11
+ from fastapi import FastAPI, File, UploadFile, HTTPException
12
+ from fastapi.responses import JSONResponse, HTMLResponse
13
+ from fastapi.middleware.cors import CORSMiddleware
14
+ import uvicorn
15
 
16
  warnings.filterwarnings("ignore")
17
 
18
+ # ============================================================
19
+ # 1. LOAD MODEL (with Hugging Face compatibility)
20
+ # ============================================================
21
  print("=" * 60)
22
+ print("πŸš€ LOADING MODEL FOR HUGGING FACE SPACES")
23
  print("=" * 60)
24
 
25
+ # Cek apakah model ada di root atau folder
26
+ MODEL_PATHS = [
27
+ "model.keras",
28
+ "./model.keras",
29
+ "/tmp/model.keras"
30
+ ]
31
 
32
  best_model = None
33
  for model_path in MODEL_PATHS:
34
  if os.path.exists(model_path):
35
  try:
36
+ print(f"πŸ“‚ Trying to load model from: {model_path}")
37
+ best_model = tf.keras.models.load_model(
38
+ model_path,
39
+ compile=False,
40
+ safe_mode=False # Important for compatibility
41
+ )
42
+ print(f"βœ… Model loaded successfully from {model_path}")
43
  break
44
  except Exception as e:
45
  print(f"❌ Failed to load from {model_path}: {e}")
46
 
47
+ # Jika model tidak ditemukan, buat dummy model
48
  if best_model is None:
49
+ print("⚠️ No model file found. Creating dummy model for demo...")
50
  from tensorflow.keras import layers, Model
51
  inputs = layers.Input(shape=(224, 224, 3))
52
  x = layers.GlobalAveragePooling2D()(inputs)
53
  dr_output = layers.Dense(5, name="dr_head")(x)
54
  dme_output = layers.Dense(3, name="dme_head")(x)
55
  best_model = Model(inputs, {"dr_head": dr_output, "dme_head": dme_output})
56
+ best_model.compile(optimizer="adam", loss="categorical_crossentropy")
57
+ print("βœ… Dummy model created")
58
+
59
+ # Summary model (debug info)
60
+ try:
61
+ best_model.summary()
62
+ except:
63
+ print("ℹ️ Model loaded, summary not available")
64
 
65
  # ============================================================
66
  # 2. CONFIG
 
70
  DME_CLASSES = ["No DME", "Low Risk", "High Risk"]
71
 
72
  # ============================================================
73
+ # 3. PREPROCESSING FUNCTIONS
74
  # ============================================================
75
+ def preprocess_pil_image(img):
76
+ """Preprocess PIL Image for prediction"""
77
+ if img.mode != 'RGB':
78
+ img = img.convert('RGB')
79
+ img = img.resize((IMG_SIZE, IMG_SIZE))
80
+ arr = np.array(img, dtype=np.float32) / 255.0
81
+ return np.expand_dims(arr, 0)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
82
 
83
  # ============================================================
84
+ # 4. SOFTMAX SAFETY
85
+ # ============================================================
86
+ def ensure_probability(x):
87
+ x = np.asarray(x, dtype=np.float32)
88
+ if x.min() < 0 or x.max() > 1.0 or abs(x.sum() - 1.0) > 1e-3:
89
+ x = tf.nn.softmax(x).numpy()
90
+ return x
91
+
92
+ # ============================================================
93
+ # 5. CORE PREDICTION FUNCTION
94
  # ============================================================
95
  def predict_image(image):
96
+ """Core prediction function that returns structured data"""
97
  try:
98
  # Preprocess
99
+ img_tensor = preprocess_pil_image(image)
 
 
 
 
100
 
101
  # Predict
102
+ preds = best_model.predict(img_tensor, verbose=0)
103
+
104
+ # Handle different model output formats
105
+ dr_pred = None
106
+ dme_pred = None
107
 
 
108
  if isinstance(preds, dict):
109
+ dr_keys = [k for k in preds.keys() if 'dr' in k.lower()]
110
+ dme_keys = [k for k in preds.keys() if 'dme' in k.lower()]
111
+
112
+ if dr_keys:
113
+ dr_pred = preds[dr_keys[0]]
114
+ if dme_keys:
115
+ dme_pred = preds[dme_keys[0]]
116
+
117
+ if dr_pred is None and len(preds) >= 2:
118
+ keys = list(preds.keys())
119
+ dr_pred = preds[keys[0]]
120
+ dme_pred = preds[keys[1]]
121
+
122
+ elif isinstance(preds, (list, tuple)):
123
+ if len(preds) >= 2:
124
+ dr_pred = preds[0]
125
+ dme_pred = preds[1]
126
+ else:
127
+ dr_pred = preds[0][:, :5] if len(preds[0].shape) > 1 else preds[0][:5]
128
+ dme_pred = preds[0][:, 5:8] if len(preds[0].shape) > 1 else preds[0][5:8]
129
+
130
+ elif isinstance(preds, np.ndarray):
131
+ if len(preds.shape) == 2:
132
+ dr_pred = preds[:, :5]
133
+ dme_pred = preds[:, 5:8]
134
+ else:
135
+ dr_pred = preds[:5]
136
+ dme_pred = preds[5:8]
137
+
138
+ # Take first batch if batch dimension exists
139
+ if dr_pred is not None and len(dr_pred.shape) > 1:
140
+ dr_pred = dr_pred[0]
141
+ if dme_pred is not None and len(dme_pred.shape) > 1:
142
+ dme_pred = dme_pred[0]
143
 
144
+ if dr_pred is None:
145
+ dr_pred = np.zeros(5)
146
+ if dme_pred is None:
147
+ dme_pred = np.zeros(3)
148
+
149
+ # Apply softmax
150
+ dr_probs = ensure_probability(dr_pred)
151
+ dme_probs = ensure_probability(dme_pred)
152
+
153
+ # Get results
154
+ dr_idx = int(np.argmax(dr_probs))
155
+ dme_idx = int(np.argmax(dme_probs))
156
+
157
  dr_name = DR_CLASSES[dr_idx]
158
  dme_name = DME_CLASSES[dme_idx]
159
+
160
  dr_conf = float(dr_probs[dr_idx] * 100)
161
  dme_conf = float(dme_probs[dme_idx] * 100)
162
+
163
+ # Generate recommendations
164
+ if dr_name in ["No DR"]:
165
+ rec_dr = "Lanjutkan pola hidup sehat dan lakukan pemeriksaan mata rutin minimal 1 tahun sekali."
166
+ elif dr_name in ["Mild", "Moderate"]:
167
+ rec_dr = "Disarankan kontrol gula darah secara ketat dan pemeriksaan mata berkala setiap 6 bulan."
168
+ else: # Severe / Proliferative
169
+ rec_dr = "Disarankan segera konsultasi ke dokter spesialis mata untuk evaluasi dan penanganan lebih lanjut."
170
+
171
+ if dme_name == "No DME":
172
+ rec_dme = "Belum ditemukan tanda edema makula diabetik, lanjutkan pemantauan rutin."
173
+ elif dme_name == "Low Risk":
174
+ rec_dme = "Perlu observasi ketat dan pemeriksaan lanjutan untuk mencegah progresivitas."
175
+ else: # High Risk
176
+ rec_dme = "Disarankan segera mendapatkan evaluasi klinis dan terapi oleh dokter spesialis mata."
177
+
178
  return {
179
  "success": True,
180
+ "predictions": {
181
+ "diabetic_retinopathy": {
182
+ "classification": dr_name,
183
+ "confidence": dr_conf,
184
+ "index": dr_idx,
185
+ "probabilities": dr_probs.tolist(),
186
+ "recommendation": rec_dr
187
+ },
188
+ "diabetic_macular_edema": {
189
+ "classification": dme_name,
190
+ "confidence": dme_conf,
191
+ "index": dme_idx,
192
+ "probabilities": dme_probs.tolist(),
193
+ "recommendation": rec_dme
194
+ }
 
 
 
 
 
 
195
  }
196
  }
197
 
 
202
  }
203
 
204
  # ============================================================
205
+ # 6. CREATE FASTAPI APP
206
  # ============================================================
207
+ app = FastAPI(
208
+ title="DR & DME Detection API",
209
+ description="API untuk mendeteksi Diabetic Retinopathy dan Diabetic Macular Edema",
210
+ version="1.0.0"
211
+ )
212
+
213
+ # Enable CORS for mobile access
214
+ app.add_middleware(
215
+ CORSMiddleware,
216
+ allow_origins=["*"],
217
+ allow_credentials=True,
218
+ allow_methods=["*"],
219
+ allow_headers=["*"],
220
+ )
221
+
222
+ # ============================================================
223
+ # 7. GRADIO UI FUNCTIONS
224
+ # ============================================================
225
+ def format_prediction_html(result):
226
+ """Format prediction result as HTML for Gradio"""
227
  if not result["success"]:
228
  return f"""
229
+ <div style="color: red; padding: 20px; border: 2px solid red; border-radius: 10px;">
230
+ <h3>❌ Error</h3>
231
  <p>{result['error']}</p>
232
  </div>
233
  """
234
 
235
+ preds = result["predictions"]
236
+ dr = preds["diabetic_retinopathy"]
237
+ dme = preds["diabetic_macular_edema"]
238
 
239
+ dr_color = {
240
+ "No DR": "#28a745",
241
+ "Mild": "#ffc107",
242
+ "Moderate": "#fd7e14",
243
+ "Severe": "#dc3545",
244
+ "Proliferative DR": "#6f42c1"
245
+ }.get(dr["classification"], "#000000")
246
+
247
+ dme_color = {
248
+ "No DME": "#28a745",
249
+ "Low Risk": "#ffc107",
250
+ "High Risk": "#dc3545"
251
+ }.get(dme["classification"], "#000000")
252
+
253
+ html = f"""
254
+ <div style="font-family: Arial, sans-serif; max-width: 800px; margin: 0 auto;">
255
+ <div style="text-align: center; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
256
+ color: white; padding: 25px; border-radius: 15px 15px 0 0; margin-bottom: 20px;">
257
+ <h1 style="margin: 0; font-size: 32px;">πŸ”¬ HASIL DETEKSI</h1>
258
+ <p style="margin: 5px 0 0 0; font-size: 16px; opacity: 0.9;">AI-Powered Retina Analysis</p>
259
+ </div>
260
+
261
+ <div style="background: white; border-radius: 10px; box-shadow: 0 4px 12px rgba(0,0,0,0.1); overflow: hidden;">
262
+ <table style="width: 100%; border-collapse: collapse;">
263
  <thead>
264
+ <tr style="background-color: #f8f9fa;">
265
+ <th style="padding: 16px; text-align: left; border-bottom: 2px solid #dee2e6; font-size: 18px;">Kondisi</th>
266
+ <th style="padding: 16px; text-align: left; border-bottom: 2px solid #dee2e6; font-size: 18px;">Klasifikasi</th>
267
+ <th style="padding: 16px; text-align: left; border-bottom: 2px solid #dee2e6; font-size: 18px;">Tingkat Kepercayaan</th>
268
  </tr>
269
  </thead>
270
  <tbody>
271
  <tr>
272
+ <td style="padding: 16px; border-bottom: 1px solid #dee2e6; font-weight: bold;">Diabetic Retinopathy (DR)</td>
273
+ <td style="padding: 16px; border-bottom: 1px solid #dee2e6;">
274
+ <span style="color: {dr_color}; font-weight: bold; font-size: 18px;">{dr['classification']}</span>
275
+ </td>
276
+ <td style="padding: 16px; border-bottom: 1px solid #dee2e6;">
277
  <div style="display: flex; align-items: center; gap: 10px;">
278
+ <div style="flex-grow: 1; background: #e9ecef; height: 20px; border-radius: 10px; overflow: hidden;">
279
+ <div style="width: {dr['confidence']}%; background: {dr_color}; height: 100%;"></div>
280
  </div>
281
  <span style="font-weight: bold; min-width: 60px;">{dr['confidence']:.1f}%</span>
282
  </div>
283
  </td>
284
  </tr>
285
  <tr>
286
+ <td style="padding: 16px; border-bottom: 1px solid #dee2e6; font-weight: bold;">Diabetic Macular Edema (DME)</td>
287
+ <td style="padding: 16px; border-bottom: 1px solid #dee2e6;">
288
+ <span style="color: {dme_color}; font-weight: bold; font-size: 18px;">{dme['classification']}</span>
289
+ </td>
290
+ <td style="padding: 16px; border-bottom: 1px solid #dee2e6;">
291
  <div style="display: flex; align-items: center; gap: 10px;">
292
+ <div style="flex-grow: 1; background: #e9ecef; height: 20px; border-radius: 10px; overflow: hidden;">
293
+ <div style="width: {dme['confidence']}%; background: {dme_color}; height: 100%;"></div>
294
  </div>
295
  <span style="font-weight: bold; min-width: 60px;">{dme['confidence']:.1f}%</span>
296
  </div>
 
298
  </tr>
299
  </tbody>
300
  </table>
301
+ </div>
302
+
303
+ <div style="margin-top: 25px; background: white; border-radius: 10px; box-shadow: 0 4px 12px rgba(0,0,0,0.1); overflow: hidden;">
304
+ <div style="background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); color: white; padding: 15px;">
305
+ <h3 style="margin: 0; font-size: 22px;">🩺 REKOMENDASI KLINIS</h3>
306
+ </div>
307
+ <div style="padding: 20px;">
308
  <div style="margin-bottom: 15px;">
309
+ <h4 style="color: #333; margin-bottom: 8px;">β€’ Diabetic Retinopathy (DR):</h4>
310
+ <p style="margin: 0; color: #555; line-height: 1.6;">{dr['recommendation']}</p>
311
  </div>
 
312
  <div>
313
+ <h4 style="color: #333; margin-bottom: 8px;">β€’ Diabetic Macular Edema (DME):</h4>
314
+ <p style="margin: 0; color: #555; line-height: 1.6;">{dme['recommendation']}</p>
315
  </div>
316
  </div>
317
+ </div>
318
+
319
+ <div style="margin-top: 20px; padding: 15px; background: #fff3cd; border: 1px solid #ffeaa7; border-radius: 8px; font-size: 14px;">
320
+ <strong>⚠️ Disclaimer:</strong> Hasil ini merupakan prediksi AI dan bukan diagnosis medis. Konsultasikan dengan dokter spesialis mata untuk diagnosis yang akurat.
321
+ </div>
322
  </div>
323
  """
324
+ return html
325
+
326
+ def gradio_predict(image):
327
+ """Main function for Gradio UI"""
328
+ if image is None:
329
+ return "❌ Silakan unggah gambar fundus retina"
330
+
331
+ result = predict_image(image)
332
+ return format_prediction_html(result)
333
 
334
  # ============================================================
335
+ # 8. MULTI TEST IMAGES
336
+ # ============================================================
337
+ TEST_IMAGES = [
338
+ "IDRiD_001test.jpg",
339
+ "IDRiD_004test.jpg",
340
+ "IDRiD_005test.jpg",
341
+ "IDRiD_006test.jpg",
342
+ "IDRiD_007test.jpg",
343
+ "IDRiD_008test.jpg",
344
+ "IDRiD_009test.jpg",
345
+ "IDRiD_010test.jpg",
346
+ "IDRiD_011test.jpg",
347
+ "IDRiD_012test.jpg",
348
+ ]
349
+
350
+ TEST_IMAGES = [[p] for p in TEST_IMAGES if os.path.exists(p)]
351
+
352
+ # ============================================================
353
+ # 9. CREATE GRADIO APP
354
  # ============================================================
355
  with gr.Blocks(
 
 
 
 
 
 
356
  title="DR & DME Detection",
357
+ theme=gr.themes.Soft()
358
  ) as demo:
359
 
360
+ gr.Markdown("""
361
+ # 🩺 DETEKSI DIABETIC RETINOPATHY & DME
362
+ ### Sistem AI untuk Analisis Citra Fundus Retina
363
+
364
+ Upload gambar fundus retina untuk mendeteksi:
365
+ - **Diabetic Retinopathy (DR)**: Kerusakan retina akibat diabetes
366
+ - **Diabetic Macular Edema (DME)**: Pembengkakan di makula
 
 
367
  """)
368
 
369
+ with gr.Row():
370
+ with gr.Column(scale=1):
 
 
 
 
 
 
 
 
 
 
371
  image_input = gr.Image(
372
  type="pil",
373
+ label="πŸ“€ Upload Gambar Retina",
374
+ height=300
 
375
  )
376
 
377
+ upload_btn = gr.Button(
378
+ "πŸ” Analisis Gambar",
379
+ variant="primary",
380
+ size="lg"
381
  )
382
 
383
+ gr.Markdown("""
384
+ **Format yang didukung:** JPG, PNG, JPEG
385
+ **Ukuran rekomendasi:** 224Γ—224 piksel
386
+ **Warna:** RGB (akan dikonversi otomatis)
 
 
 
 
 
387
  """)
388
 
389
+ with gr.Column(scale=2):
 
390
  output_html = gr.HTML(
391
+ label="πŸ“Š Hasil Analisis",
392
+ value="<div style='text-align: center; padding: 50px; color: #666;'>Hasil analisis akan muncul di sini setelah mengupload gambar.</div>"
 
 
 
 
 
 
 
 
 
 
393
  )
394
+
395
+ gr.Markdown("### πŸ§ͺ Data Testing")
396
+ gr.Examples(
397
+ examples=TEST_IMAGES,
398
+ inputs=image_input
399
+ )
400
 
401
+ # API Info section
402
+ gr.Markdown("---")
403
+ with gr.Accordion("πŸ“± Akses API dari Mobile App", open=False):
404
  gr.Markdown("""
405
+ ### API Endpoints:
 
 
 
 
 
 
 
 
406
 
407
+ 1. **POST /api/predict** - Upload file gambar
408
+ ```bash
409
+ curl -X POST "https://kodetr-idrid.hf.space/api/predict" \\
410
+ -F "file=@retina_image.jpg"
411
+ ```
412
 
413
+ 2. **GET /api/health** - Health check
 
 
414
 
415
+ 3. **GET /api/info** - API info
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
416
  """)
417
 
418
+ upload_btn.click(
 
 
 
 
 
 
 
 
419
  fn=gradio_predict,
420
  inputs=image_input,
421
  outputs=output_html
422
  )
423
 
424
  # ============================================================
425
+ # 10. FASTAPI ENDPOINTS (under /api path)
426
  # ============================================================
427
+ @app.get("/api/info")
428
+ async def api_info():
429
+ """API info endpoint"""
430
+ return {
431
+ "message": "DR & DME Detection API",
432
+ "version": "1.0.0",
433
+ "endpoints": {
434
+ "docs": "/docs",
435
+ "health": "/api/health",
436
+ "predict": "/api/predict",
437
+ "ui": "/" # Gradio UI at root
438
+ }
439
+ }
 
 
 
 
440
 
441
+ @app.get("/api/health")
442
+ async def health_check():
443
+ """Health check endpoint"""
444
+ return {
445
+ "status": "healthy",
446
+ "model_loaded": best_model is not None
447
+ }
448
+
449
+ @app.post("/api/predict")
450
+ async def predict_endpoint(file: UploadFile = File(...)):
451
+ """
452
+ Predict endpoint for form-data file upload
453
+ Accepts: image file (jpg, png, jpeg)
454
+ """
455
+ try:
456
+ if not file.content_type.startswith('image/'):
457
+ raise HTTPException(status_code=400, detail="File must be an image")
458
+
459
+ contents = await file.read()
460
+ img = Image.open(io.BytesIO(contents)).convert("RGB")
461
+
462
+ result = predict_image(img)
463
+
464
+ if not result["success"]:
465
+ raise HTTPException(status_code=500, detail=result["error"])
466
+
467
+ return JSONResponse(content=result)
468
+
469
+ except HTTPException:
470
+ raise
471
+ except Exception as e:
472
+ raise HTTPException(status_code=500, detail=str(e))
473
 
474
  # ============================================================
475
+ # 11. MOUNT GRADIO TO ROOT PATH
476
+ # ============================================================
477
+ # Ini penting: Mount Gradio ke root path
478
+ app = gr.mount_gradio_app(app, demo, path="/")
479
+
480
+ # ============================================================
481
+ # 11. MAIN ENTRY POINT
482
  # ============================================================
483
  if __name__ == "__main__":
484
+ print("\n" + "="*60)
485
+ print("πŸš€ SERVER STARTING")
486
+ print("="*60)
487
+ print(f"πŸ–₯️ Gradio UI: http://0.0.0.0:7860/")
488
+ print(f"πŸ“± API Docs: http://0.0.0.0:7860/docs")
489
+ print(f"πŸ₯ Health Check: http://0.0.0.0:7860/api/health")
490
+ print(f"πŸ”§ API Info: http://0.0.0.0:7860/api/info")
491
+ print(f"πŸ“€ Predict: http://0.0.0.0:7860/api/predict")
492
+ print("="*60)
493
+
494
+ uvicorn.run(
495
+ app,
496
+ host="0.0.0.0",
497
+ port=7860,
498
+ log_level="info"
499
  )