Spaces:
Sleeping
Sleeping
| import gradio as gr | |
| import numpy as np | |
| import pandas as pd | |
| from io import BytesIO | |
| import os | |
| os.environ["TF_CPP_MIN_LOG_LEVEL"] = "2" | |
| import tensorflow as tf | |
| tf.config.set_visible_devices([], 'GPU') | |
| import matplotlib | |
| matplotlib.use('Agg') | |
| import matplotlib.pyplot as plt | |
| from scipy.signal import find_peaks, resample | |
| from PIL import Image | |
| # ----------- LOAD MODEL ----------- | |
| model = tf.keras.models.load_model("ecg_cnn_model.h5", compile=False) | |
| # Auto-detect what input length the model expects | |
| MODEL_INPUT_SHAPE = model.input_shape # e.g. (None, 187) or (None, 6016) etc. | |
| if isinstance(MODEL_INPUT_SHAPE, list): | |
| MODEL_INPUT_SHAPE = MODEL_INPUT_SHAPE[0] | |
| EXPECTED_LEN = MODEL_INPUT_SHAPE[-1] | |
| print(f"[CardioSense] Model expects input length: {EXPECTED_LEN}") | |
| # ----------- PREPROCESS ----------- | |
| def preprocess(filepath): | |
| try: | |
| data = pd.read_csv(filepath, encoding='latin1', on_bad_lines='skip') | |
| except Exception: | |
| return None | |
| data.columns = data.columns.str.replace("'", "").str.strip().str.upper() | |
| if data.shape[1] < 1: | |
| return None | |
| if 'MLII' in data.columns: | |
| signal = data['MLII'].dropna().values | |
| elif data.shape[1] >= 2: | |
| signal = data.iloc[:, 1].dropna().values | |
| else: | |
| signal = data.iloc[:, 0].dropna().values | |
| if len(signal) < 100: | |
| return None | |
| signal = signal.astype(np.float32) | |
| signal = (signal - np.mean(signal)) / (np.std(signal) + 1e-8) | |
| # Resample raw signal to exactly what the model expects | |
| raw_segment = signal[:min(len(signal), EXPECTED_LEN * 4)] # grab a generous chunk | |
| resampled = resample(raw_segment, EXPECTED_LEN).astype(np.float32) | |
| segment = resampled.reshape(1, EXPECTED_LEN) | |
| return segment, signal | |
| # ----------- PREDICT ----------- | |
| def predict_ecg(file): | |
| if file is None: | |
| return "❌ No file uploaded.", None | |
| result_data = preprocess(file.name) | |
| if result_data is None: | |
| return "❌ Invalid ECG file. Make sure it has at least 100 rows of numeric data.", None | |
| segment, signal = result_data | |
| try: | |
| pred = model.predict(segment, verbose=0)[0][0] | |
| except Exception as e: | |
| return f"❌ Prediction failed: {str(e)}", None | |
| confidence = round(float(pred) * 100, 2) if pred > 0.5 else round((1 - float(pred)) * 100, 2) | |
| result = "Abnormal" if pred > 0.5 else "Normal" | |
| # Heart rate estimation | |
| peaks, _ = find_peaks(signal, distance=150, height=0.5) | |
| if len(peaks) > 1 and np.mean(np.diff(peaks)) > 0: | |
| rr = np.diff(peaks) / 360.0 | |
| heart_rate = int(60 / np.mean(rr)) | |
| heart_rate = max(30, min(220, heart_rate)) # clamp to physiological range | |
| else: | |
| heart_rate = None | |
| # Plot | |
| plot_len = min(500, len(signal)) | |
| plt.figure(figsize=(8, 3)) | |
| plt.plot(signal[:plot_len], color='royalblue', label="ECG Signal", linewidth=0.8) | |
| for p in peaks: | |
| if p < plot_len: | |
| plt.plot(p, signal[p], "ro", markersize=4) | |
| bpm_label = f"{heart_rate} BPM" if heart_rate else "-- BPM" | |
| plt.title(f"ECG Signal ({bpm_label})", fontsize=13) | |
| plt.xlabel("Sample") | |
| plt.ylabel("Amplitude") | |
| plt.grid(alpha=0.3) | |
| plt.tight_layout() | |
| img_buf = BytesIO() | |
| plt.savefig(img_buf, format='png', dpi=120) | |
| img_buf.seek(0) | |
| plt.close() | |
| pil_img = Image.open(img_buf) | |
| emoji = "🔴" if result == "Abnormal" else "🟢" | |
| advice = "Please consult a cardiologist." if result == "Abnormal" else "Your ECG looks healthy." | |
| output_text = ( | |
| f"{emoji} **Result: {result}**\n\n" | |
| f"📊 Confidence: {confidence}%\n\n" | |
| f"❤️ Heart Rate: {heart_rate if heart_rate else 'N/A'} BPM\n\n" | |
| f"⚠️ {advice}\n\n" | |
| f"_This tool is for educational purposes only. Always consult a doctor._" | |
| ) | |
| return output_text, pil_img | |
| # ----------- GRADIO UI ----------- | |
| with gr.Blocks(title="CardioSense AI") as demo: | |
| gr.Markdown(""" | |
| # 💓 CardioSense AI | |
| ### ECG Heart Condition Detector | |
| Upload your ECG data as a **CSV file** to detect if it is **Normal** or **Abnormal**. | |
| > Supports MIT-BIH format CSVs (e.g. with MLII column) or any single-column numeric ECG data. | |
| """) | |
| with gr.Row(): | |
| with gr.Column(): | |
| file_input = gr.File(label="📂 Upload ECG CSV File", file_types=[".csv"]) | |
| predict_btn = gr.Button("🔍 Analyze ECG", variant="primary") | |
| with gr.Column(): | |
| result_output = gr.Markdown(label="Result") | |
| plot_output = gr.Image(label="📈 ECG Graph", type="pil") | |
| predict_btn.click( | |
| fn=predict_ecg, | |
| inputs=file_input, | |
| outputs=[result_output, plot_output] | |
| ) | |
| gr.Markdown("> ⚠️ For educational use only. Not a substitute for medical advice.") | |
| demo.launch() |