oewis16 commited on
Commit
3fe2c4c
Β·
verified Β·
1 Parent(s): ef14cd5

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +187 -155
app.py CHANGED
@@ -1,168 +1,200 @@
1
  import gradio as gr
2
  import numpy as np
3
- import pandas as pd
4
  import matplotlib.pyplot as plt
5
- import os
6
-
7
- # Unified Classification Information (English)
8
- CLASS_INFO = {
9
- 0: {"name": "Normal Beat (N)", "icon": "βœ…", "color": "#27ae60", "risk": "Low Risk", "desc": "Normal cardiac rhythm."},
10
- 1: {"name": "Supraventricular Premature Beat (S)", "icon": "⚠️", "color": "#e67e22", "risk": "Medium Risk", "desc": "Arrhythmia originating above the ventricles."},
11
- 2: {"name": "Premature Ventricular Contraction (V)", "icon": "🚨", "color": "#e74c3c", "risk": "High Risk", "desc": "Early ventricular contraction β€” Critical condition."},
12
- 3: {"name": "Fusion of Ventricular and Normal (F)", "icon": "⚑", "color": "#8e44ad", "risk": "Medium-High Risk", "desc": "A beat that fuses normal and ventricular pathways."},
13
- 4: {"name": "Unclassifiable Beat (Q)", "icon": "❓", "color": "#7f8c8d", "risk": "Undetermined", "desc": "Unclassifiable morphology β€” Manual review required."},
 
 
 
 
 
 
 
 
 
14
  }
15
 
16
- MODEL_PATH = "cnn_ecg_final.keras"
17
- model = None
18
- model_status = ""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
 
20
- # Auto-load the model from local directory if it exists
21
- if os.path.exists(MODEL_PATH):
22
  try:
23
- from tensorflow.keras.models import load_model
24
- model = load_model(MODEL_PATH)
25
- model_status = "βœ… Local Keras Model Loaded Automatically!"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
  except Exception as e:
