AlvinSiang commited on
Commit
f89ff89
Β·
verified Β·
1 Parent(s): 967ec39

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +366 -0
app.py ADDED
@@ -0,0 +1,366 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import tensorflow as tf
3
+ import numpy as np
4
+ import cv2
5
+ from PIL import Image
6
+ import os
7
+
8
+ # ==============================
9
+ # REGISTER CUSTOM LAYERS
10
+ # ==============================
11
+ import tensorflow as tf
12
+ from tensorflow.keras.layers import Layer
13
+ from tensorflow.keras.utils import register_keras_serializable
14
+
15
+ @register_keras_serializable(package="Custom")
16
+ class ChannelMeanPooling(Layer):
17
+ def call(self, inputs):
18
+ return tf.reduce_mean(inputs, axis=3, keepdims=True)
19
+
20
+ @register_keras_serializable(package="Custom")
21
+ class ChannelMaxPooling(Layer):
22
+ def call(self, inputs):
23
+ return tf.reduce_max(inputs, axis=3, keepdims=True)
24
+
25
+
26
+ # ==============================
27
+ # CONFIGURATION
28
+ # ==============================
29
+ IMG_SIZE = 224
30
+ CLASS_NAMES = ["cataracts", "diabetic retinopathy", "glaucoma", "normal"]
31
+
32
+ MODEL_CONFIG = {
33
+ "VGG16": {
34
+ "path": os.path.join("models", "SpaAtt_vgg16_model.keras"),
35
+ "preprocess": tf.keras.applications.vgg16.preprocess_input,
36
+ },
37
+ "Inception-v3": {
38
+ "path": os.path.join("models", "SpaAtt_inceptionv3_model.keras"),
39
+ "preprocess": tf.keras.applications.inception_v3.preprocess_input,
40
+ },
41
+ "ResNet50": {
42
+ "path": os.path.join("models", "SpaAtt_resnet50_model.keras"),
43
+ "preprocess": tf.keras.applications.resnet50.preprocess_input,
44
+ },
45
+ "DenseNet121": {
46
+ "path": os.path.join("models", "SpaAtt_densenet121_model.keras"),
47
+ "preprocess": tf.keras.applications.densenet.preprocess_input,
48
+ },
49
+ "EfficientNet-B0": {
50
+ "path": os.path.join("models", "SpaAtt_efficientnetb0_model.keras"),
51
+ "preprocess": tf.keras.applications.efficientnet.preprocess_input,
52
+ },
53
+ }
54
+
55
+
56
+ # ==============================
57
+ # LOAD MODELS (ONCE)
58
+ # ==============================
59
+ custom_objects = {
60
+ "ChannelMeanPooling": ChannelMeanPooling,
61
+ "ChannelMaxPooling": ChannelMaxPooling,
62
+ }
63
+
64
+ MODELS = {}
65
+
66
+ for name, cfg in MODEL_CONFIG.items():
67
+ if not os.path.exists(cfg["path"]):
68
+ raise FileNotFoundError(f"Model not found: {cfg['path']}")
69
+ MODELS[name] = tf.keras.models.load_model(
70
+ cfg["path"],
71
+ custom_objects = custom_objects,
72
+ safe_mode = False
73
+ )
74
+ print(f"Loaded model: {name}")
75
+
76
+
77
+ # ==============================
78
+ # IMAGE PROCESSING UTILITIES
79
+ # ==============================
80
+ def extract_retinal_fov(img_rgb):
81
+ """
82
+ Extract circular retinal field of view using brightness mask.
83
+ """
84
+ gray = cv2.cvtColor(img_rgb, cv2.COLOR_RGB2GRAY)
85
+ _, thresh = cv2.threshold(gray, 15, 255, cv2.THRESH_BINARY)
86
+
87
+ contours, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
88
+ if not contours:
89
+ return img_rgb
90
+
91
+ c = max(contours, key = cv2.contourArea)
92
+ x, y, w, h = cv2.boundingRect(c)
93
+ return img_rgb[y:y+h, x:x+w]
94
+
95
+ def clahe_l_channel(img_rgb, clip = 2.0):
96
+ lab = cv2.cvtColor(img_rgb, cv2.COLOR_RGB2LAB)
97
+ l, a, b = cv2.split(lab)
98
+ clahe = cv2.createCLAHE(clipLimit = clip, tileGridSize = (8,8))
99
+ l = clahe.apply(l)
100
+ lab = cv2.merge((l, a, b))
101
+ return cv2.cvtColor(lab, cv2.COLOR_LAB2RGB)
102
+
103
+ def enhance_vessels(img_rgb, ksize = 15):
104
+ gray = cv2.cvtColor(img_rgb, cv2.COLOR_RGB2GRAY)
105
+ kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (ksize, ksize))
106
+ top_hat = cv2.morphologyEx(gray, cv2.MORPH_TOPHAT, kernel)
107
+ black_hat = cv2.morphologyEx(gray, cv2.MORPH_BLACKHAT, kernel)
108
+ enhanced = cv2.add(gray, top_hat)
109
+ enhanced = cv2.subtract(enhanced, black_hat)
110
+ enhanced = cv2.normalize(enhanced, None, 0, 255, cv2.NORM_MINMAX)
111
+ return cv2.cvtColor(enhanced, cv2.COLOR_GRAY2RGB)
112
+
113
+ def enhance_optic_disc(img_rgb, ksize = 30):
114
+ gray = cv2.cvtColor(img_rgb, cv2.COLOR_RGB2GRAY)
115
+ kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (ksize, ksize))
116
+ closing = cv2.morphologyEx(gray, cv2.MORPH_CLOSE, kernel)
117
+ disc = cv2.subtract(gray, closing)
118
+ disc = cv2.normalize(disc, None, 0, 255, cv2.NORM_MINMAX)
119
+ return cv2.cvtColor(disc, cv2.COLOR_GRAY2RGB)
120
+
121
+ def full_enhancement_pipeline(img_rgb):
122
+ img = extract_retinal_fov(img_rgb)
123
+ img = cv2.resize(img, (IMG_SIZE, IMG_SIZE))
124
+ img = clahe_l_channel(img)
125
+ vessels = enhance_vessels(img)
126
+ disc = enhance_optic_disc(img)
127
+
128
+ #Blend enhancements
129
+ img = cv2.addWeighted(img, 0.85, vessels, 0.15, 0)
130
+ img = cv2.addWeighted(img, 0.85, disc, 0.15, 0)
131
+ return img
132
+
133
+
134
+ # ==============================
135
+ # ENSEMBLE PREDICTION
136
+ # ==============================
137
+ def ensemble_predict(input_image):
138
+ if input_image is None:
139
+ return None, "❌ No image uploaded."
140
+
141
+ #Convert to RGB
142
+ img_rgb = np.array(input_image.convert("RGB"))
143
+ img = full_enhancement_pipeline(img_rgb)
144
+
145
+ probs = []
146
+
147
+ for model_name, model in MODELS.items():
148
+ preprocess = MODEL_CONFIG[model_name]["preprocess"]
149
+
150
+ img_input = np.expand_dims(img, axis = 0)
151
+ img_input = preprocess(img_input.astype(np.float32))
152
+
153
+ pred = model.predict(img_input, verbose = 0)[0]
154
+ probs.append(pred)
155
+
156
+ #Soft Voting (Average Probabilities)
157
+ probs = np.array(probs)
158
+ mean_probs = probs.mean(axis = 0)
159
+
160
+ result = {CLASS_NAMES[i]: float(mean_probs[i]) for i in range(len(CLASS_NAMES))}
161
+ predicted_class_name = CLASS_NAMES[int(np.argmax(mean_probs))]
162
+
163
+ return result, f"βœ… Prediction: **{predicted_class_name.upper()}**"
164
+
165
+
166
+ # ==============================
167
+ # GRADIO UI
168
+ # ==============================
169
+ js = """
170
+ function createGradioAnimation() {
171
+ const run = () => {
172
+ var container = document.createElement('div');
173
+ container.id = 'gradio-animation';
174
+ container.style.fontSize = '2em';
175
+ container.style.fontWeight = 'bold';
176
+ container.style.textAlign = 'center';
177
+ container.style.marginBottom = '20px';
178
+
179
+ var text = 'A-EYE: An Intelligent Eye Disease Classifier';
180
+
181
+ for (var i = 0; i < text.length; i++) {
182
+ setTimeout(function(){
183
+ var letter = document.createElement('span');
184
+ letter.style.opacity = '0';
185
+ letter.style.transition = 'opacity 0.5s';
186
+ letter.innerText = text[i];
187
+
188
+ container.appendChild(letter);
189
+
190
+ setTimeout(() {
191
+ letter.style.opacity = '1';
192
+ }, 50);
193
+ }, i * 100);
194
+ }
195
+ const gradioContainer = document.querySelector('.gradio-container');
196
+ if (gradioContainer){
197
+ gradioContainer.insertBefore(container, gradioContainer.firstChild);
198
+ }
199
+ };
200
+ window.addEventListener("load", function(){
201
+ createGradioAnimation();
202
+ });
203
+ }
204
+ """
205
+
206
+ css = """
207
+ #banner-img img {
208
+ width: 100% !important;
209
+ height: auto !important;
210
+ max-height: 280px;
211
+ object-fit: contain;
212
+ }
213
+
214
+ #input-img img {
215
+ width: 300px !important;
216
+ height: 300px !important;
217
+ object-fit: contain;
218
+ }
219
+
220
+ #output-label {
221
+ height: 300px !important;
222
+ display: flex;
223
+ flex-direction: column;
224
+ justify-content: center; /*vertical alignment*/
225
+ }
226
+
227
+ #input-box img {
228
+ width: 100%;
229
+ height: 100%;
230
+ object-fit: contain; /* preserves aspect ratio */
231
+ }
232
+
233
+ #output-box > div {
234
+ height: 100%;
235
+ display: flex;
236
+ flex-direction: column;
237
+ justify-content: center;
238
+ }
239
+
240
+ /* Hide all image action buttons (download, share, etc.) */
241
+ button[aria-label="Download"],
242
+ button[aria-label="Share"],
243
+ button[aria-label="Open in new tab"] {
244
+ display: none !important;
245
+ }
246
+
247
+ /* Also hide top-right image toolbar if present */
248
+ .gradio-container .absolute.top-0.right-0 {
249
+ display: none !important;
250
+ }
251
+ """
252
+
253
+
254
+ #Background
255
+ background_img = Image.open('background.png').resize((3000, 700))
256
+
257
+ with gr.Blocks() as demo:
258
+ gr.HTML("""
259
+ <link href="https://fonts.googleapis.com/css2?family=Poppins:wght@600&display=swap" rel="stylesheet">
260
+
261
+ <style>
262
+ @keyframes fadeIn{
263
+ from { opacity: 0; transform: translateY(-10px); }
264
+ to { opacity: 1; transform: translateY(0);}
265
+ }
266
+ .title {
267
+ font-family: 'Poppins', sans-serif;
268
+ text-align: center;
269
+ font-size: 36px;
270
+ color: #2c3e50;
271
+ animation: fadeIn 1.5s ease-in-out;
272
+ }
273
+ </style>
274
+
275
+ <div class="title">
276
+ A-EYE: An Intelligent Eye Disease Classifier
277
+ </div>
278
+ """)
279
+
280
+ with gr.Row():
281
+ gr.Image(
282
+ background_img,
283
+ interactive = False,
284
+ elem_id = "banner-img",
285
+ #show_download_button = False,
286
+ #show_share_button = False,
287
+ )
288
+
289
+ with gr.Row():
290
+ gr.Markdown(
291
+ """
292
+ <div style="text-align: justify;">
293
+ A-EYE is an intelligent eye disease classifier designed for accurate eye disease classification using deep learning.
294
+ With its enhanced model, A-EYE analyzes fundus images to detect Cataracts, Diabetic Retinopathy, Glaucoma, or Normal conditions.
295
+ Users can easily upload a fundus image, and the system will process it to provide detailed classification results.
296
+ The results display probability percentages for each condition, ensuring transparency and confidence in the diagnosis.
297
+ The condition with the highest probability is assigned to the image, offering a reliable and efficient tool for eye health assessment.
298
+ A-EYE has the potential to assist ophthalmologists in providing first screening of suspected eye diseases.
299
+ </div>
300
+ """
301
+ )
302
+ # gr.HTML("""
303
+ # <div style="
304
+ # #max-width: 1000px;
305
+ # width: 100%
306
+ # margin: 10px auto;
307
+ # padding: 10px 15px;
308
+ # background: #f8fafc;
309
+ # border-radius: 12px;
310
+ # box-shadow: 0 4px 12px rgba(0,0,0,0.05);
311
+ # font-family: 'Inter', sans-serif;
312
+ # line-height: 1.6;
313
+ # color: #2c3e50;
314
+ # ">
315
+ # <h3 style="text-align: center; margin-bottom: 15px;">
316
+ # πŸ” About A-EYE
317
+ # </h3>
318
+
319
+ # <p style="text-align: justify;">
320
+ # <b>A-EYE</b> is an intelligent eye disease classifier designed for accurate eye disease classification using
321
+ # <b>five attention-enhanced deep learning models</b> combined via a
322
+ # <span style="color:#2563eb; font-weight:600;">soft-voting ensemble</span>.
323
+ # </p>
324
+ # <hr style="border: none; border-top: 1px solid #e0e0e0; margin: 15px 0;">
325
+ # <p style="text-align: justify;">
326
+ # It analyzes fundus images to detect:
327
+ # <b>🟑 Cataracts</b>, <b>πŸ”΄ Diabetic Retinopathy</b>, <b>πŸ”΅ Glaucoma</b>, or <b>🟒 Normal</b> conditions.
328
+ # </p>
329
+ # <hr style="border: none; border-top: 1px solid #e0e0e0; margin: 15px 0;">
330
+ # <p style="text-align: justify;">
331
+ # User can easily upload a fundus image, and A-EYE will generate classification results with <b>probability scores</b> for each condition - ensuring transparency and confidence.
332
+ # </p>
333
+
334
+ # <p style="text-align: justify;">
335
+ # The highest probability determines the final prediction, offering a reliable tool for <b>early screening and decision support</b>.
336
+ # </p>
337
+ # <hr style="border: none; border-top: 1px solid #e0e0e0; margin: 15px 0;">
338
+ # <p style="text-align: justify;">
339
+ # πŸ’‘ <i>A-EYE is designed to assist ophthalmologists in providing first screening of eye diseases.</i>
340
+ # </p>
341
+ # </div>
342
+ # """
343
+ # )
344
+
345
+ with gr.Row():
346
+ with gr.Column():
347
+ img_input = gr.Image(
348
+ type = "pil",
349
+ label = "Upload Fundus Image",
350
+ elem_id = "input-img"
351
+ )
352
+ predict_button = gr.Button("πŸ” Predict")
353
+ with gr.Column():
354
+ output_label = gr.Label(
355
+ num_top_classes = 4,
356
+ elem_id = "output-label"
357
+ )
358
+ output_text = gr.Markdown()
359
+
360
+ predict_button.click(
361
+ fn = ensemble_predict,
362
+ inputs = img_input,
363
+ outputs = [output_label, output_text]
364
+ )
365
+
366
+ demo.launch(debug = True, share = True, theme=gr.themes.Soft(), css=css, js = js)