{"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":"nvidiaTeslaT4","dataSources":[{"sourceId":97984,"databundleVersionId":14096757,"sourceType":"competition"},{"sourceId":132436161,"sourceType":"kernelVersion"},{"sourceId":690094,"sourceType":"modelInstanceVersion","isSourceIdPinned":true,"modelInstanceId":523118,"modelId":537126}],"dockerImageVersionId":31234,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import torch\nimport cv2\nimport numpy as np\nimport pandas as pd\nimport albumentations as A\nfrom albumentations.pytorch import ToTensorV2\nfrom glob import glob\nfrom tqdm.notebook import tqdm\n\nimport os","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-12-17T06:54:50.070722Z","iopub.execute_input":"2025-12-17T06:54:50.071434Z","iopub.status.idle":"2025-12-17T06:54:50.075485Z","shell.execute_reply.started":"2025-12-17T06:54:50.071403Z","shell.execute_reply":"2025-12-17T06:54:50.074578Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Cell 1: 自动寻找并安装离线包 (Auto Offline Install)\nimport os\nimport sys\nfrom glob import glob\n\nprint(\"🔍 Searching for offline packages...\")\n\n# 1. 自动搜索路径\ninput_root = '/kaggle/input'\nlib_path = None\n\n# 列出 input 下所有目录，找带 segment 和 offline 关键字的\nfor d in os.listdir(input_root):\n    if 'segmentation' in d.lower() and 'offline' in d.lower():\n        lib_path = os.path.join(input_root, d)\n        print(f\"✅ Found offline lib dataset at: {lib_path}\")\n        break\n\n# 2. 如果没找到，打印所有目录帮你看一眼\nif lib_path is None:\n    print(\"❌ Could not find folder. Current inputs:\")\n    print(os.listdir(input_root))\n    print(\"👉 Please make sure you added the dataset!\")\nelse:\n    # 3. 执行安装命令\n    print(f\"📦 Installing from {lib_path}...\")\n    # 使用 !pip install 的魔法命令\n    try:\n        # 注意：这里我们用 python 的 subprocess 或者直接用 ! (在 Jupyter 里)\n        # 为了保证路径没问题，我们构造完整的命令字符串\n        cmd = f\"pip install -q segmentation-models-pytorch --no-index --find-links '{lib_path}'\"\n        os.system(cmd)\n        \n        # 验证是否成功\n        import segmentation_models_pytorch as smp\n        print(\"🎉 Success! segmentation_models_pytorch is installed.\")\n        \n    except ImportError:\n        print(\"⚠️ Installation seemed to run, but import failed. Trying one more backup method...\")\n        # 备用方案：有时候路径甚至可以是上一级，或者是里面的子目录\n        # 尝试直接把那个目录加入 sys.path (如果是 pure python 包)\n        sys.path.append(lib_path)\n        try:\n            import segmentation_models_pytorch as smp\n            print(\"🎉 Success (via sys.path)!\")\n        except:\n            print(\"❌ Installation failed.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-17T06:54:50.077057Z","iopub.execute_input":"2025-12-17T06:54:50.077305Z","iopub.status.idle":"2025-12-17T06:54:53.243186Z","shell.execute_reply.started":"2025-12-17T06:54:50.077274Z","shell.execute_reply":"2025-12-17T06:54:53.242243Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nfrom glob import glob\nprint(\"🔍 Scanning /kaggle/input for exact paths...\")\n# 1. 寻找比赛数据目录 (找包含 test.csv 或 test 文件夹的目录)\ncomp_path = None\npotential_dirs = glob(\"/kaggle/input/*\")\nfor d in potential_dirs:\n    if os.path.exists(f\"{d}/test\") and os.path.exists(f\"{d}/sample_submission.csv\") or os.path.exists(f\"{d}/sample_submission.parquet\"):\n        comp_path = d\n        print(f\"✅ Found Competition Data at: {comp_path}\")\n        break\nif not comp_path:\n    # 尝试更宽泛的搜索\n    for d in potential_dirs:\n        if \"physionet\" in d.lower():\n            comp_path = d\n            print(f\"⚠️ Guessed Competition Data at: {comp_path} (Please verify)\")\n            break\n# 2. 寻找模型文件 (找 .pth)\nmodel_file_path = None\n# 搜索 input 下所有的 .pth 文件\nall_pths = glob(\"/kaggle/input/**/*.pth\", recursive=True)\nif len(all_pths) > 0:\n    model_file_path = all_pths[0] # 默认取第一个\n    print(f\"✅ Found Model Checkpoint at: {model_file_path}\")\n    if len(all_pths) > 1:\n        print(f\"   (Also found others: {all_pths[1:]}, using the first one)\")\nelse:\n    print(\"❌ Error: No .pth file found! Did you add your model dataset?\")\n# --- 3. 最终确认 ---\nif comp_path and model_file_path:\n    print(\"\\n🚀 Ready! Please copy these paths into your main code:\")\n    print(f\"TEST_IMG_DIR = '{comp_path}/test'\")\n    print(f\"MODEL_PATH = '{model_file_path}'\")\nelse:\n    print(\"\\n❌ Path detection failed. Please check your Inputs.\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-17T06:54:53.244154Z","iopub.execute_input":"2025-12-17T06:54:53.244439Z","iopub.status.idle":"2025-12-17T06:54:54.652364Z","shell.execute_reply.started":"2025-12-17T06:54:53.244413Z","shell.execute_reply":"2025-12-17T06:54:54.651624Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ================= 配置 =================\nMODEL_PATH = '/kaggle/input/12-17-10000-training-v1/pytorch/default/1/unet_512_crops_10k_v1.pth'\nTEST_IMG_DIR = '/kaggle/input/physionet-ecg-image-digitization/test'\nDEVICE = 'cuda' if torch.cuda.is_available() else 'cpu'\n# =======================================\n# 1. 核心武器：自动切图函数\ndef split_leads(image, debug=False):\n    \"\"\"\n    将一张 ECG 图片切分为 12 个标准导联区域。\n    假设布局为标准的 3行 x 4列 (3x4 Layout)。\n    \"\"\"\n    # 步骤 A: 找到有效内容区域 (ROI)\n    # 很多图片周围有大量留白，我们需要切掉它，否则切分比例会对不准\n    gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)\n    # 二值化，把波形和网格变黑，背景变白 (或反之)\n    # 这里我们要找非背景区域。假设背景是浅色。\n    _, binary = cv2.threshold(gray, 200, 255, cv2.THRESH_BINARY_INV)\n    \n    # 找轮廓\n    coords = cv2.findNonZero(binary)\n    if coords is not None:\n        x, y, w, h = cv2.boundingRect(coords)\n        # 稍微加一点 padding，防止切到波峰\n        pad = 5\n        x = max(0, x - pad)\n        y = max(0, y - pad)\n        w = min(image.shape[1] - x, w + 2*pad)\n        h = min(image.shape[0] - y, h + 2*pad)\n        \n        # 裁剪出 ROI\n        roi = image[y:y+h, x:x+w]\n    else:\n        # 如果找不到内容，就用原图\n        roi = image\n    # 步骤 B: 3x4 网格切分\n    # 标准顺序：\n    # Row 1: I, aVR, V1, V4\n    # Row 2: II, aVL, V2, V5\n    # Row 3: III, aVF, V3, V6\n    \n    leads = {}\n    lead_names = [\n        ['I', 'aVR', 'V1', 'V4'],\n        ['II', 'aVL', 'V2', 'V5'],\n        ['III', 'aVF', 'V3', 'V6']\n    ]\n    \n    rh, rw = roi.shape[:2]\n    row_h = rh // 3\n    col_w = rw // 4\n    \n    for r in range(3):\n        for c in range(4):\n            name = lead_names[r][c]\n            # 计算坐标\n            y1 = r * row_h\n            y2 = (r + 1) * row_h\n            x1 = c * col_w\n            x2 = (c + 1) * col_w\n            \n            # 切片\n            crop = roi[y1:y2, x1:x2]\n            leads[name] = crop\n            \n    return leads\n# 2. 信号提取工具\ndef extract_signal_from_mask(mask, target_len=1000):\n    h, w = mask.shape\n    signal = []\n    # 简单的重心法 (Center of Mass)\n    for col in range(w):\n        col_data = mask[:, col]\n        rows = np.where(col_data > 0.5)[0]\n        if len(rows) > 0:\n            # 归一化到 0-1 (1是顶部还是底部？通常 ECG 向上是正)\n            # 图像坐标 y=0 是顶部。所以 val = (h - y) / h\n            y_avg = np.mean(rows)\n            val = 1.0 - (y_avg / h) \n            # 归一化到 -1 ~ 1 还是 0 ~ 1? 题目好像是 0-1 或者任意电压值\n            # 保持 0-1 比较安全，线性变换不影响相关性\n        else:\n            val = np.nan\n        signal.append(val)\n        \n    signal = np.array(signal)\n    \n    # 去除 NaN (线性插值)\n    nans = np.isnan(signal)\n    if np.any(nans) and not np.all(nans):\n        x = np.arange(len(signal))\n        signal[nans] = np.interp(x[nans], x[~nans], signal[~nans])\n    elif np.all(nans):\n        signal = np.zeros(len(signal))\n        \n    # 重采样\n    if len(signal) != target_len:\n        signal = np.interp(np.linspace(0, len(signal), target_len), np.arange(len(signal)), signal)\n        \n    return signal\n# 3. 准备工作\n# 读取 Sample Submission 确定我们要生成的 ID 和 Leads\nsample_path = '/kaggle/input/physionet-ecg-image-digitization/sample_submission.parquet'\nif not os.path.exists(sample_path):\n    sample_path = '/kaggle/input/physionet-ecg-image-digitization/sample_submission.csv'\nprint(f\"📋 Reading template from: {sample_path}\")\nif sample_path.endswith('.parquet'):\n    df = pd.read_parquet(sample_path)\nelse:\n    df = pd.read_csv(sample_path)\n# 解析需要预测的 Record 和 Lead\ndf['record_id'] = df['id'].apply(lambda x: x.split('_')[0])\ndf['lead'] = df['id'].apply(lambda x: x.split('_')[-1])\ndf['sample_idx'] = df['id'].apply(lambda x: int(x.split('_')[1]))\n# 确定每条信号需要的长度 (通常是 max_index + 1)\nMAX_LEN = df['sample_idx'].max() + 1\nprint(f\"🎯 Target Signal Length: {MAX_LEN}\")\n# 4. 加载模型\nprint(f\"🧠 Loading Model...\")\nmodel = smp.Unet(encoder_name=\"resnet18\", in_channels=3, classes=1, encoder_weights=None).to(DEVICE)\nmodel.load_state_dict(torch.load(MODEL_PATH, map_location=DEVICE))\nmodel.eval()\ninfer_transform = A.Compose([\n    A.Resize(512, 512), \n    A.Normalize(mean=(0.485,), std=(0.229,)), \n    ToTensorV2()\n])\n# 5. 寻找图片文件\nimg_map = {}\nall_imgs = glob(f\"{TEST_IMG_DIR}/**/*.png\", recursive=True) + glob(f\"{TEST_IMG_DIR}/**/*.jpg\", recursive=True)\nfor p in all_imgs:\n    base = os.path.basename(p).split('.')[0]\n    img_map[str(base)] = p\n    if '_' in base: img_map[base.split('_')[0]] = p\n# 6. 主预测循环\nunique_records = df['record_id'].unique()\npredictions_store = {} # { '1053': { 'I': [...], 'II': [...] } }\nprint(f\"🚀 Processing {len(unique_records)} records with Lead Separation...\")\nfor rec_id in tqdm(unique_records):\n    rec_preds = {}\n    \n    # 找到图片\n    img_path = img_map.get(str(rec_id))\n    if img_path:\n        try:\n            # 读取\n            img = cv2.imread(img_path)\n            if img is not None:\n                # A. 切分导联！\n                leads_dict = split_leads(img)\n                \n                # B. 对每个导联分别预测\n                for lead_name, crop_img in leads_dict.items():\n                    # 预处理\n                    crop_rgb = cv2.cvtColor(crop_img, cv2.COLOR_BGR2RGB)\n                    tensor = infer_transform(image=crop_rgb)['image'].unsqueeze(0).to(DEVICE)\n                    \n                    # 预测\n                    with torch.no_grad():\n                        mask_pred = torch.sigmoid(model(tensor)).cpu().numpy()[0, 0]\n                        \n                    # 提取信号\n                    sig = extract_signal_from_mask(mask_pred, target_len=MAX_LEN)\n                    rec_preds[lead_name] = sig\n            else:\n                print(f\"⚠️ Read Fail: {rec_id}\")\n        except Exception as e:\n            print(f\"⚠️ Error {rec_id}: {e}\")\n            \n    predictions_store[str(rec_id)] = rec_preds\n# 7. 填充 Submission (Vectorized Lookup)\n# 定义一个 lookup 函数\ndef get_prediction(row):\n    rec = row['record_id']\n    lead = row['lead']\n    idx = row['sample_idx']\n    \n    # 获取该记录的所有导联预测\n    rec_data = predictions_store.get(rec, {})\n    # 获取特定导联的信号\n    sig = rec_data.get(lead)\n    \n    if sig is None:\n        return 0.0 # 没预测到，填0\n    \n    if idx < len(sig):\n        return sig[idx]\n    return 0.0\nprint(\"💾 Assembling submission file...\")\ntqdm.pandas()\ndf['value'] = df.progress_apply(get_prediction, axis=1)\n# 保存\nsub_final = df[['id', 'value']]\nsub_final.to_csv('submission.csv', index=False)\nprint(\"✅ Submission with Lead Separation generated!\")\nprint(sub_final.head())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-17T06:54:54.653343Z","iopub.execute_input":"2025-12-17T06:54:54.653594Z","iopub.status.idle":"2025-12-17T06:54:56.650474Z","shell.execute_reply.started":"2025-12-17T06:54:54.653569Z","shell.execute_reply":"2025-12-17T06:54:56.649744Z"}},"outputs":[],"execution_count":null}]}