27
- model_status = f"❌ Error loading local model: {str(e)}"
28
- else:
29
- model_status = "πŸ”¬ Running in Demo Mode (Model file not found in directory)"
30
-
31
- def preprocess(signal):
32
- s = (signal - signal.mean()) / (signal.std() + 1e-8)
33
- return s.reshape(1, 187, 1).astype("float32")
34
-
35
- # Main inference and plotting function
36
- def process_ecg(input_text, csv_file, row_index):
37
- global model
38
- features = None
39
- true_label_text = "Not Provided"
40
-
41
- # 1. Data Extraction (CSV or Text Input)
42
- if csv_file is not None:
43
- try:
44
- df = pd.read_csv(csv_file.name, header=None)
45
- idx = int(row_index) if row_index < len(df) else 0
46
- row = df.iloc[idx]
47
- if df.shape[1] == 188:
48
- features = row.iloc[:-1].values.astype(float)
49
- true_cls = int(row.iloc[-1])
50
- true_label_text = f"{CLASS_INFO[true_cls]['icon']} {CLASS_INFO[true_cls]['name']}"
51
- elif df.shape[1] == 187:
52
- features = row.values.astype(float)
53
- except Exception as e:
54
- return None, f"Error parsing CSV: {str(e)}", "", "", "", model_status
55
- elif input_text.strip():
56
- try:
57
- vals = [float(x) for x in input_text.replace(",", " ").split() if x.strip()]
58
- if len(vals) == 187:
59
- features = np.array(vals)
60
- elif len(vals) == 188:
61
- features = np.array(vals[:-1])
62
- true_cls = int(vals[-1])
63
- true_label_text = f"{CLASS_INFO[true_cls]['icon']} {CLASS_INFO[true_cls]['name']}"
64
- else:
65
- return None, f"🚨 Input error: {len(vals)} values entered. Exactly 187 or 188 values are required.", "", "", "", model_status
66
- except ValueError:
67
- return None, "🚨 Error: Text contains non-numeric values.", "", "", "", model_status
68
-
69
- if features is None:
70
- return None, "⚠️ Please provide ECG signal data (Text or CSV file).", "", "", "", model_status
71
-
72
- # 2. Model Inference or Demo Mode
73
- if model is not None:
74
- probs = model.predict(preprocess(features), verbose=0)[0]
75
- pred_cls = int(np.argmax(probs))
76
- conf = float(probs[pred_cls]) * 100
77
- is_demo = False
78
- else:
79
- # Smart simulated inference for Demo Mode
80
- pred_cls = int(abs(features.sum()) % 5)
81
- np.random.seed(int(abs(features.sum()) * 100) % 9999)
82
- probs = np.random.dirichlet(np.ones(5))
83
- probs[pred_cls] = np.random.uniform(0.7, 0.95)
84
- probs /= probs.sum()
85
- conf = float(probs[pred_cls]) * 100
86
- is_demo = True
87
-
88
- pi = CLASS_INFO[pred_cls]
89
-
90
- # 3. Generate ECG Waveform Plot
91
- fig, ax = plt.subplots(figsize=(10, 3.5))
92
- ax.plot(features, color=pi["color"], lw=2)
93
- ax.fill_between(range(187), features, alpha=0.15, color=pi["color"])
94
- ax.set_title(f"ECG Waveform β€” Predicted: {pi['name']} ({conf:.1f}%)", fontsize=11, fontweight="bold", color=pi["color"])
95
- ax.set_xlabel("Time Steps")
96
- ax.set_ylabel("Amplitude")
97
- ax.grid(True, ls="--", alpha=0.4)
98
- ax.spines["top"].set_visible(False)
99
- ax.spines["right"].set_visible(False)
100
- plt.tight_layout()
101
-
102
- # 4. Prepare UI Output Elements
103
- result_html = f"""
104
- <div style='background: #f8f9fa; border-left: 8px solid {pi["color"]}; border-radius: 8px; padding: 15px; margin-top: 10px;'>
105
- <h2 style='color: {pi["color"]}; margin: 0;'>{pi["icon"]} {pi["name"]}</h2>
106
- <p style='font-size: 1.1em; color: #333;'><b>Diagnosis:</b> {pi["desc"]}</p>
107
- <p style='margin: 5px 0;'><b>Confidence Level:</b> <span style='font-size: 1.2em; color: {pi["color"]}; font-weight:bold;'>{conf:.2f}%</span></p>
108
- <p style='margin: 5px 0;'><b>Risk Level:</b> <span style='font-weight:bold;'>{pi["risk"]}</span> {' (πŸ”¬ Demo Mode)' if is_demo else ''}</p>
109
- </div>
110
- """
111
-
112
- # Medical Alerts
113
- if pred_cls == 2:
114
- alert_html = "<div style='background-color: #ffd2d2; color: #d8000c; padding: 10px; border-radius: 5px; font-weight: bold;'>🚨 Alert: Premature Ventricular Contraction detected β€” Consult a cardiologist immediately.</div>"
115
- elif pred_cls in [1, 3]:
116
- alert_html = "<div style='background-color: #ffe0b2; color: #e65100; padding: 10px; border-radius: 5px; font-weight: bold;'>⚠️ Warning: Cardiac arrhythmia detected β€” Medical follow-up is highly recommended.</div>"
117
- elif pred_cls == 4:
118
- alert_html = "<div style='background-color: #e0f7fa; color: #006064; padding: 10px; border-radius: 5px; font-weight: bold;'>ℹ️ Notice: Unclassifiable heartbeat β€” Manual review required.</div>"
119
- else:
120
- alert_html = "<div style='background-color: #d4edda; color: #155724; padding: 10px; border-radius: 5px; font-weight: bold;'>βœ… Normal heartbeat detected.</div>"
121
-
122
- label_dict = {CLASS_INFO[i]["name"]: float(probs[i]) for i in range(5)}
123
-
124
- return fig, result_html, alert_html, label_dict, true_label_text, model_status
125
-
126
- # ── Gradio Blocks UI Layout ──────────────────────
127
- with gr.Blocks() as demo:
128
- gr.HTML("""
129
- <div style='text-align:center; background: linear-gradient(135deg, #1a1a2e, #0f3460); color:white; padding:20px; border-radius:12px;'>
130
- <h1 style='margin:0;'>πŸ«€ ECG Heartbeat Classification</h1>
131
- <p style='margin:5px 0 0 0; opacity:0.8;'>CNN Model Β· MIT-BIH Dataset Β· 96.05% Accuracy</p>
132
- </div>
133
  """)
