{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.12.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"none","dataSources":[],"dockerImageVersionId":28755,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# ECG-Image-Database — Sample Notebook (Kaggle)\n\n**ECG-Image-Database** is a large-scale collection of paired electrocardiogram (ECG) images and digital time-series signals for ECG image digitization, waveform reconstruction, image-based classification, and benchmarking. It contains **37,191 images** derived from **2,243 ECG records** from three sources:\n\n| Source | Directory | Records | Description |\n|---|---|---|---|\n| PTB-XL (Germany) | `D0` | 977 | Representative subset of the public PTB-XL clinical 12-lead ECG dataset |\n| Emory Healthcare (USA) | `D1` | 1,000 | Clinical 12-lead ECGs recorded at Emory Healthcare |\n| Akershus University Hospital (Norway) | `D2` | 266 | Thermal-paper ECGs from routine clinical care, paired with archived digital signals |\n\nEach record directory (e.g. `D1/D1_000034/`) contains one or more ECG **images** (`Dn_[RECORD-ID]_[VARIANT-ID].png/jpg`) together with the corresponding **WFDB** signal/header files (`Dn_[RECORD-ID].dat` / `.hea`). Multiple images in the same directory are different imaging/artifact variants (scans, phone photos, monitor photos, wrinkles, stains, water damage, etc.) of the same underlying ECG.\n\nSubsets of this database are used in the [**PhysioNet - Digitization of ECG Images** Kaggle competition](https://www.kaggle.com/competitions/physionet-ecg-image-digitization) and were used in the [George B. Moody PhysioNet Challenge 2024](https://moody-challenge.physionet.org/2024/).\n\n**This notebook demonstrates how to:**\n1. Locate the data in the Kaggle environment (`/kaggle/input/`) — a read-only mount, so only the sample record's files are read; the full dataset is never downloaded.\n2. **Load the WFDB reference signals** and plot all channels in separate rows on a **uniform, standard ECG grid** (large squares = 0.2 s × 0.5 mV; small squares = 0.04 s × 0.1 mV), including the **standard 1 mV / 200 ms calibration pulse**.\n3. **Load and display the ECG image variants** paired with each record.\n\n> **To run on Kaggle:** attach the competition data via *Add Input → Competitions → PhysioNet - Digitization of ECG Images*.\n\n---\n### Citations\n\nIf you use this dataset, please cite:\n\n> Reyna MA, Deepanshi, Weigle J, Koscova Z, Campbell K, Shivashankara KK, Saghafi S, Nikookar S, Motie-Shirazi M, Kiarashi Y, Seyedi S, Clifford GD, Sameni R. *ECG-image-database: large-scale paired ECG images and time-series with real-world artifacts; a foundation for computerized ECG digitization and analysis.* Physiological Measurement 2026; 47(7):075015. DOI: [10.1088/1361-6579/ae85b2](https://doi.org/10.1088/1361-6579/ae85b2)\n\nThe images were generated with **ECG-Image-Kit**; please also cite:\n\n> Shivashankara KK, Deepanshi, Shervedani AM, Clifford GD, Reyna MA, Sameni R. *ECG-Image-Kit: a synthetic image generation toolbox to facilitate deep learning-based electrocardiogram digitization.* Physiological Measurement 2024;45(5):055019. DOI: [10.1088/1361-6579/ad4954](https://doi.org/10.1088/1361-6579/ad4954)\n\n> Deepanshi, Shivashankara KK, Clifford GD, Reyna MA, Sameni R. *ECG-Image-Kit: A Toolkit for Synthesis, Analysis, and Digitization of Electrocardiogram Images*, 2024. https://github.com/alphanumericslab/ecg-image-kit\n\nFor the PTB-XL and Ahus subsets, also include the following:\n\n> *PTB-XL dataset:* Wagner, P., Strodthoff, N., Bousseljot, R.-D., Kreiseler, D., Lunze, F. I., Samek, W., & Schaeffter, T. (2020). PTB-XL, a large publicly available electrocardiography dataset. Scientific Data, 7(1). DOI: [10.1038/s41597-020-0495-6](https://doi.org/10.1038/s41597-020-0495-6).\n\n> *AHUS dataset:* E. Stenhede, A. M. Bjørnstad and A. Ranjbar, A. (2026). Digitizing paper ECGs at scale: an open-source algorithm for clinical research. Npj Digital Medicine, 9(1). DOI: [10.1038/s41746-025-02327-1](https://doi.org/10.1038/s41746-025-02327-1)\n\nAuthor: Reza Sameni","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19"}},{"cell_type":"code","source":"# This Python 3 environment comes with many helpful analytics libraries installed\n# It is defined by the kaggle/python Docker image: https://github.com/kaggle/docker-python\n\nimport numpy as np\nimport pandas as pd\nimport os\n\nROOT_DIR = '/kaggle/input/datasets/physionet/ecg-image-database/'\n\n# Input data files are available in the read-only \"../input/\" directory.\n# The full dataset has ~37k images, so we only peek at the first few paths here:\n_n = 0\nfor dirname, _, filenames in os.walk(ROOT_DIR):\n    for filename in filenames:\n        print(os.path.join(dirname, filename))\n        _n += 1\n        if _n >= 20:\n            break\n    if _n >= 20:\n        print('... (truncated)')\n        break\n\n# You can write up to 20GB to /kaggle/working/ (preserved with \"Save & Run All\"),\n# and temporary files to /kaggle/temp/ (not preserved).\n\n# Install the one dependency not preinstalled on Kaggle:\n!pip install -q wfdb","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-27T10:33:46.308858Z","iopub.execute_input":"2026-08-27T10:33:46.309140Z","iopub.status.idle":"2026-08-27T10:33:49.853667Z","shell.execute_reply.started":"2026-08-27T10:33:46.309117Z","shell.execute_reply":"2026-08-27T10:33:49.852885Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import glob\n\nimport matplotlib.pyplot as plt\nimport matplotlib.ticker as mticker\nfrom PIL import Image\nimport wfdb\n\n# ---------------------------------------------------------------------------\n# ROOT_DIR was set in the first cell. The dataset root contains the source\n# folders D0, D1, D2, each holding one directory per record:\n#   <ROOT>/D0/D0_000022/D0_000022.hea, D0_000022.dat, D0_000022_00.png, ...\n# Simply list the record folders you want to load and plot:\n# ---------------------------------------------------------------------------\nSAMPLE_RECORDS = [\n    \"D0/D0_000022\",\n    # \"D1/D1_000034\",   # add more records/sources here\n    # \"D2/D2_000010\",\n]\nMAX_IMAGES_PER_RECORD = 10   # how many image variants to show per record\n\n# Resolve each sample folder to its WFDB record path (the .hea/.dat basename)\nsample_records = []\nfor rel in SAMPLE_RECORDS:\n    record_dir = os.path.join(ROOT_DIR, rel)\n    record_path = os.path.join(record_dir, os.path.basename(record_dir.rstrip(\"/\")))  # .../D0_000022/D0_000022\n    if os.path.isfile(record_path + \".hea\"):\n        sample_records.append(record_path)\n    else:\n        print(f\"Record not found: {record_path}.hea\")\n        # Show what actually exists, to help adjust SAMPLE_RECORDS:\n        src_dir = os.path.join(ROOT_DIR, rel.split(\"/\")[0])\n        if os.path.isdir(src_dir):\n            print(f\"  First entries in {rel.split('/')[0]}/:\", sorted(os.listdir(src_dir))[:5])\n\nprint(\"Sample records:\", [os.path.basename(p) for p in sample_records])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-27T10:33:49.855179Z","iopub.execute_input":"2026-08-27T10:33:49.855420Z","iopub.status.idle":"2026-08-27T10:33:49.863242Z","shell.execute_reply.started":"2026-08-27T10:33:49.855389Z","shell.execute_reply":"2026-08-27T10:33:49.862312Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 1. Plot the WFDB reference signals on a uniform standard ECG grid\n\nAll records and all channels are drawn on the **same fixed grid** so plots are directly comparable across records and image variants:\n\n- **Large squares:** 0.2 s × 0.5 mV (equivalent to 25 mm/s, 10 mm/mV paper at 5 mm squares)\n- **Small squares:** 0.04 s × 0.1 mV\n- **Fixed amplitude range** for every lead (default ±2 mV — 8 large squares)\n- **Equal scaling** is enforced with a fixed axis aspect ratio, so a large square always renders square\n- A **standard calibration pulse** (1 mV tall, 200 ms wide) is drawn at the start of every lead","metadata":{}},{"cell_type":"code","source":"# ---------------- Uniform grid configuration (identical for every plot) ----------------\nY_LIM      = (-2.0, 2.0)   # mV; fixed for ALL leads and ALL records (8 large squares)\nT_MAX      = 10.0          # s; fixed time window for ALL records (50 large squares)\nCAL_WIDTH  = 0.2           # s; standard calibration pulse width (one large square)\nCAL_HEIGHT = 1.0           # mV; standard calibration pulse height (two large squares)\nCAL_PAD    = 0.2           # s of flat baseline on each side of the calibration pulse\nT_MIN      = -(CAL_WIDTH + 2 * CAL_PAD)  # signal starts at t = 0; pulse sits before it\n\nMAJOR_T, MINOR_T = 0.2, 0.04   # s   (large / small square)\nMAJOR_V, MINOR_V = 0.5, 0.1    # mV  (large / small square)\n\n\ndef draw_ecg_grid(ax):\n    \"\"\"Standard ECG grid, identical on every axis.\"\"\"\n    ax.xaxis.set_minor_locator(mticker.MultipleLocator(MINOR_T))\n    ax.yaxis.set_minor_locator(mticker.MultipleLocator(MINOR_V))\n    ax.xaxis.set_major_locator(mticker.MultipleLocator(MAJOR_T))\n    ax.yaxis.set_major_locator(mticker.MultipleLocator(MAJOR_V))\n    ax.grid(which=\"minor\", color=(1.0, 0.78, 0.78), linewidth=0.4)\n    ax.grid(which=\"major\", color=(1.0, 0.45, 0.45), linewidth=0.8)\n    ax.set_xlim(T_MIN, T_MAX)\n    ax.set_ylim(*Y_LIM)\n    # Equal scaling: one large square (0.2 s) renders as tall as it is wide (0.5 mV)\n    ax.set_aspect(MAJOR_T / MAJOR_V)\n    # Only label whole seconds to keep the axis readable\n    ax.xaxis.set_major_formatter(\n        mticker.FuncFormatter(lambda v, _: f\"{v:g}\" if np.isclose(v % 1, 0) or np.isclose(v % 1, 1) else \"\")\n    )\n    ax.tick_params(axis=\"both\", labelsize=7)\n\n\ndef calibration_pulse():\n    \"\"\"Standard ECG calibration pulse: 1 mV tall, 200 ms wide, drawn before t = 0.\"\"\"\n    t = [T_MIN, T_MIN + CAL_PAD, T_MIN + CAL_PAD,\n         T_MIN + CAL_PAD + CAL_WIDTH, T_MIN + CAL_PAD + CAL_WIDTH, 0.0]\n    v = [0.0, 0.0, CAL_HEIGHT, CAL_HEIGHT, 0.0, 0.0]\n    return np.array(t), np.array(v)\n\n\ndef plot_wfdb_record(record_path):\n    \"\"\"Load one WFDB record and plot every channel in its own row on the uniform grid.\"\"\"\n    record = wfdb.rdrecord(record_path)\n    sig   = record.p_signal          # (n_samples, n_channels), physical units (mV)\n    fs    = record.fs\n    n_ch  = record.n_sig\n    names = record.sig_name\n\n    t = np.arange(sig.shape[0]) / fs\n    keep = t <= T_MAX\n    t, sig = t[keep], sig[keep]\n\n    total_s  = T_MAX - T_MIN\n    total_mv = Y_LIM[1] - Y_LIM[0]\n    fig_w = total_s * 1.6 + 1.5\n    fig_h = n_ch * total_mv * 0.65 + 1.0\n    fig, axes = plt.subplots(n_ch, 1, figsize=(fig_w, fig_h), sharex=True, squeeze=False)\n    axes = axes.ravel()\n\n    ct, cv = calibration_pulse()\n    for i, ax in enumerate(axes):\n        draw_ecg_grid(ax)\n        ax.plot(ct, cv, color=\"black\", linewidth=1.0)           # calibration pulse\n        ax.plot(t, sig[:, i], color=\"black\", linewidth=0.7)     # ECG trace\n        ax.set_ylabel(names[i], fontsize=10, rotation=0, ha=\"right\", va=\"center\", labelpad=18)\n\n    axes[-1].set_xlabel(\"Time (s)   —   grid: 0.2 s × 0.5 mV (major), 0.04 s × 0.1 mV (minor)\")\n    fig.suptitle(f\"{os.path.basename(record_path)}   |   fs = {fs} Hz   |   cal. pulse: 1 mV / 200 ms\",\n                 fontsize=12)\n    fig.tight_layout(rect=[0, 0, 1, 0.98])\n    plt.show()\n    return record","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-27T10:33:49.864413Z","iopub.execute_input":"2026-08-27T10:33:49.864664Z","iopub.status.idle":"2026-08-27T10:33:49.887791Z","shell.execute_reply.started":"2026-08-27T10:33:49.864634Z","shell.execute_reply":"2026-08-27T10:33:49.886908Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"for record_path in sample_records:\n    print(f\"\\nRecord: {os.path.relpath(record_path, ROOT_DIR)}\")\n    try:\n        plot_wfdb_record(record_path)\n    except Exception as e:\n        print(f\"  Could not read {record_path}: {e}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-27T10:33:49.889427Z","iopub.execute_input":"2026-08-27T10:33:49.889693Z","iopub.status.idle":"2026-08-27T10:33:56.787121Z","shell.execute_reply.started":"2026-08-27T10:33:49.889672Z","shell.execute_reply":"2026-08-27T10:33:56.785965Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 2. Display the paired ECG images\n\nFor the sample record(s) in `SAMPLE_RECORDS` we display up to `MAX_IMAGES_PER_RECORD` of its image variants. Images named `Dn_[RECORD-ID]_[VARIANT-ID]` are different imaging/artifact variants (electronic printouts, color/grayscale scans, phone photos, monitor photos, wrinkled/stained/water-damaged paper, etc.) of the WFDB record `Dn_[RECORD-ID]` plotted above — the WFDB signal is the ground truth for all of them.\n\nTo browse *every* image of *every* record instead, loop over `hea_files` and drop the two limits.","metadata":{}},{"cell_type":"code","source":"IMAGE_EXTS = (\".png\", \".jpg\", \".jpeg\", \".bmp\", \".gif\", \".tif\", \".tiff\", \".webp\")\n\nfor record_path in sample_records:\n    record_dir  = os.path.dirname(record_path)\n    record_name = os.path.basename(record_path)\n\n    # All images in this record's folder (its imaging/artifact variants)\n    images = sorted(\n        f for f in glob.glob(os.path.join(record_dir, \"*\"))\n        if f.lower().endswith(IMAGE_EXTS)\n    )\n    print(f\"\\nRecord {record_name}: {len(images)} image variant(s), \"\n          f\"showing up to {MAX_IMAGES_PER_RECORD}\")\n\n    for path in images[:MAX_IMAGES_PER_RECORD]:\n        try:\n            img = Image.open(path)\n            plt.figure(figsize=(10, 10 * img.height / max(img.width, 1)))\n            plt.imshow(img, cmap=\"gray\" if img.mode in (\"L\", \"1\") else None)\n            plt.title(os.path.basename(path))\n            plt.axis(\"off\")\n            plt.show()\n        except Exception as e:\n            print(f\"Could not open {path}: {e}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-27T10:33:56.788303Z","iopub.execute_input":"2026-08-27T10:33:56.788613Z","iopub.status.idle":"2026-08-27T10:34:03.714852Z","shell.execute_reply.started":"2026-08-27T10:33:56.788589Z","shell.execute_reply":"2026-08-27T10:34:03.713683Z"}},"outputs":[],"execution_count":null}]}