{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"none","dataSources":[{"sourceId":97984,"databundleVersionId":14096757,"sourceType":"competition"}],"dockerImageVersionId":31153,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import pandas as pd\nimport matplotlib.pyplot as plt\nimport numpy as np\nimport cv2\nimport os\n\n# === Paths ===\nBASE_PATH = \"/kaggle/input/physionet-ecg-image-digitization\"  # <-- update if different\ntrain_csv = os.path.join(BASE_PATH, \"train.csv\")\n\n# === Load train metadata ===\ntrain_meta = pd.read_csv(train_csv)\nprint(\"Training metadata shape:\", train_meta.shape)\nprint(train_meta.head())\n\n# === Pick one example ===\nexample_id = train_meta.iloc[0][\"id\"]\nprint(\"Example ID:\", example_id)\n\n# === Load corresponding ECG CSV ===\nsignal_path = os.path.join(BASE_PATH, \"train\", str(example_id), f\"{example_id}.csv\")\nsignal_df = pd.read_csv(signal_path)\nprint(\"Signal shape:\", signal_df.shape)\nprint(\"Columns:\", signal_df.columns)\n\n# Plot one lead\nplt.figure(figsize=(12, 3))\nplt.plot(signal_df[\"II\"].values[:1000])\nplt.title(f\"Lead II waveform (first 1000 samples) — {example_id}\")\nplt.xlabel(\"Samples\")\nplt.ylabel(\"mV\")\nplt.show()\n\n# === Load an ECG image (e.g., the clean synthetic one) ===\nimg_path = os.path.join(BASE_PATH, \"train\", str(example_id), f\"{example_id}-0001.png\")\nimg = cv2.imread(img_path)\nimg_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)\n\nplt.figure(figsize=(12, 6))\nplt.imshow(img_rgb)\nplt.title(f\"ECG Image: {example_id}-0001.png\")\nplt.axis(\"off\")\nplt.show()\n","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-10-23T11:38:07.93095Z","iopub.execute_input":"2025-10-23T11:38:07.931339Z","iopub.status.idle":"2025-10-23T11:38:09.268475Z","shell.execute_reply.started":"2025-10-23T11:38:07.931309Z","shell.execute_reply":"2025-10-23T11:38:09.267518Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import cv2\nimport numpy as np\nimport matplotlib.pyplot as plt\n\ndef preprocess_ecg_image(img):\n    \"\"\"\n    Convert ECG image to a cleaned, normalized grayscale version.\n    - Converts to grayscale\n    - Applies adaptive threshold to highlight waveform lines\n    - Optionally deskews (disabled for now)\n    \"\"\"\n    # 1️⃣ Convert to grayscale\n    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)\n\n    # 2️⃣ Normalize brightness\n    gray = cv2.equalizeHist(gray)\n\n    # 3️⃣ Adaptive thresholding — separate ECG trace (dark lines) from background\n    thresh = cv2.adaptiveThreshold(\n        gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY_INV, 51, 8\n    )\n\n    # 4️⃣ (Optional) Remove small noise blobs\n    kernel = np.ones((2, 2), np.uint8)\n    cleaned = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, kernel)\n\n    return gray, cleaned\n\n\n# === Test it on your example ===\nimg_path = \"/kaggle/input/physionet-ecg-image-digitization/train/7663343/7663343-0001.png\"\nimg = cv2.imread(img_path)\n\ngray, cleaned = preprocess_ecg_image(img)\n\nplt.figure(figsize=(15,5))\nplt.subplot(1,2,1)\nplt.imshow(gray, cmap=\"gray\")\nplt.title(\"Grayscale Equalized Image\")\nplt.axis(\"off\")\n\nplt.subplot(1,2,2)\nplt.imshow(cleaned, cmap=\"gray\")\nplt.title(\"Thresholded + Cleaned Image (ECG trace visible)\")\nplt.axis(\"off\")\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-23T11:42:30.351523Z","iopub.execute_input":"2025-10-23T11:42:30.352138Z","iopub.status.idle":"2025-10-23T11:42:31.412562Z","shell.execute_reply.started":"2025-10-23T11:42:30.352104Z","shell.execute_reply":"2025-10-23T11:42:31.411577Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import cv2\nimport matplotlib.pyplot as plt\n\ndef split_into_leads(image, rows=3, cols=4):\n    \"\"\"\n    Splits an ECG image roughly into 12 rectangular lead regions (3 rows × 4 columns).\n    Works well for standard 12-lead printouts.\n    \"\"\"\n    h, w = image.shape[:2]\n    lead_h, lead_w = h // rows, w // cols\n    boxes = []\n    for r in range(rows):\n        for c in range(cols):\n            y0, y1 = r * lead_h, (r + 1) * lead_h\n            x0, x1 = c * lead_w, (c + 1) * lead_w\n            boxes.append(((x0, y0, x1, y1), image[y0:y1, x0:x1]))\n    return boxes\n\n\n# --- Load the same example and preprocess ---\nimg_path = \"/kaggle/input/physionet-ecg-image-digitization/train/7663343/7663343-0001.png\"\nimg = cv2.imread(img_path)\ngray, cleaned = preprocess_ecg_image(img)\n\n# --- Split into 12 approximate leads ---\nlead_boxes = split_into_leads(cleaned)\n\n# Display all 12 crops for inspection\nplt.figure(figsize=(12, 8))\nfor i, (coords, crop) in enumerate(lead_boxes):\n    plt.subplot(3, 4, i + 1)\n    plt.imshow(crop, cmap=\"gray\")\n    plt.title(f\"Lead {i + 1}\")\n    plt.axis(\"off\")\nplt.tight_layout()\nplt.show()\n\n# --- Select Lead II (2nd region, index 1 in 0-based) ---\nleadII_img = lead_boxes[1][1]\nplt.figure(figsize=(10, 3))\nplt.imshow(leadII_img, cmap=\"gray\")\nplt.title(\"Lead II cropped region\")\nplt.axis(\"off\")\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-23T12:31:56.089995Z","iopub.execute_input":"2025-10-23T12:31:56.090288Z","iopub.status.idle":"2025-10-23T12:31:57.758733Z","shell.execute_reply.started":"2025-10-23T12:31:56.090268Z","shell.execute_reply":"2025-10-23T12:31:57.757787Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import numpy as np\nimport matplotlib.pyplot as plt\nimport cv2\n\ndef trace_waveform(binary_img):\n    \"\"\"\n    Converts a preprocessed (white-on-black) ECG image into a 1D waveform.\n    For each column, computes the intensity-weighted vertical centroid\n    of the white trace pixels.\n    \"\"\"\n    # ensure grayscale\n    if len(binary_img.shape) == 3:\n        gray = cv2.cvtColor(binary_img, cv2.COLOR_BGR2GRAY)\n    else:\n        gray = binary_img.copy()\n\n    # invert if necessary (trace should be bright)\n    if np.mean(gray) < 127:\n        gray = 255 - gray\n\n    h, w = gray.shape\n    y_positions = np.zeros(w, dtype=float)\n\n    for x in range(w):\n        column = gray[:, x].astype(np.float32)\n        weights = column / 255.0 + 1e-6\n        y_positions[x] = np.sum(np.arange(h) * weights) / np.sum(weights)\n\n    # normalize vertically (invert y and center)\n    waveform = (h - y_positions) - np.mean(h - y_positions)\n    return waveform\n\n# === Apply to the Lead II cropped image ===\nleadII_waveform = trace_waveform(leadII_img)\n\nplt.figure(figsize=(12, 3))\nplt.plot(leadII_waveform)\nplt.title(\"Extracted Waveform from Lead II image\")\nplt.xlabel(\"Column index (time proxy)\")\nplt.ylabel(\"Relative amplitude (pixels)\")\nplt.show()\n\nprint(\"Extracted waveform length:\", len(leadII_waveform))\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-23T12:32:05.696127Z","iopub.execute_input":"2025-10-23T12:32:05.696594Z","iopub.status.idle":"2025-10-23T12:32:05.905723Z","shell.execute_reply.started":"2025-10-23T12:32:05.696558Z","shell.execute_reply":"2025-10-23T12:32:05.904481Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from scipy.signal import resample\nimport numpy as np\nimport matplotlib.pyplot as plt\n\n# --- Load the ground truth signal again ---\nsignal_path = \"/kaggle/input/physionet-ecg-image-digitization/train/7663343/7663343.csv\"\nsignal_df = pd.read_csv(signal_path)\ngt_waveform = signal_df[\"II\"].values  # true Lead II\n\n# --- Resample extracted waveform to match length of ground truth ---\nresampled_waveform = resample(leadII_waveform, len(gt_waveform))\n\n# --- Normalize both for fair comparison ---\nresampled_waveform = (resampled_waveform - np.mean(resampled_waveform)) / np.std(resampled_waveform)\ngt_norm = (gt_waveform - np.mean(gt_waveform)) / np.std(gt_waveform)\n\n# --- Overlay plot ---\nplt.figure(figsize=(12, 4))\nplt.plot(gt_norm[:2000], label=\"Ground Truth Lead II\", linewidth=2)\nplt.plot(resampled_waveform[:2000], label=\"Extracted from Image\", alpha=0.7)\nplt.title(\"Overlay: True vs Extracted Waveform (First ~4 s)\")\nplt.xlabel(\"Samples\")\nplt.ylabel(\"Normalized amplitude\")\nplt.legend()\nplt.show()\n\n# --- Correlation (rough alignment quality) ---\ncorr = np.corrcoef(gt_norm, resampled_waveform)[0, 1]\nprint(f\"Correlation between extracted and true waveform: {corr:.3f}\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-23T12:32:20.726504Z","iopub.execute_input":"2025-10-23T12:32:20.726844Z","iopub.status.idle":"2025-10-23T12:32:21.024549Z","shell.execute_reply.started":"2025-10-23T12:32:20.726816Z","shell.execute_reply":"2025-10-23T12:32:21.023263Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from scipy.signal import correlate\n\n# --- 1️⃣ Flip polarity (invert y-axis sense) ---\nflipped_waveform = -resampled_waveform\n\n# --- 2️⃣ Simple amplitude scaling to match ground truth std ---\nflipped_waveform *= np.std(gt_waveform) / np.std(flipped_waveform)\n\n# --- 3️⃣ Cross-correlation alignment (to remove small time shift) ---\ncorr_full = correlate(gt_norm, flipped_waveform, mode=\"full\")\nshift = np.argmax(corr_full) - len(gt_norm) + 1\nprint(f\"Best shift: {shift} samples\")\n\n# align signals by shift\nif shift > 0:\n    aligned_waveform = flipped_waveform[shift:]\n    gt_aligned = gt_waveform[:len(aligned_waveform)]\nelse:\n    aligned_waveform = flipped_waveform[:len(gt_waveform)+shift]\n    gt_aligned = gt_waveform[-shift:len(gt_waveform)]\n\n# --- 4️⃣ Normalize and compare again ---\naligned_waveform = (aligned_waveform - np.mean(aligned_waveform)) / np.std(aligned_waveform)\ngt_aligned_norm = (gt_aligned - np.mean(gt_aligned)) / np.std(gt_aligned)\n\nplt.figure(figsize=(12,4))\nplt.plot(gt_aligned_norm[:2000], label=\"Ground Truth (aligned)\")\nplt.plot(aligned_waveform[:2000], label=\"Extracted (flipped + aligned)\", alpha=0.7)\nplt.title(\"After alignment and polarity correction\")\nplt.xlabel(\"Samples\")\nplt.ylabel(\"Normalized amplitude\")\nplt.legend()\nplt.show()\n\ncorr2 = np.corrcoef(gt_aligned_norm, aligned_waveform)[0,1]\nprint(f\"Updated correlation after alignment: {corr2:.3f}\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-23T12:43:46.225585Z","iopub.execute_input":"2025-10-23T12:43:46.225919Z","iopub.status.idle":"2025-10-23T12:43:46.456157Z","shell.execute_reply.started":"2025-10-23T12:43:46.225896Z","shell.execute_reply":"2025-10-23T12:43:46.455157Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\n\nLEAD_NAMES = [\"I\",\"II\",\"III\",\"aVR\",\"aVL\",\"aVF\",\"V1\",\"V2\",\"V3\",\"V4\",\"V5\",\"V6\"]\n\ndef extract_all_leads_from_image(img_path):\n    \"\"\"\n    For a given ECG image path:\n      - preprocess\n      - split into 12 regions\n      - trace each region\n    Returns a dict of lead_name -> waveform array\n    \"\"\"\n    img = cv2.imread(img_path)\n    if img is None:\n        print(\"⚠️ Image not found:\", img_path)\n        return {}\n\n    gray, cleaned = preprocess_ecg_image(img)\n    lead_boxes = split_into_leads(cleaned)\n    leads_dict = {}\n\n    for i, (coords, crop) in enumerate(lead_boxes):\n        waveform = trace_waveform(crop)\n        leads_dict[LEAD_NAMES[i]] = waveform\n\n    return leads_dict\n\n\n# === Test it on the same example ===\nimg_path = \"/kaggle/input/physionet-ecg-image-digitization/train/7663343/7663343-0001.png\"\nleads_data = extract_all_leads_from_image(img_path)\n\n# Show extracted Lead II vs original Lead II waveform (short preview)\nplt.figure(figsize=(12, 4))\nplt.plot(leads_data[\"II\"], label=\"Extracted Lead II (pixels)\")\nplt.title(\"Batch Extracted Lead II (from all-leads function)\")\nplt.xlabel(\"Column index\")\nplt.ylabel(\"Relative amplitude (pixels)\")\nplt.legend()\nplt.show()\n\nprint(\"Extracted leads:\", list(leads_data.keys()))\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-23T12:45:49.255306Z","iopub.execute_input":"2025-10-23T12:45:49.257256Z","iopub.status.idle":"2025-10-23T12:45:49.90163Z","shell.execute_reply.started":"2025-10-23T12:45:49.257213Z","shell.execute_reply":"2025-10-23T12:45:49.897765Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from scipy.signal import savgol_filter\nimport numpy as np\nimport cv2\nimport matplotlib.pyplot as plt\n\ndef remove_grid_background(img):\n    \"\"\"\n    Suppress regular grid patterns via frequency-domain notch filtering.\n    Keeps ECG trace edges intact.\n    \"\"\"\n    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) if len(img.shape) == 3 else img.copy()\n    f = np.fft.fft2(gray)\n    fshift = np.fft.fftshift(f)\n\n    rows, cols = gray.shape\n    crow, ccol = rows // 2, cols // 2\n    mask = np.ones((rows, cols), np.uint8)\n\n    # Mask out horizontal + vertical gridline frequencies\n    span = 5\n    mask[crow - span:crow + span, :] = 0\n    mask[:, ccol - span:ccol + span] = 0\n\n    fshift = fshift * mask\n    f_ishift = np.fft.ifftshift(fshift)\n    img_back = np.abs(np.fft.ifft2(f_ishift))\n\n    img_back = cv2.normalize(img_back, None, 0, 255, cv2.NORM_MINMAX).astype(np.uint8)\n    return img_back\n\ndef refined_trace(crop_img):\n    \"\"\"Remove grid → trace → smooth\"\"\"\n    no_grid = remove_grid_background(crop_img)\n    waveform = trace_waveform(no_grid)\n    waveform_smooth = savgol_filter(waveform, 21, 3)  # window=21, polyorder=3\n    return waveform_smooth\n\n# --- Test on Lead II ---\nimg_path = \"/kaggle/input/physionet-ecg-image-digitization/train/7663343/7663343-0001.png\"\nimg = cv2.imread(img_path)\ngray, _ = preprocess_ecg_image(img)\nleads = split_into_leads(gray)\nleadII_crop = leads[1][1]\n\nwaveform_refined = refined_trace(leadII_crop)\n\nplt.figure(figsize=(12, 3))\nplt.plot(waveform_refined)\nplt.title(\"Refined Lead II waveform (grid removed + smoothed)\")\nplt.xlabel(\"Column index\")\nplt.ylabel(\"Relative amplitude (pixels)\")\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-23T12:50:15.671758Z","iopub.execute_input":"2025-10-23T12:50:15.672533Z","iopub.status.idle":"2025-10-23T12:50:16.28211Z","shell.execute_reply.started":"2025-10-23T12:50:15.672484Z","shell.execute_reply":"2025-10-23T12:50:16.281023Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import numpy as np\nfrom scipy.signal import correlate\n\ndef compute_modified_snr(pred, true, fs, max_shift_s=0.2):\n    \"\"\"\n    Safe Modified SNR per PhysioNet spec.\n    Handles constant / zero-variance signals gracefully.\n    \"\"\"\n    pred = np.nan_to_num(pred, nan=0.0, posinf=0.0, neginf=0.0)\n    true = np.nan_to_num(true, nan=0.0, posinf=0.0, neginf=0.0)\n\n    # check for constant or empty signals\n    if np.std(true) < 1e-8 or np.std(pred) < 1e-8:\n        return -100.0  # penalty for degenerate signals\n\n    pred = (pred - np.mean(pred)) / (np.std(pred) + 1e-8)\n    true = (true - np.mean(true)) / (np.std(true) + 1e-8)\n\n    n = len(true)\n    shift_max = int(max_shift_s * fs)\n\n    corr = correlate(true, pred, mode=\"full\", method=\"direct\")\n    shift_vals = np.arange(-len(pred)+1, len(true))\n    valid = (shift_vals >= -shift_max) & (shift_vals <= shift_max)\n    best_shift = shift_vals[np.argmax(corr[valid])]\n\n    if best_shift > 0:\n        pred_aligned = pred[:-best_shift]\n        true_aligned = true[best_shift:]\n    elif best_shift < 0:\n        pred_aligned = pred[-best_shift:]\n        true_aligned = true[:len(pred_aligned)]\n    else:\n        pred_aligned = pred\n        true_aligned = true\n\n    offset = np.mean(true_aligned - pred_aligned)\n    pred_aligned += offset\n\n    signal_power = np.sum(true_aligned ** 2)\n    noise_power = np.sum((true_aligned - pred_aligned) ** 2) + 1e-12\n\n    if noise_power <= 0:\n        return -100.0\n\n    snr = 10 * np.log10(signal_power / noise_power)\n    return float(snr)\n\n\n\n# === Test on current example ===\nfrom scipy.signal import resample\nsignal_df = pd.read_csv(\"/kaggle/input/physionet-ecg-image-digitization/train/7663343/7663343.csv\")\ntrue_sig = signal_df[\"II\"].values\nfs = int(pd.read_csv(\"/kaggle/input/physionet-ecg-image-digitization/train.csv\")\\\n         .query(\"id==7663343\")[\"fs\"].iloc[0])\n\n# resample refined waveform to same length\npred_sig = resample(waveform_refined, len(true_sig))\n\nsnr_val = compute_modified_snr(pred_sig, true_sig, fs)\nprint(f\"Modified SNR for Lead II = {snr_val:.2f} dB\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-23T13:24:46.732891Z","iopub.execute_input":"2025-10-23T13:24:46.733289Z","iopub.status.idle":"2025-10-23T13:24:46.778738Z","shell.execute_reply.started":"2025-10-23T13:24:46.733264Z","shell.execute_reply":"2025-10-23T13:24:46.777453Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch\nimport torch.nn as nn\nfrom torch.utils.data import Dataset, DataLoader\nfrom torchvision import transforms\nimport cv2, numpy as np, pandas as pd, os\nfrom scipy.signal import resample \n\n# --- Dataset ---\nclass ECGLeadDataset(Dataset):\n    def __init__(self, base_path, ids, lead=\"II\", target_len=5000, transform=None):\n        self.base_path = base_path\n        self.ids = ids\n        self.lead = lead\n        self.target_len = target_len\n        self.transform = transform\n\n    def __len__(self):\n        return len(self.ids)\n\n    def __getitem__(self, idx):\n        pid = self.ids[idx]\n        img_path = f\"{self.base_path}/train/{pid}/{pid}-0001.png\"\n        sig_path = f\"{self.base_path}/train/{pid}/{pid}.csv\"\n\n        img = cv2.imread(img_path, cv2.IMREAD_COLOR)\n        img = cv2.resize(img, (512, 256))  # normalize input size\n\n        signal = pd.read_csv(sig_path)[self.lead].values.astype(np.float32)\n        # --- resample all signals to same target length ---\n        signal = resample(signal, self.target_len)\n        signal = (signal - np.mean(signal)) / (np.std(signal) + 1e-8)\n\n        if self.transform:\n            img = self.transform(img)\n        else:\n            img = transforms.ToTensor()(img)\n\n        return img, torch.tensor(signal, dtype=torch.float32)\n\n# --- Simple UNet-like regression model ---\nclass ECGDigitizer(nn.Module):\n    def __init__(self):\n        super().__init__()\n        self.encoder = nn.Sequential(\n            nn.Conv2d(3, 16, 3, padding=1), nn.ReLU(),\n            nn.MaxPool2d(2),\n            nn.Conv2d(16, 32, 3, padding=1), nn.ReLU(),\n            nn.MaxPool2d(2),\n            nn.Conv2d(32, 64, 3, padding=1), nn.ReLU(),\n        )\n        self.decoder = nn.Sequential(\n            nn.ConvTranspose2d(64, 32, 2, stride=2), nn.ReLU(),\n            nn.ConvTranspose2d(32, 16, 2, stride=2), nn.ReLU(),\n            nn.Conv2d(16, 1, 1)\n        )\n\n    def forward(self, x):\n        x = self.encoder(x)\n        x = self.decoder(x)\n        x = torch.mean(x, dim=2)  # average over height → (B,1,W)\n        return x.squeeze(1)       # output shape (B, W)\n\n# --- Training setup ---\ntransform = transforms.Compose([transforms.ToTensor()])\ntrain_meta = pd.read_csv(\"/kaggle/input/physionet-ecg-image-digitization/train.csv\")\nids = train_meta[\"id\"].astype(str).tolist()[:50]  # subset\n\ndataset = ECGLeadDataset(\"/kaggle/input/physionet-ecg-image-digitization\", ids)\nloader = DataLoader(dataset, batch_size=4, shuffle=True)\n\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nprint(\"Using device:\", device)\n\nmodel = ECGDigitizer().to(device)\noptimizer = torch.optim.Adam(model.parameters(), lr=1e-4)\ncriterion = nn.MSELoss()\n\nfor epoch in range(3):\n    for imgs, signals in loader:\n        imgs, signals = imgs.to(device), signals.to(device)\n        preds = model(imgs)\n        signals_resamp = torch.nn.functional.interpolate(\n            signals.unsqueeze(1), size=preds.shape[1], mode=\"linear\"\n        ).squeeze(1)\n        loss = criterion(preds, signals_resamp)\n        optimizer.zero_grad()\n        loss.backward()\n        optimizer.step()\n    print(f\"Epoch {epoch+1}, loss={loss.item():.6f}\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-23T13:24:51.630204Z","iopub.execute_input":"2025-10-23T13:24:51.63131Z","iopub.status.idle":"2025-10-23T13:25:28.415581Z","shell.execute_reply.started":"2025-10-23T13:24:51.631275Z","shell.execute_reply":"2025-10-23T13:25:28.414481Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch\nimport matplotlib.pyplot as plt\nfrom scipy.signal import resample\n\n# pick one random record\npid = ids[5]  # change if you like\nimg_path = f\"/kaggle/input/physionet-ecg-image-digitization/train/{pid}/{pid}-0001.png\"\nsig_path = f\"/kaggle/input/physionet-ecg-image-digitization/train/{pid}/{pid}.csv\"\n\n# prepare data\nimg = cv2.imread(img_path, cv2.IMREAD_COLOR)\nimg = cv2.resize(img, (512, 256))\nimg_tensor = transforms.ToTensor()(img).unsqueeze(0).to(device)\n\ntrue_sig = pd.read_csv(sig_path)[\"II\"].values.astype(np.float32)\ntrue_sig = resample(true_sig, 5000)  # match target_len\ntrue_sig = (true_sig - np.mean(true_sig)) / (np.std(true_sig) + 1e-8)\n\n# model inference\nmodel.eval()\nwith torch.no_grad():\n    pred = model(img_tensor).cpu().numpy().flatten()\n\n# normalize prediction\npred = (pred - np.mean(pred)) / (np.std(pred) + 1e-8)\n\n# overlay plot\nplt.figure(figsize=(12,4))\nplt.plot(true_sig, label=\"Ground Truth Lead II\", linewidth=2)\nplt.plot(pred, label=\"Model Prediction\", alpha=0.7)\nplt.title(f\"Model Prediction vs Ground Truth — ID {pid}\")\nplt.xlabel(\"Samples\")\nplt.ylabel(\"Normalized Amplitude\")\nplt.legend()\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-23T13:26:50.806708Z","iopub.execute_input":"2025-10-23T13:26:50.807144Z","iopub.status.idle":"2025-10-23T13:26:51.227273Z","shell.execute_reply.started":"2025-10-23T13:26:50.807115Z","shell.execute_reply":"2025-10-23T13:26:51.226147Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def snr_loss(pred, target, eps=1e-6):\n    \"\"\"\n    Differentiable safe negative SNR loss.\n    Handles constant signals and avoids NaN/Inf.\n    \"\"\"\n    # sanitize\n    pred = torch.nan_to_num(pred, nan=0.0, posinf=0.0, neginf=0.0)\n    target = torch.nan_to_num(target, nan=0.0, posinf=0.0, neginf=0.0)\n\n    signal_power = torch.sum(target ** 2, dim=1) + eps\n    noise_power  = torch.sum((target - pred) ** 2, dim=1) + eps\n    ratio = signal_power / noise_power\n    ratio = torch.clamp(ratio, min=eps, max=1e6)\n    snr = 10 * torch.log10(ratio)\n    snr = torch.nan_to_num(snr, nan=0.0, posinf=0.0, neginf=0.0)\n    return -torch.mean(snr)\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-23T13:55:20.065573Z","iopub.execute_input":"2025-10-23T13:55:20.065968Z","iopub.status.idle":"2025-10-23T13:55:20.074133Z","shell.execute_reply.started":"2025-10-23T13:55:20.065944Z","shell.execute_reply":"2025-10-23T13:55:20.073019Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class ECGLeadDataset(Dataset):\n    def __init__(self, base_path, ids, leads=None, target_len=5000, transform=None):\n        self.base_path = base_path\n        self.ids = ids\n        self.leads = leads or [\"I\",\"II\",\"III\",\"aVR\",\"aVL\",\"aVF\",\"V1\",\"V2\",\"V3\",\"V4\",\"V5\",\"V6\"]\n        self.target_len = target_len\n        self.transform = transform\n\n    def __len__(self):\n        return len(self.ids) * len(self.leads)\n\n    def __getitem__(self, idx):\n        pid = self.ids[idx // len(self.leads)]\n        lead = self.leads[idx % len(self.leads)]\n        img_path = f\"{self.base_path}/train/{pid}/{pid}-0001.png\"\n        sig_path = f\"{self.base_path}/train/{pid}/{pid}.csv\"\n\n        img = cv2.imread(img_path, cv2.IMREAD_COLOR)\n        img = cv2.resize(img, (512, 256))\n        signal = pd.read_csv(sig_path)[lead].values.astype(np.float32)\n        signal = resample(signal, self.target_len)\n        signal = (signal - np.mean(signal)) / (np.std(signal) + 1e-8)\n\n        if self.transform:\n            img = self.transform(img)\n        else:\n            img = transforms.ToTensor()(img)\n\n        return img, torch.tensor(signal, dtype=torch.float32)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-23T13:55:24.743799Z","iopub.execute_input":"2025-10-23T13:55:24.744149Z","iopub.status.idle":"2025-10-23T13:55:24.754087Z","shell.execute_reply.started":"2025-10-23T13:55:24.744124Z","shell.execute_reply":"2025-10-23T13:55:24.752519Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"lambda_snr = 0.7\ncriterion_mse = nn.MSELoss()\n\nfor epoch in range(5):\n    for imgs, signals, lead_idx in loader:\n        imgs, signals, lead_idx = imgs.to(device), signals.to(device), lead_idx.to(device)\n        preds = model(imgs, lead_idx)\n\n        signals_resamp = torch.nn.functional.interpolate(\n            signals.unsqueeze(1), size=preds.shape[1], mode='linear'\n        ).squeeze(1)\n\n        loss_mse = criterion_mse(preds, signals_resamp)\n        loss_snr = snr_loss(preds, signals_resamp)\n        loss = (1 - lambda_snr) * loss_mse + lambda_snr * loss_snr\n\n        # skip NaN batches if any appear\n        if torch.isnan(loss) or torch.isinf(loss):\n            print(\"⚠️  NaN loss detected — skipping batch\")\n            optimizer.zero_grad(set_to_none=True)\n            continue\n\n        optimizer.zero_grad()\n        loss.backward()\n        optimizer.step()\n\n    print(f\"Epoch {epoch+1}, total_loss={loss.item():.6f}, snr_loss={loss_snr.item():.4f}\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-23T13:57:07.712603Z","iopub.execute_input":"2025-10-23T13:57:07.712951Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import numpy as np\nfrom scipy.signal import resample\nimport matplotlib.pyplot as plt\n\ndef safe_normalize(sig):\n    sig = np.nan_to_num(sig, nan=0.0, posinf=0.0, neginf=0.0)\n    if np.std(sig) < 1e-8:\n        sig = sig - np.mean(sig)  # flatten to zero if constant\n    else:\n        sig = (sig - np.mean(sig)) / (np.std(sig) + 1e-8)\n    return sig\n\ndef evaluate_record(pid, model, base_path=\"/kaggle/input/physionet-ecg-image-digitization\", fs=500):\n    img_path = f\"{base_path}/train/{pid}/{pid}-0001.png\"\n    sig_path = f\"{base_path}/train/{pid}/{pid}.csv\"\n    img = cv2.imread(img_path, cv2.IMREAD_COLOR)\n    img = cv2.resize(img, (512, 256))\n    img_tensor = transforms.ToTensor()(img).unsqueeze(0).to(device)\n\n    true_df = pd.read_csv(sig_path)\n    lead_snr = {}\n    model.eval()\n    with torch.no_grad():\n        for lead in true_df.columns:\n            true_sig = resample(true_df[lead].values, 5000)\n            pred = model(img_tensor).cpu().numpy().flatten()\n            pred = resample(pred, len(true_sig))\n\n            # --- sanitize both before metric ---\n            true_sig = safe_normalize(true_sig)\n            pred = safe_normalize(pred)\n\n            snr_val = compute_modified_snr(pred, true_sig, fs)\n            lead_snr[lead] = snr_val\n\n    avg_snr = np.nanmean(list(lead_snr.values()))\n    return avg_snr, lead_snr\n\n\n# --- Evaluate a few samples ---\ntest_ids = ids[:3]\nfor pid in test_ids:\n    fs = int(train_meta.query(f\"id=={pid}\")[\"fs\"].iloc[0])\n    avg_snr, per_lead = evaluate_record(pid, model, fs=fs)\n    print(f\"\\nRecord {pid}: Average SNR = {avg_snr:.2f} dB\")\n    for k, v in per_lead.items():\n        print(f\"  {k:>4}: {v:6.2f} dB\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-23T13:28:28.006081Z","iopub.execute_input":"2025-10-23T13:28:28.006538Z","iopub.status.idle":"2025-10-23T13:28:29.756835Z","shell.execute_reply.started":"2025-10-23T13:28:28.006508Z","shell.execute_reply":"2025-10-23T13:28:29.755486Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch.nn.functional as F\n\nclass MultiLeadDigitizer(nn.Module):\n    def __init__(self, n_leads=12):\n        super().__init__()\n        self.encoder = nn.Sequential(\n            nn.Conv2d(3, 16, 3, padding=1), nn.ReLU(),\n            nn.MaxPool2d(2),\n            nn.Conv2d(16, 32, 3, padding=1), nn.ReLU(),\n            nn.MaxPool2d(2),\n            nn.Conv2d(32, 64, 3, padding=1), nn.ReLU(),\n        )\n        self.decoder = nn.Sequential(\n            nn.ConvTranspose2d(64, 32, 2, stride=2), nn.ReLU(),\n            nn.ConvTranspose2d(32, 16, 2, stride=2), nn.ReLU(),\n            nn.Conv2d(16, 1, 1)\n        )\n        self.lead_embed = nn.Embedding(n_leads, 64)  # learn per-lead bias\n\n    def forward(self, x, lead_idx):\n        feat = self.encoder(x)               # (B, 64, H', W')\n        feat = self.decoder(feat)            # (B, 1, H, W)\n    \n        # Collapse height → 1D waveform\n        feat = torch.mean(feat, dim=2)       # (B, 1, W)\n        feat = feat.squeeze(1)               # (B, W)\n    \n        # Lead embedding bias\n        lead_bias = self.lead_embed(lead_idx).mean(dim=1, keepdim=True)\n        feat = feat + lead_bias              # broadcast add\n    \n        # --- temporal smoothing (requires 3D input: B×C×L)\n        feat = feat.unsqueeze(1)             # (B, 1, W)\n        feat = F.avg_pool1d(feat, kernel_size=5, stride=1, padding=2)\n        feat = feat.squeeze(1)               # (B, W)\n    \n        return feat\n\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-23T13:38:12.87981Z","iopub.execute_input":"2025-10-23T13:38:12.880208Z","iopub.status.idle":"2025-10-23T13:38:12.890977Z","shell.execute_reply.started":"2025-10-23T13:38:12.880182Z","shell.execute_reply":"2025-10-23T13:38:12.890138Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"LEADS = [\"I\",\"II\",\"III\",\"aVR\",\"aVL\",\"aVF\",\"V1\",\"V2\",\"V3\",\"V4\",\"V5\",\"V6\"]\nlead_to_idx = {l:i for i,l in enumerate(LEADS)}\n\nclass ECGLeadDataset(Dataset):\n    def __init__(self, base_path, ids, leads=LEADS, target_len=5000, transform=None):\n        self.base_path = base_path\n        self.ids = ids\n        self.leads = leads\n        self.target_len = target_len\n        self.transform = transform\n\n    def __len__(self):\n        return len(self.ids) * len(self.leads)\n\n    def __getitem__(self, idx):\n        pid = self.ids[idx // len(self.leads)]\n        lead = self.leads[idx % len(self.leads)]\n        img_path = f\"{self.base_path}/train/{pid}/{pid}-0001.png\"\n        sig_path = f\"{self.base_path}/train/{pid}/{pid}.csv\"\n\n        img = cv2.imread(img_path, cv2.IMREAD_COLOR)\n        img = cv2.resize(img, (512, 256))\n        sig = pd.read_csv(sig_path)[lead].values.astype(np.float32)\n        sig = resample(sig, self.target_len)\n        sig = (sig - np.mean(sig)) / (np.std(sig) + 1e-8)\n        img = transforms.ToTensor()(img)\n        return img, torch.tensor(sig), torch.tensor(lead_to_idx[lead])\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-23T13:38:18.878867Z","iopub.execute_input":"2025-10-23T13:38:18.879192Z","iopub.status.idle":"2025-10-23T13:38:18.890153Z","shell.execute_reply.started":"2025-10-23T13:38:18.879168Z","shell.execute_reply":"2025-10-23T13:38:18.888962Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"dataset = ECGLeadDataset(\"/kaggle/input/physionet-ecg-image-digitization\", ids)\nloader = DataLoader(dataset, batch_size=4, shuffle=True)\n\nmodel = MultiLeadDigitizer().to(device)\noptimizer = torch.optim.Adam(model.parameters(), lr=1e-4)\n\nfor epoch in range(5):\n    for imgs, signals, lead_idx in loader:\n        imgs, signals, lead_idx = imgs.to(device), signals.to(device), lead_idx.to(device)\n        preds = model(imgs, lead_idx)\n        signals_resamp = F.interpolate(signals.unsqueeze(1), size=preds.shape[1], mode='linear').squeeze(1)\n        loss_mse = criterion_mse(preds, signals_resamp)\n        loss_snr = snr_loss(preds, signals_resamp)\n        loss = 0.3*loss_mse + 0.7*loss_snr\n        optimizer.zero_grad()\n        loss.backward()\n        optimizer.step()\n    print(f\"Epoch {epoch+1}: loss={loss.item():.5f}\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-23T13:38:30.376332Z","iopub.execute_input":"2025-10-23T13:38:30.376672Z","iopub.status.idle":"2025-10-23T13:50:51.944263Z","shell.execute_reply.started":"2025-10-23T13:38:30.376651Z","shell.execute_reply":"2025-10-23T13:50:51.943078Z"}},"outputs":[],"execution_count":null}]}