134
-
135
  with gr.Row():
136
  with gr.Column(scale=1):
137
- gr.Markdown("### πŸ” Model Status")
138
- status_out = gr.Textbox(value=model_status, label="Current System Status", interactive=False)
139
-
140
- gr.Markdown("### πŸ“ Input Data")
141
- with gr.Tab("Upload CSV"):
142
- csv_input = gr.File(label="Choose ECG CSV File", file_types=[".csv"])
143
- row_idx = gr.Number(label="Select Heartbeat Row Index", value=0, precision=0)
144
-
145
- with gr.Tab("Manual Input"):
146
- text_input = gr.Textbox(label="Paste 187 or 188 numbers", placeholder="e.g., 0.5 0.2 0.1 ...", lines=5)
147
-
148
- btn = gr.Button("πŸš€ Classify This Heartbeat", variant="primary")
149
-
150
- with gr.Column(scale=1):
151
- gr.Markdown("### πŸ“Š Output Results")
152
- plot_output = gr.Plot(label="ECG Waveform Graph")
153
- true_label_output = gr.Textbox(label="True Label (From Data)", interactive=False)
154
-
155
- result_output = gr.HTML()
156
- alert_output = gr.HTML()
157
- label_output = gr.Label(label="Class Probabilities")
158
-
159
- btn.click(
160
- fn=process_ecg,
161
- inputs=[text_input, csv_input, row_idx],
162
- outputs=[plot_output, result_output, alert_output, label_output, true_label_output, status_out]
 
 
 
 
163
  )
164
-
165
- gr.HTML("<p style='text-align:center; color:#999; font-size:0.85em; margin-top:20px;'>⚠️ For research and educational purposes only β€” Does not replace professional medical advice.</p>")
166
 
167
- if __name__ == "__main__":
168
- demo.launch(theme=gr.themes.Soft())
 
 
 
 
 
1
  import gradio as gr
2
  import numpy as np
 
3
  import matplotlib.pyplot as plt
4
+ from sklearn.preprocessing import MinMaxScaler
5
+ import tensorflow as tf
6
+ from tensorflow.keras import layers, models, regularizers
7
+
8
+ # ── Class Info ────────────────────────────────────────────────────────────────
9
+ CLASS_MAPPING = {
10
+ 0: "Normal Beat (N)",
11
+ 1: "Supraventricular Premature Beat (S)",
12
+ 2: "Premature Ventricular Contraction (V)",
13
+ 3: "Fusion of Ventricular and Normal Beat (F)",
14
+ 4: "Unclassifiable Beat (Q)"
15
+ }
16
+ CLASS_RISK = {
17
+ 0: "βœ… Normal β€” No action required",
18
+ 1: "⚠️ Moderate β€” Monitor and consult physician",
19
+ 2: "πŸ”΄ High Risk β€” Immediate medical attention recommended",
20
+ 3: "πŸ”΄ High Risk β€” Immediate medical attention recommended",
21
+ 4: "❓ Unknown β€” Further evaluation needed"
22
  }
23
 
