{"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":31239,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# ECG Digitization: Centerline Extraction v1–v4 Comparison\n\nThis notebook documents an iterative refinement of ECG waveform extraction\nfrom scanned ECG images, focusing on centerline estimation and post-processing.\n\nRather than introducing a fully novel method, this work emphasizes:\n- Stability of centerline extraction\n- Reduction of large-scale drift\n- Visual and quantitative comparison across versions (v1–v4)\n\nThis notebook is intended as a transparent research log and comparison study.\n","metadata":{}},{"cell_type":"markdown","source":"###  1. Imports & Environment","metadata":{}},{"cell_type":"code","source":"import numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nimport os, cv2, numpy as np, pandas as pd, matplotlib.pyplot as plt\n\nfrom scipy.ndimage import median_filter, gaussian_filter1d\nfrom scipy.signal import savgol_filter\n\nplt.rcParams[\"figure.figsize\"] = (12,4)\nplt.rcParams[\"axes.grid\"] = True\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-21T04:33:18.354833Z","iopub.execute_input":"2025-12-21T04:33:18.355267Z","iopub.status.idle":"2025-12-21T04:33:19.384253Z","shell.execute_reply.started":"2025-12-21T04:33:18.355236Z","shell.execute_reply":"2025-12-21T04:33:19.383012Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 2. Load ECG Mask / Preprocessed Image","metadata":{}},{"cell_type":"markdown","source":"## Data\nThis notebook assumes that ECG waveform masks have already been extracted\nfrom the original images (e.g., via thresholding or segmentation).\n\nThe comparison focuses on **post-mask centerline extraction**.\n","metadata":{}},{"cell_type":"code","source":"#\n# Kaggle 上のデータ構造に合わせる\nBASE_PATH = \"//kaggle/input/physionet-ecg-image-digitization/\"\nTRAIN_DIR = f\"{BASE_PATH}/train\"\nTEST_DIR = f\"{BASE_PATH}/test\"\n\n# サンプルIDを選択\nsample_id = \"1006427285\"\nimg_name = f\"{sample_id}-0001.png\"\nimg_path = f\"{TRAIN_DIR}/{sample_id}/{img_name}\"\nprint(\"Image path:\", img_path)\n\nprint(\"Image path2:\",\"/kaggle/input/physionet-ecg-image-digitization/train/1006427285/1006427285-0001.png\")\n\n# Read Image\nimg = cv2.imread(img_path)\n##print(\"img:\",np.array(img))\ngray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)\n\nplt.figure(figsize=(12,6))\nplt.imshow(gray, cmap='gray')\nplt.title(f\"Sample ECG: {img_name}\")\nplt.axis('off')\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-21T04:33:23.777129Z","iopub.execute_input":"2025-12-21T04:33:23.777726Z","iopub.status.idle":"2025-12-21T04:33:24.491633Z","shell.execute_reply.started":"2025-12-21T04:33:23.777690Z","shell.execute_reply":"2025-12-21T04:33:24.490319Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"img_hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)\nmask_red = cv2.inRange(img_hsv, (0,50,50), (10,255,255)) | cv2.inRange(img_hsv, (160,50,50), (180,255,255))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-21T04:33:29.369099Z","iopub.execute_input":"2025-12-21T04:33:29.369486Z","iopub.status.idle":"2025-12-21T04:33:29.399642Z","shell.execute_reply.started":"2025-12-21T04:33:29.369457Z","shell.execute_reply":"2025-12-21T04:33:29.398316Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 3. Shared Preprocessing\nRed waveform extraction using HSV thresholding.\n","metadata":{}},{"cell_type":"code","source":"img_hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)\n\nmask_red = (\n    cv2.inRange(img_hsv, (0,50,50), (10,255,255)) |\n    cv2.inRange(img_hsv, (160,50,50), (180,255,255))\n)\n\nplt.imshow(mask_red, cmap=\"gray\")\nplt.title(\"Red Mask\")\nplt.axis(\"off\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-21T04:33:32.092174Z","iopub.execute_input":"2025-12-21T04:33:32.092564Z","iopub.status.idle":"2025-12-21T04:33:32.602649Z","shell.execute_reply.started":"2025-12-21T04:33:32.092536Z","shell.execute_reply":"2025-12-21T04:33:32.601325Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 4. Centerline Extraction Functions\nEach version differs mainly in how the centerline is estimated and smoothed.\n","metadata":{}},{"cell_type":"code","source":"def extract_v1(mask):\n    # simplest: mean y of non-zero pixels\n    H, W = mask.shape\n    y = np.full(W, np.nan)\n    for x in range(W):\n        ys = np.where(mask[:,x]>0)[0]\n        if len(ys)>0:\n            y[x] = ys.mean()\n    return pd.Series(y).interpolate().values\n\n\ndef extract_v2(mask):\n    # median + gaussian smoothing\n    y = extract_v1(mask)\n    y = gaussian_filter1d(y, sigma=3)\n    return y\n\n\ndef extract_v3(mask):\n    # cog + peak hybrid\n    H, W = mask.shape\n    y_cog = np.full(W, np.nan)\n    y_peak = np.full(W, np.nan)\n\n    for x in range(W):\n        ys = np.where(mask[:,x]>0)[0]\n        if len(ys)>0:\n            y_cog[x] = ys.mean()\n            y_peak[x] = np.median(ys)\n\n    y = np.nanmean(np.vstack([y_cog, y_peak]), axis=0)\n    return pd.Series(y).interpolate().values\n\n\ndef extract_v4(mask):\n    # v4.1 style: hybrid + median + gaussian + savgol\n    y = extract_v3(mask)\n    y = median_filter(y, size=5)\n    y = gaussian_filter1d(y, sigma=4)\n    y = savgol_filter(y, 21, 3)\n    return y\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-21T04:33:39.748733Z","iopub.execute_input":"2025-12-21T04:33:39.749104Z","iopub.status.idle":"2025-12-21T04:33:39.759277Z","shell.execute_reply.started":"2025-12-21T04:33:39.749077Z","shell.execute_reply":"2025-12-21T04:33:39.757861Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 5. Comparison of v1–v4 Centerlines\n\nWe compare four versions:\n\n- **v1**: Simple center-of-mass\n- **v2**: Center-of-mass + interpolation\n- **v3**: Hybrid (center-of-mass + peak)\n- **v4**: Robust hybrid + denoising + smoothing\n\n","metadata":{}},{"cell_type":"code","source":"y1 = extract_v1(mask_red)\ny2 = extract_v2(mask_red)\ny3 = extract_v3(mask_red)\ny4 = extract_v4(mask_red)\n\nfig, axes = plt.subplots(4,1, sharex=True, figsize=(12,10))\n\nfor ax, y, title in zip(\n    axes,\n    [y1,y2,y3,y4],\n    [\"v1: mean\", \"v2: gaussian\", \"v3: cog+peak\", \"v4: hybrid + smoothing\"]\n):\n    ax.plot(y, color=\"red\")\n    ax.set_title(title)\n    ax.invert_yaxis()\n\nplt.xlabel(\"x (pixel)\")\nplt.tight_layout()\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-21T04:33:42.315304Z","iopub.execute_input":"2025-12-21T04:33:42.315685Z","iopub.status.idle":"2025-12-21T04:33:43.518067Z","shell.execute_reply.started":"2025-12-21T04:33:42.315657Z","shell.execute_reply":"2025-12-21T04:33:43.517114Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 6. Discussion\n\nObservations:\n- v1 is highly noisy and unstable\n- v2 reduces noise but blurs peaks\n- v3 preserves morphology but still shows local jitter\n- v4 provides the most visually stable waveform\n\nHowever:\n- SNR values are still low\n- This suggests that **post-centerline processing**\n  (baseline correction, normalization, denoising)\n  is the next bottleneck\n","metadata":{}},{"cell_type":"markdown","source":"## 7. Next Steps\n\nPlanned improvements:\n1. Baseline wander removal\n2. Amplitude normalization using grid scale\n3. Frequency-domain noise suppression\n4. Lead-wise alignment before SNR evaluation\n\nThis notebook will serve as a foundation for future refinements.\n","metadata":{}},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}