{"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":false,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import os\n\nimport numpy as np\nimport pandas as pd\nfrom PIL import Image\nimport matplotlib.pyplot as plt\nfrom scipy.signal import resample","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-10-22T14:51:58.357059Z","iopub.execute_input":"2025-10-22T14:51:58.357333Z","iopub.status.idle":"2025-10-22T14:52:01.944274Z","shell.execute_reply.started":"2025-10-22T14:51:58.357313Z","shell.execute_reply":"2025-10-22T14:52:01.943213Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 1. Data Preparation","metadata":{}},{"cell_type":"code","source":"df_test = pd.read_csv('/kaggle/input/physionet-ecg-image-digitization/test.csv')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-22T14:52:01.945224Z","iopub.execute_input":"2025-10-22T14:52:01.945707Z","iopub.status.idle":"2025-10-22T14:52:01.966091Z","shell.execute_reply.started":"2025-10-22T14:52:01.945675Z","shell.execute_reply":"2025-10-22T14:52:01.965054Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"df_test","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-22T14:52:01.969053Z","iopub.execute_input":"2025-10-22T14:52:01.969469Z","iopub.status.idle":"2025-10-22T14:52:02.016689Z","shell.execute_reply.started":"2025-10-22T14:52:01.969445Z","shell.execute_reply":"2025-10-22T14:52:02.015727Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 2. Prediction","metadata":{}},{"cell_type":"code","source":"# (x1, y1, x2, y2)\nCROP_COORD_DICT = {\n    # row 1\n    'I': (125, 580, 605, 850),\n    'aVR': (615, 580, 1095, 850),\n    'V1': (1105, 580, 1590, 850),\n    'V4': (1600, 580, 2085, 850),\n    # row 2\n    # 'II': (125, 850, 605, 1130),\n    'aVL': (615, 850, 1095, 1130),\n    'V2': (1105, 850, 1590, 1130),\n    'V5': (1600, 850, 2085, 1130),\n    # row 3\n    'III': (125, 1130, 605, 1410),\n    'aVF': (615, 1130, 1095, 1410),\n    'V3': (1105, 1130, 1590, 1410),\n    'V6': (1600, 1130, 2085, 1410),\n    # row 4\n    'II': (125, 1410, 2085, 1620),\n}\n\nI_REF_SCALE = 0.719 / 51.41695713533565\nCALIB_SCALE_DICT = {\n    # row 1\n    'I': I_REF_SCALE,\n    'aVR': I_REF_SCALE,\n    'V1': I_REF_SCALE,\n    'V4': I_REF_SCALE,\n    # row 2\n    # 'II': ,\n    'aVL': I_REF_SCALE * (CROP_COORD_DICT['I'][3] - CROP_COORD_DICT['I'][1]) / (CROP_COORD_DICT['aVL'][3] - CROP_COORD_DICT['aVL'][1]),\n    'V2': I_REF_SCALE * (CROP_COORD_DICT['I'][3] - CROP_COORD_DICT['I'][1]) / (CROP_COORD_DICT['V2'][3] - CROP_COORD_DICT['V2'][1]),\n    'V5': I_REF_SCALE * (CROP_COORD_DICT['I'][3] - CROP_COORD_DICT['I'][1]) / (CROP_COORD_DICT['V5'][3] - CROP_COORD_DICT['V5'][1]),\n    # row 3\n    'III': I_REF_SCALE * (CROP_COORD_DICT['I'][3] - CROP_COORD_DICT['I'][1]) / (CROP_COORD_DICT['III'][3] - CROP_COORD_DICT['III'][1]),\n    'aVF': I_REF_SCALE * (CROP_COORD_DICT['I'][3] - CROP_COORD_DICT['I'][1]) / (CROP_COORD_DICT['aVF'][3] - CROP_COORD_DICT['aVF'][1]),\n    'V3': I_REF_SCALE * (CROP_COORD_DICT['I'][3] - CROP_COORD_DICT['I'][1]) / (CROP_COORD_DICT['V3'][3] - CROP_COORD_DICT['V3'][1]),\n    'V6': I_REF_SCALE * (CROP_COORD_DICT['I'][3] - CROP_COORD_DICT['I'][1]) / (CROP_COORD_DICT['V6'][3] - CROP_COORD_DICT['V6'][1]),\n    # row 4\n    'II': I_REF_SCALE * (CROP_COORD_DICT['I'][3] - CROP_COORD_DICT['I'][1]) / (CROP_COORD_DICT['II'][3] - CROP_COORD_DICT['II'][1]),\n}\n\n\ndef clean_and_resample(y: np.ndarray, num: int):\n    s = pd.Series(y)\n    s = s.interpolate(limit_direction=\"both\")\n    s = s.ffill().bfill()\n    y_filled = s.to_numpy()\n    return resample(y_filled, num)\n\n\ndef min_max_scale(y: np.ndarray):\n    return (y - y.min()) / max(y.max() - y.min(), 1e-6)\n\n\ndef adjust_scale(y: np.ndarray, lead: str):\n    y_scaled = CALIB_SCALE_DICT[lead] * y\n    y_scaled = y_scaled - y_scaled.mean()\n    return y_scaled\n\n\ndef extract_y_coord(pil_image_cropped, lead) -> np.ndarray:\n    cropped_np = np.asarray(pil_image_cropped)[:, :, :3]\n    # mask = ((cropped_np < 60) & (cropped_np > 5)).all(axis=-1)\n    mask = (cropped_np < 60).all(axis=-1)\n    text_region_mask = np.ones_like(mask)\n    text_region_mask_max_x = int(len(lead)*0.035*text_region_mask.shape[1]) if lead != 'II' else int(0.25*len(lead)*0.035*text_region_mask.shape[1])\n    text_region_mask_min_y = int(0.56*text_region_mask.shape[0])\n    text_region_mask[text_region_mask_min_y:, :text_region_mask_max_x] = 0\n    # plt.imshow(mask)\n    # plt.show()\n    # plt.imshow(text_region_mask)\n    # plt.show()\n    mask &= text_region_mask\n    y_idx = np.arange(mask.shape[0])[:, None]\n    sum_y = (mask * y_idx).sum(axis=0)\n    count_y = mask.sum(axis=0)\n    y_coords = np.full(mask.shape[1], np.nan, dtype=float)\n    y_coords[count_y > 0] = sum_y[count_y > 0] / count_y[count_y > 0]\n    if np.all(np.isnan(y_coords)):\n        print(\"[Warning] extract_y_coord: all NaN detected, filling with zeros.\")\n        y_coords[:] = 0.0\n    return -y_coords, mask\n\n\ndef predict_single_id(base_id: int, df_test: pd.DataFrame = df_test, debug: bool = False) -> pd.DataFrame:\n    df_test_tgt_id = df_test[df_test['id'] == base_id]\n    pil_image = Image.open(f\"/kaggle/input/physionet-ecg-image-digitization/test/{base_id}.png\")\n    df_pred_list = []\n    for i, sr_row in df_test_tgt_id.iterrows():\n        lead = sr_row.lead\n        n_rows = sr_row.number_of_rows\n        crop_bbox = CROP_COORD_DICT[lead]\n        pil_image_cropped = pil_image.crop(crop_bbox)\n        y_coords, mask = extract_y_coord(pil_image_cropped, lead)\n        y_processed = clean_and_resample(y_coords, n_rows)\n        y_processed = adjust_scale(y_processed, lead)\n        if debug:\n            plt.plot(y_processed)\n            plt.title(f'extracted signal / {base_id}_{lead}')\n            plt.show()\n            plt.imshow(mask)\n            plt.title(f'mask / {base_id}_{lead}')\n            plt.show()\n            display(pil_image_cropped)\n            print(100*'=')\n        df_pred = pd.DataFrame({\n            'id': [f'{base_id}_{row_id}_{lead}' for row_id in range(len(y_processed))],\n            'value': y_processed\n        })\n        df_pred_list.append(df_pred)\n    df_pred = pd.concat(df_pred_list)\n    return df_pred","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-22T14:52:02.017588Z","iopub.execute_input":"2025-10-22T14:52:02.017899Z","iopub.status.idle":"2025-10-22T14:52:02.037466Z","shell.execute_reply.started":"2025-10-22T14:52:02.017870Z","shell.execute_reply":"2025-10-22T14:52:02.036161Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"uniq_ids = df_test['id'].unique().tolist()\n\ndebug = False if os.getenv('KAGGLE_IS_COMPETITION_RERUN') else True\ndf_pred = pd.concat([predict_single_id(base_id, debug=debug) for base_id in uniq_ids])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-22T14:52:02.038586Z","iopub.execute_input":"2025-10-22T14:52:02.038905Z","iopub.status.idle":"2025-10-22T14:52:10.863670Z","shell.execute_reply.started":"2025-10-22T14:52:02.038875Z","shell.execute_reply":"2025-10-22T14:52:10.862807Z"},"scrolled":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"df_pred","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-22T14:52:10.864666Z","iopub.execute_input":"2025-10-22T14:52:10.864983Z","iopub.status.idle":"2025-10-22T14:52:10.878066Z","shell.execute_reply.started":"2025-10-22T14:52:10.864955Z","shell.execute_reply":"2025-10-22T14:52:10.876877Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 3. Submission","metadata":{}},{"cell_type":"code","source":"df_pred.to_csv('submission.csv', index=False)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-22T14:52:14.494168Z","iopub.execute_input":"2025-10-22T14:52:14.495098Z","iopub.status.idle":"2025-10-22T14:52:14.709227Z","shell.execute_reply.started":"2025-10-22T14:52:14.495059Z","shell.execute_reply":"2025-10-22T14:52:14.708021Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"pd.read_csv('submission.csv')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-22T14:52:14.710475Z","iopub.execute_input":"2025-10-22T14:52:14.710715Z","iopub.status.idle":"2025-10-22T14:52:14.779686Z","shell.execute_reply.started":"2025-10-22T14:52:14.710695Z","shell.execute_reply":"2025-10-22T14:52:14.778792Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 4. Train Sample Evaluation","metadata":{}},{"cell_type":"code","source":"def predict_single_id_train(base_id: int, df_test: pd.DataFrame = df_test, debug: bool = False) -> pd.DataFrame:\n    df_train_tgt_id = pd.read_csv(f'/kaggle/input/physionet-ecg-image-digitization/train/{base_id}/{base_id}.csv')\n    pil_image = Image.open(f\"/kaggle/input/physionet-ecg-image-digitization/train/{base_id}/{base_id}-0001.png\")\n    df_pred_list = []\n    for lead, crop_bbox in CROP_COORD_DICT.items():\n        n_rows = len(df_train_tgt_id[lead].dropna())\n        pil_image_cropped = pil_image.crop(crop_bbox)\n        y_coords, mask = extract_y_coord(pil_image_cropped, lead)\n        y_processed = clean_and_resample(y_coords, n_rows)\n        y_processed = adjust_scale(y_processed, lead)\n        if debug:\n            plt.plot(y_processed)\n            plt.title(f'{base_id}_{lead}')\n            plt.show()\n            display(pil_image_cropped)\n            print(100*'=')\n        df_pred = pd.DataFrame({\n            'id': [f'{base_id}_{row_id}_{lead}' for row_id in range(len(y_processed))],\n            'value': y_processed\n        })\n        df_pred['base_id'] = base_id\n        df_pred['lead'] = lead\n        df_pred_list.append(df_pred)\n    df_pred = pd.concat(df_pred_list)\n    return df_pred","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-22T14:52:16.764179Z","iopub.execute_input":"2025-10-22T14:52:16.764518Z","iopub.status.idle":"2025-10-22T14:52:16.772227Z","shell.execute_reply.started":"2025-10-22T14:52:16.764495Z","shell.execute_reply":"2025-10-22T14:52:16.771357Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"sample_base_id = 1006427285\n# sample_base_id = 1220748004\n# sample_base_id = 1475354244\n# sample_base_id = 1643754023","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-22T14:52:18.267966Z","iopub.execute_input":"2025-10-22T14:52:18.268279Z","iopub.status.idle":"2025-10-22T14:52:18.272895Z","shell.execute_reply.started":"2025-10-22T14:52:18.268256Z","shell.execute_reply":"2025-10-22T14:52:18.271802Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"df_gt_tgt_id = pd.read_csv(f'/kaggle/input/physionet-ecg-image-digitization/train/{sample_base_id}/{sample_base_id}.csv')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-22T14:52:18.442933Z","iopub.execute_input":"2025-10-22T14:52:18.443291Z","iopub.status.idle":"2025-10-22T14:52:18.471577Z","shell.execute_reply.started":"2025-10-22T14:52:18.443267Z","shell.execute_reply":"2025-10-22T14:52:18.470488Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"df_pred_tgt_id = predict_single_id_train(sample_base_id)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-22T14:52:18.713865Z","iopub.execute_input":"2025-10-22T14:52:18.714712Z","iopub.status.idle":"2025-10-22T14:52:18.917689Z","shell.execute_reply.started":"2025-10-22T14:52:18.714681Z","shell.execute_reply":"2025-10-22T14:52:18.916308Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"for lead, crop_bbox in CROP_COORD_DICT.items():\n    df_pred_tgt_id[(df_pred_tgt_id['lead'] == lead)]['value'].reset_index(drop=True).plot(label='pred')\n    df_gt_tgt_id[lead].dropna().reset_index(drop=True).plot(label='gt')\n    plt.legend()\n    plt.title(lead)\n    plt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-22T14:52:51.169932Z","iopub.execute_input":"2025-10-22T14:52:51.170247Z","iopub.status.idle":"2025-10-22T14:52:53.798785Z","shell.execute_reply.started":"2025-10-22T14:52:51.170225Z","shell.execute_reply":"2025-10-22T14:52:53.797647Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## ","metadata":{}}]}