File size: 7,001 Bytes
ece8c53
 
69ee924
 
ece8c53
3fe2c4c
69ee924
f784919
69ee924
 
3fe2c4c
 
 
 
 
 
 
 
 
 
 
69ee924
 
3fe2c4c
ece8c53
69ee924
ece8c53
26924da
 
 
 
 
 
 
 
 
b9fe86b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26924da
ece8c53
 
3fe2c4c
 
 
 
 
 
 
69ee924
 
3fe2c4c
 
 
26924da
 
 
3fe2c4c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
69ee924
3fe2c4c
 
 
b9fe86b
 
3fe2c4c
69ee924
3fe2c4c
 
 
 
 
 
69ee924
3fe2c4c
 
 
b9fe86b
69ee924
3fe2c4c
b9fe86b
69ee924
 
3fe2c4c
69ee924
3fe2c4c
ece8c53
3fe2c4c
 
b9fe86b
69ee924
3fe2c4c
69ee924
 
 
 
ece8c53
 
b9fe86b
 
 
 
 
69ee924
3fe2c4c
69ee924
 
 
 
 
 
 
 
3fe2c4c
 
69ee924
 
 
3fe2c4c
b9fe86b
 
 
 
 
69ee924
3fe2c4c
c8ace6a
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
import gradio as gr
import numpy as np
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
from sklearn.preprocessing import MinMaxScaler
import os
import spaces

os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3'

CLASS_MAPPING = {
    0: "Normal Beat (N)",
    1: "Supraventricular Premature Beat (S)",
    2: "Premature Ventricular Contraction (V)",
    3: "Fusion of Ventricular and Normal Beat (F)",
    4: "Unclassifiable Beat (Q)"
}
CLASS_RISK = {
    0: "βœ… Normal β€” No action required",
    1: "⚠️ Moderate β€” Monitor and consult physician",
    2: "πŸ”΄ High Risk β€” Immediate medical attention",
    3: "πŸ”΄ High Risk β€” Immediate medical attention",
    4: "❓ Unknown β€” Further evaluation needed"
}
COLORS = ['#2ecc71','#3498db','#e74c3c','#f39c12','#9b59b6']

# ── Load model lazily inside GPU function ─────────────────────────────────────
model = None

