{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.12.12","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"none","dataSources":[],"dockerImageVersionId":31234,"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-17T07:37:16.002194Z","iopub.execute_input":"2025-12-17T07:37:16.002573Z","iopub.status.idle":"2025-12-17T07:37:16.450447Z","shell.execute_reply.started":"2025-12-17T07:37:16.002547Z","shell.execute_reply":"2025-12-17T07:37:16.449341Z"}},"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":"# placeholder\nmask = ...   # binary ECG mask (H, W)\ngrid_dx = ...\ngrid_dy = ...\nscale_time = ...\nscale_mv = ...","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-17T07:53:58.677537Z","iopub.execute_input":"2025-12-17T07:53:58.677878Z","iopub.status.idle":"2025-12-17T07:53:58.690019Z","shell.execute_reply.started":"2025-12-17T07:53:58.677851Z","shell.execute_reply":"2025-12-17T07:53:58.688452Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"input_dir = \"/kaggle/input/physionet-ecg-image-digitization/train/1006867983/\"       # ECG画像を入れたフォルダを指定\noutput_dir = \"./ecg_csv_outputs\"  # CSV出力先\n\nos.makedirs(output_dir, exist_ok=True)\n\n# 画像ファイルの一覧取得（png, jpgどちらも対応）\nimage_files = sorted(glob.glob(os.path.join(input_dir, \"*.png\")) +\n                     glob.glob(os.path.join(input_dir, \"*.jpg\")))\n\nprint(f\"Found {len(image_files)} ECG images.\")\n# =====================================================\n# パラメータ設定\n# =====================================================\n#grid_dx = 20   # 1mmあたりのピクセル（横）\n#grid_dy = 20   # 1mmあたりのピクセル（縦）\ngrid_dx = 20 #22   # 画像の横スケールが広めなら少し大きく\ngrid_dy = 10#18   # 縦スケールが狭いなら少し小さく\nscale_time = 25.0  # 25mm/s\nscale_mv = 10.0    # 10mm/mV","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"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-17T07:54:49.642718Z","iopub.execute_input":"2025-12-17T07:54:49.643539Z","iopub.status.idle":"2025-12-17T07:54:49.710409Z","shell.execute_reply.started":"2025-12-17T07:54:49.643504Z","shell.execute_reply":"2025-12-17T07:54:49.709388Z"}},"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-17T07:54:53.374315Z","iopub.execute_input":"2025-12-17T07:54:53.374653Z","iopub.status.idle":"2025-12-17T07:54:53.382817Z","shell.execute_reply.started":"2025-12-17T07:54:53.374624Z","shell.execute_reply":"2025-12-17T07:54:53.381578Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\n\n\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","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"#### 3. Centerline Extraction Functions","metadata":{}},{"cell_type":"markdown","source":"## Centerline Extraction Versions\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","metadata":{}},{"cell_type":"code","source":"## V1\n\ndef extract_v1(mask):\n    H, W = mask.shape\n    y = np.zeros(W)\n    for x in range(W):\n        ys = np.where(mask[:, x] > 0)[0]\n        y[x] = np.mean(ys) if len(ys) > 0 else np.nan\n    return pd.Series(y).interpolate().values\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-17T07:39:27.629067Z","iopub.execute_input":"2025-12-17T07:39:27.629452Z","iopub.status.idle":"2025-12-17T07:39:27.635400Z","shell.execute_reply.started":"2025-12-17T07:39:27.629425Z","shell.execute_reply":"2025-12-17T07:39:27.634480Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"## v2\n\ndef extract_v2(mask):\n    y = extract_v1(mask)\n    return gaussian_filter1d(y, sigma=2)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-17T07:39:49.652403Z","iopub.execute_input":"2025-12-17T07:39:49.653065Z","iopub.status.idle":"2025-12-17T07:39:49.657280Z","shell.execute_reply.started":"2025-12-17T07:39:49.653031Z","shell.execute_reply":"2025-12-17T07:39:49.656561Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"## v3\n\ndef extract_v3(mask):\n    H, W = mask.shape\n    y_cog = np.zeros(W)\n    y_peak = np.zeros(W)\n\n    for x in range(W):\n        ys = np.where(mask[:, x] > 0)[0]\n        if len(ys) == 0:\n            y_cog[x] = np.nan\n            y_peak[x] = np.nan\n        else:\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","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-17T07:40:10.359699Z","iopub.execute_input":"2025-12-17T07:40:10.360060Z","iopub.status.idle":"2025-12-17T07:40:10.369685Z","shell.execute_reply.started":"2025-12-17T07:40:10.360032Z","shell.execute_reply":"2025-12-17T07:40:10.368764Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"## v4\n\ndef extract_v4(mask):\n    y = extract_v3(mask)\n\n    # spike removal\n    y = median_filter(y, size=5)\n\n    # multi-stage smoothing\n    y = gaussian_filter1d(y, sigma=4)\n    y = savgol_filter(y, window_length=21, polyorder=3)\n\n    return y\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-17T07:40:46.285133Z","iopub.execute_input":"2025-12-17T07:40:46.285530Z","iopub.status.idle":"2025-12-17T07:40:46.290962Z","shell.execute_reply.started":"2025-12-17T07:40:46.285500Z","shell.execute_reply":"2025-12-17T07:40:46.290016Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"#### 4. Pixel → Time / Voltage Conversion","metadata":{}},{"cell_type":"markdown","source":"## Pixel to Physical Units Conversion\n","metadata":{}},{"cell_type":"code","source":"def to_physical(y, grid_dx, grid_dy, scale_time, scale_mv):\n    baseline = np.nanmedian(y)\n    time = np.arange(len(y)) / (grid_dx * (scale_time / 25.0))\n    voltage = ((baseline - y) / grid_dy) * (1.0 / (scale_mv / 10.0))\n    return time, voltage\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-17T07:42:03.432507Z","iopub.execute_input":"2025-12-17T07:42:03.432875Z","iopub.status.idle":"2025-12-17T07:42:03.438260Z","shell.execute_reply.started":"2025-12-17T07:42:03.432846Z","shell.execute_reply":"2025-12-17T07:42:03.437168Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"#### 5. v1–v4 Comparison Plot","metadata":{}},{"cell_type":"code","source":"versions = {\n    \"v1\": extract_v1(mask),\n    \"v2\": extract_v2(mask),\n    \"v3\": extract_v3(mask),\n    \"v4\": extract_v4(mask),\n}\n\nplt.figure(figsize=(14,6))\nfor name, y in versions.items():\n    t, v = to_physical(y, grid_dx, grid_dy, scale_time, scale_mv)\n    plt.plot(t, v, label=name, alpha=0.85)\n\nplt.legend()\nplt.xlabel(\"Time [sec]\")\nplt.ylabel(\"Voltage [mV]\")\nplt.title(\"Centerline Extraction Comparison (v1–v4)\")\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-17T07:42:39.951348Z","iopub.execute_input":"2025-12-17T07:42:39.951781Z","iopub.status.idle":"2025-12-17T07:42:39.962109Z","shell.execute_reply.started":"2025-12-17T07:42:39.951749Z","shell.execute_reply":"2025-12-17T07:42:39.960929Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}