24
+ # ── Build & Load Model ────────────────────────────────────────────────────────
25
+ def build_model():
26
+ model = models.Sequential([
27
+ layers.Input(shape=(187, 1)),
28
+ layers.Conv1D(64, 7, padding='same', activation='relu',
29
+ kernel_regularizer=regularizers.l2(1e-4)),
30
+ layers.BatchNormalization(),
31
+ layers.MaxPooling1D(2),
32
+ layers.Dropout(0.2),
33
+ layers.Conv1D(128, 5, padding='same', activation='relu',
34
+ kernel_regularizer=regularizers.l2(1e-4)),
35
+ layers.BatchNormalization(),
36
+ layers.MaxPooling1D(2),
37
+ layers.Dropout(0.25),
38
+ layers.Conv1D(256, 3, padding='same', activation='relu',
39
+ kernel_regularizer=regularizers.l2(1e-4)),
40
+ layers.BatchNormalization(),
41
+ layers.MaxPooling1D(2),
42
+ layers.Dropout(0.3),
43
+ layers.Conv1D(256, 3, padding='same', activation='relu',
44
+ kernel_regularizer=regularizers.l2(1e-4)),
45
+ layers.BatchNormalization(),
46
+ layers.GlobalAveragePooling1D(),
47
+ layers.Dropout(0.3),
48
+ layers.Dense(256, activation='relu',
49
+ kernel_regularizer=regularizers.l2(1e-4)),
50
+ layers.BatchNormalization(),
51
+ layers.Dropout(0.4),
52
+ layers.Dense(128, activation='relu',
53
+ kernel_regularizer=regularizers.l2(1e-4)),
54
+ layers.Dropout(0.3),
55
+ layers.Dense(5, activation='softmax')
56
+ ], name="CNN_ECG")
57
+ model.compile(optimizer='adam', loss='categorical_crossentropy', metrics=['accuracy'])
58
+ model.load_weights("cnn_weights.weights.h5")
59
+ return model
60
+
61
+ model = build_model()
62
 
63
+ # ── Predict Function ──────────────────────────────────────────────────────────
64
+ def predict_ecg(text_input):
65
  try:
66
+ # Parse input
67
+ cleaned = text_input.replace(",", " ").replace("\n", " ").replace("\t", " ")
68
+ parsed = [float(x) for x in cleaned.split() if x.strip()]
69
+ count = len(parsed)
70
+
71
+ if count not in (187, 188):
72
+ return None, f"❌ Expected 187 or 188 values, got {count}.", "", ""
73
+
74
+ features = np.array(parsed[:187], dtype=np.float32)
75
+ true_label = int(parsed[187]) if count == 188 else None
76
+
77
+ # Normalize + predict
78
+ scaler = MinMaxScaler()
79
+ signal_scaled = scaler.fit_transform(features.reshape(-1, 1)).reshape(1, 187, 1)
80
+ probs = model.predict(signal_scaled, verbose=0)[0]
81
+ pred_class = int(np.argmax(probs))
82
+ confidence = float(np.max(probs)) * 100
83
+
84
+ # ECG Plot
85
+ fig, axes = plt.subplots(1, 2, figsize=(14, 4))
86
+ fig.patch.set_facecolor('#0e1117')
87
+
88
+ # Signal
89
+ ax1 = axes[0]
90
+ ax1.set_facecolor('#1a1a2e')
91
+ ax1.plot(features, color='#00ff88', linewidth=1.8)
92
+ ax1.fill_between(range(187), features, alpha=0.15, color='#00ff88')
93
+ r_idx = int(np.argmax(features))
94
+ ax1.axvline(r_idx, color='red', linestyle='--', alpha=0.7)
95
+ ax1.scatter(r_idx, features[r_idx], color='red', s=80, zorder=5)
96
+ ax1.set_title("ECG Signal", color='white', fontsize=13, fontweight='bold')
97
+ ax1.set_xlabel("Time Steps", color='#aaa')
98
+ ax1.set_ylabel("Amplitude", color='#aaa')
99
+ ax1.tick_params(colors='#aaa')
100
+ ax1.grid(True, alpha=0.2, color='#444')
101
+ for spine in ax1.spines.values():
102
+ spine.set_edgecolor('#333')
103
+
104
+ # Probabilities
105
+ ax2 = axes[1]
106
+ ax2.set_facecolor('#1a1a2e')
107
+ colors = ['#2ecc71','#3498db','#e74c3c','#f39c12','#9b59b6']
108
+ bars = ax2.barh([CLASS_MAPPING[i] for i in range(5)],
109
+ probs * 100, color=colors, alpha=0.85)
110
+ for bar, val in zip(bars, probs):
111
+ ax2.text(val * 100 + 0.5, bar.get_y() + bar.get_height() / 2,
112
+ f"{val*100:.1f}%", va='center', color='white', fontsize=9)
113
+ ax2.set_xlabel("Probability (%)", color='#aaa')
114
+ ax2.set_title("Class Probabilities", color='white', fontsize=13, fontweight='bold')
115
+ ax2.set_xlim(0, 115)
116
+ ax2.tick_params(colors='#aaa')
117
+ ax2.grid(True, alpha=0.2, axis='x', color='#444')
118
+ for spine in ax2.spines.values():
119
+ spine.set_edgecolor('#333')
120
+
121
+ plt.tight_layout()
122
+
123
+ # Result text
124
+ result_text = f"""
125
+ **πŸ€– Predicted Class:** {CLASS_MAPPING[pred_class]}
126
+ **πŸ“Š Confidence:** {confidence:.2f}%
127
+ **βš•οΈ Risk:** {CLASS_RISK[pred_class]}
128
+ """.strip()
129
+
130
+ true_text = ""
131
+ if true_label is not None:
132
+ match = "βœ… Correct!" if pred_class == true_label else "❌ Incorrect"
133
+ true_text = f"**🏷️ True Label:** {CLASS_MAPPING.get(true_label, 'Unknown')} β€” {match}"
134
+
135
+ stats_text = f"""
136
+ **Signal Stats:**
137
+ - R-peak amplitude: {features.max():.4f} @ timestep {r_idx}
138
+ - Min amplitude: {features.min():.4f}
139
+ - Mean: {features.mean():.4f}
140
+ - Std Dev: {features.std():.4f}
141
+ """.strip()
142
+
143
+ return fig, result_text, true_text, stats_text
144
+
145
  except Exception as e:
146
+ return None, f"❌ Error: {str(e)}", "", ""
147
+
148
+ # ── Gradio UI ─────────────────────────────────────────────────────────────────
149
+ with gr.Blocks(
150
+ title="ECG Arrhythmia Classification",
151
+ theme=gr.themes.Base(primary_hue="green")
152
+ ) as demo:
153
+
154
+ gr.Markdown("""
155
+ # πŸ«€ ECG Arrhythmia Classification System
156
+ **Smart Medical System powered by Deep Learning (1D CNN)**
157
+ > MIT-BIH Arrhythmia Dataset | Accuracy: **97.25%** | Macro F1: **0.8796**
158
+ ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
159
  """)
160
+
161
  with gr.Row():
162
  with gr.Column(scale=1):
163
+ gr.Markdown("### πŸ“₯ Input ECG Signal")
164
+ text_input = gr.Textbox(
165
+ label="Paste 187 or 188 ECG values (comma or space or newline separated)",
166
+ placeholder="0.5, 0.8, 0.3, 1.0, 0.2 ...\n(187 signal values, optionally followed by class label)",
167
+ lines=8
168
+ )
169
+ predict_btn = gr.Button("β–Ά Run Prediction", variant="primary", size="lg")
170
+
171
+ gr.Markdown("""
172
+ ### πŸ“‹ Classes
173
+ | Label | Class | Risk |
174
+ |-------|-------|------|
175
+ | 0 | Normal (N) | βœ… None |
176
+ | 1 | Supraventricular (S) | ⚠️ Moderate |
177
+ | 2 | Ventricular (V) | πŸ”΄ High |
178
+ | 3 | Fusion (F) | πŸ”΄ High |
179
+ | 4 | Unknown (Q) | ❓ Unknown |
180
+ """)
181
+
182
+ with gr.Column(scale=2):
183
+ gr.Markdown("### πŸ“ˆ Visualization & Results")
184
+ plot_out = gr.Plot(label="ECG Signal & Probabilities")
185
+ result_out = gr.Markdown(label="Prediction Result")
186
+ true_out = gr.Markdown(label="True Label Check")
187
+ stats_out = gr.Markdown(label="Signal Statistics")
188
+
189
+ predict_btn.click(
190
+ fn=predict_ecg,
191
+ inputs=[text_input],
192
+ outputs=[plot_out, result_out, true_out, stats_out]
193
  )
 
 
194
 
195
+ gr.Markdown("""
196
+ ---
197
+ *DEPI Final Project 2025 | ECG Arrhythmia Classification | CNN Model*
198
+ """)
199
+
200
+ demo.launch()