@spaces.GPU
def predict_ecg(text_input):
    global model
    if model is None:
        import tensorflow as tf
        from tensorflow.keras import layers, models, regularizers
        m = models.Sequential([
            layers.Input(shape=(187, 1)),
            layers.Conv1D(64, 7, padding='same', activation='relu',
                          kernel_regularizer=regularizers.l2(1e-4)),
            layers.BatchNormalization(), layers.MaxPooling1D(2), layers.Dropout(0.2),
            layers.Conv1D(128, 5, padding='same', activation='relu',
                          kernel_regularizer=regularizers.l2(1e-4)),
            layers.BatchNormalization(), layers.MaxPooling1D(2), layers.Dropout(0.25),
            layers.Conv1D(256, 3, padding='same', activation='relu',
                          kernel_regularizer=regularizers.l2(1e-4)),
            layers.BatchNormalization(), layers.MaxPooling1D(2), layers.Dropout(0.3),
            layers.Conv1D(256, 3, padding='same', activation='relu',
                          kernel_regularizer=regularizers.l2(1e-4)),
            layers.BatchNormalization(), layers.GlobalAveragePooling1D(), layers.Dropout(0.3),
            layers.Dense(256, activation='relu', kernel_regularizer=regularizers.l2(1e-4)),
            layers.BatchNormalization(), layers.Dropout(0.4),
            layers.Dense(128, activation='relu', kernel_regularizer=regularizers.l2(1e-4)),
            layers.Dropout(0.3),
            layers.Dense(5, activation='softmax')
        ], name="CNN_ECG")
        m.compile(optimizer='adam', loss='categorical_crossentropy', metrics=['accuracy'])
        m.load_weights("cnn_weights.weights.h5")
        model = m

    try:
        cleaned = text_input.replace(",", " ").replace("\n", " ").replace("\t", " ")
        parsed  = [float(x) for x in cleaned.split() if x.strip()]
        count   = len(parsed)

        if count not in (187, 188):
            return None, f"❌ Expected 187 or 188 values, got {count}.", "", ""

        features   = np.array(parsed[:187], dtype=np.float32)
        true_label = int(parsed[187]) if count == 188 else None

        scaler        = MinMaxScaler()
        signal_scaled = scaler.fit_transform(features.reshape(-1, 1)).reshape(1, 187, 1)
        probs         = model.predict(signal_scaled, verbose=0)[0]
        pred_class    = int(np.argmax(probs))
        confidence    = float(np.max(probs)) * 100

        fig, axes = plt.subplots(1, 2, figsize=(14, 4))
        fig.patch.set_facecolor('#0e1117')

        ax1 = axes[0]
        ax1.set_facecolor('#1a1a2e')
        ax1.plot(features, color='#00ff88', linewidth=1.8)
        ax1.fill_between(range(187), features, alpha=0.15, color='#00ff88')
        r_idx = int(np.argmax(features))
        ax1.axvline(r_idx, color='red', linestyle='--', alpha=0.7)
        ax1.scatter(r_idx, features[r_idx], color='red', s=80, zorder=5)
        ax1.set_title("ECG Signal", color='white', fontsize=13, fontweight='bold')
        ax1.set_xlabel("Time Steps", color='#aaa')
        ax1.set_ylabel("Amplitude", color='#aaa')
        ax1.tick_params(colors='#aaa')
        ax1.grid(True, alpha=0.2, color='#444')
        for sp in ax1.spines.values(): sp.set_edgecolor('#333')

        ax2 = axes[1]
        ax2.set_facecolor('#1a1a2e')
        bars = ax2.barh([CLASS_MAPPING[i] for i in range(5)],
                        probs * 100, color=COLORS, alpha=0.85)
        for bar, val in zip(bars, probs):
            ax2.text(val*100+0.5, bar.get_y()+bar.get_height()/2,
                     f"{val*100:.1f}%", va='center', color='white', fontsize=9)
        ax2.set_xlabel("Probability (%)", color='#aaa')
        ax2.set_title("Class Probabilities", color='white', fontsize=13, fontweight='bold')
        ax2.set_xlim(0, 115)
        ax2.tick_params(colors='#aaa')
        ax2.grid(True, alpha=0.2, axis='x', color='#444')
        for sp in ax2.spines.values(): sp.set_edgecolor('#333')

        plt.tight_layout()

        result   = f"**πŸ€– Predicted:** {CLASS_MAPPING[pred_class]}\n\n**πŸ“Š Confidence:** {confidence:.2f}%\n\n**βš•οΈ Risk:** {CLASS_RISK[pred_class]}"
        true_out = ""
        if true_label is not None:
            match    = "βœ… Correct!" if pred_class == true_label else "❌ Incorrect"
            true_out = f"**🏷️ True Label:** {CLASS_MAPPING.get(true_label,'Unknown')} β€” {match}"
        stats = f"**Signal Stats:** R-peak={features.max():.4f} @ t={r_idx} | Min={features.min():.4f} | Mean={features.mean():.4f} | Std={features.std():.4f}"

        return fig, result, true_out, stats

    except Exception as e:
        return None, f"❌ Error: {str(e)}", "", ""

# ── UI ────────────────────────────────────────────────────────────────────────
with gr.Blocks(title="ECG Classification") as demo:
    gr.Markdown("""
# πŸ«€ ECG Arrhythmia Classification
**1D CNN | MIT-BIH Dataset | Accuracy: 97.25% | Macro F1: 0.8796**
---
""")
    with gr.Row():
        with gr.Column(scale=1):
            text_input  = gr.Textbox(
                label="Paste 187 or 188 ECG values",
                lines=8,
                placeholder="0.5, 0.8, 0.3, 1.0 ..."
            )
            predict_btn = gr.Button("β–Ά Run Prediction", variant="primary")
            gr.Markdown("""
| Label | Class | Risk |
|-------|-------|------|
| 0 | Normal (N) | βœ… |
| 1 | Supraventricular (S) | ⚠️ |
| 2 | Ventricular (V) | πŸ”΄ |
| 3 | Fusion (F) | πŸ”΄ |
| 4 | Unknown (Q) | ❓ |
""")
        with gr.Column(scale=2):
            plot_out   = gr.Plot(label="ECG Signal & Probabilities")
            result_out = gr.Markdown()
            true_out   = gr.Markdown()
            stats_out  = gr.Markdown()

    predict_btn.click(
        predict_ecg,
        inputs=[text_input],
        outputs=[plot_out, result_out, true_out, stats_out]
    )
    gr.Markdown("*DEPI Final Project 2025*")

demo.launch(ssr_mode=False)