{"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":"gpu","dataSources":[{"sourceId":97984,"databundleVersionId":14096757,"sourceType":"competition"},{"sourceId":728232,"sourceType":"modelInstanceVersion","isSourceIdPinned":true,"modelInstanceId":554508,"modelId":567070}],"dockerImageVersionId":31236,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"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# For example, here's several helpful packages to load\n\nimport numpy as np # linear algebra\nimport pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)\n\n# Input data files are available in the read-only \"../input/\" directory\n# For example, running this (by clicking run or pressing Shift+Enter) will list all files under the input directory\n\nimport os\nfor dirname, _, filenames in os.walk('/kaggle/input'):\n    for filename in filenames:\n        print(os.path.join(dirname, filename))\n\n# You can write up to 20GB to the current directory (/kaggle/working/) that gets preserved as output when you create a version using \"Save & Run All\" \n# You can also write temporary files to /kaggle/temp/, but they won't be saved outside of the current session","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2026-01-21T07:28:49.389743Z","iopub.execute_input":"2026-01-21T07:28:49.390089Z","iopub.status.idle":"2026-01-21T07:28:56.213334Z","shell.execute_reply.started":"2026-01-21T07:28:49.390059Z","shell.execute_reply":"2026-01-21T07:28:56.212610Z"},"collapsed":true,"jupyter":{"outputs_hidden":true}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =========================================================\n# PHYSIONET ECG DIGITIZATION\n# FINAL CORRECTED PIPELINE (Fixes PyTorch Warnings)\n# =========================================================\n\nimport os\nimport cv2\nimport gc\nimport numpy as np\nimport pandas as pd\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\n\n# =============================\n# CONFIGURATION\n# =============================\nWORK = \"/kaggle/input/physionet-ecg-image-digitization\"\nTEST_DIR = f\"{WORK}/test\"\nDEVICE = \"cuda\" if torch.cuda.is_available() else \"cpu\"\n\nLEADS_ORDER = [\"I\", \"II\", \"III\", \"aVR\", \"aVL\", \"aVF\",\n               \"V1\", \"V2\", \"V3\", \"V4\", \"V5\", \"V6\"]\n\n# =============================\n# 1. FFT RESAMPLER (Fixed Warnings)\n# =============================\ndef resample_torch(x, num, dim=-1):\n    \"\"\"\n    Resamples x to length 'num' along dimension 'dim' using FFT.\n    Fixed to use tuple indexing.\n    \"\"\"\n    dim = (x.dim() + dim) if dim < 0 else dim\n    X = torch.fft.fft(x, dim=dim)\n    Nx = X.shape[dim]\n\n    sl = [slice(None)] * X.ndim\n    newshape = list(X.shape)\n    newshape[dim] = num\n    Y = torch.zeros(newshape, dtype=X.dtype, device=X.device)\n\n    N = min(num, Nx)\n    \n    # First half\n    sl[dim] = slice(0, (N + 1) // 2)\n    Y[tuple(sl)] = X[tuple(sl)]\n    \n    # Second half\n    sl[dim] = slice(-(N - 1) // 2, None)\n    Y[tuple(sl)] = X[tuple(sl)]\n\n    # Handling edge cases for even/odd lengths\n    if N % 2 == 0:\n        if N < Nx:\n            sl[dim] = slice(N//2, N//2+1)\n            Y[tuple(sl)] += X[tuple(sl)]\n        elif N < num:\n            sl[dim] = slice(num-N//2, num-N//2+1)\n            Y[tuple(sl)] /= 2\n            temp = Y[tuple(sl)]\n            sl[dim] = slice(N//2, N//2+1)\n            Y[tuple(sl)] = temp\n\n    y = torch.fft.ifft(Y, dim=dim).real * (float(num) / float(Nx))\n    return y\n\n# =============================\n# 2. SOFT-ARGMAX EXTRACTION\n# =============================\nclass SoftArgmaxExtractor(nn.Module):\n    def __init__(self):\n        super().__init__()\n\n    def forward(self, img_crop):\n        # Input: [H, W] tensor\n        x = img_crop.unsqueeze(0).unsqueeze(0) \n        B, C, H, W = x.shape\n        \n        # Softmax along Height (dim=2)\n        # Multiply by temperature (50.0) to sharpen the probability distribution\n        prob = F.softmax(x * 50.0, dim=2) \n        \n        # Weighted Sum coordinate\n        y_coord = torch.arange(H, device=x.device, dtype=x.dtype).view(1, 1, H, 1)\n        y_pixel = (prob * y_coord).sum(dim=2) \n        \n        # Invert Y (image top is 0) and center\n        signal = (H/2.0) - y_pixel\n        return signal.squeeze()\n\nextractor = SoftArgmaxExtractor().to(DEVICE).eval()\n\n# =============================\n# 3. PREPROCESSING\n# =============================\ndef preprocess_to_tensor(img_crop):\n    if len(img_crop.shape) == 3:\n        gray = cv2.cvtColor(img_crop, cv2.COLOR_BGR2GRAY)\n    else:\n        gray = img_crop\n        \n    # Invert so signal is bright\n    gray = cv2.bitwise_not(gray)\n    \n    # Normalize\n    gray = (gray - gray.min()) / (gray.max() - gray.min() + 1e-6)\n    \n    return torch.tensor(gray, dtype=torch.float32).to(DEVICE)\n\n# =============================\n# 4. MAIN PIPELINE\n# =============================\ndef process_lead(img_crop, target_len):\n    t_img = preprocess_to_tensor(img_crop)\n    \n    with torch.no_grad():\n        # Extract raw signal curve\n        sig = extractor(t_img)\n        \n        # Resample using FFT\n        sig_resampled = resample_torch(sig, target_len, dim=0)\n        \n    y = sig_resampled.cpu().numpy()\n    \n    # Normalize Z-score\n    if np.std(y) < 1e-3: return np.zeros(target_len)\n    return (y - np.mean(y)) / (np.std(y) + 1e-6)\n\ndef generate_leads(img, fs, sig_len_long):\n    h, w = img.shape[:2]\n    \n    # Layout Logic\n    rhythm_h = int(h * 0.16)\n    main_h = h - rhythm_h\n    row_h = main_h // 3\n    col_w = w // 4\n    \n    leads_data = {}\n    \n    # Grid Leads\n    grid_map = {\n        \"I\": (0,0), \"II\": (1,0), \"III\": (2,0),\n        \"aVR\": (0,1), \"aVL\": (1,1), \"aVF\": (2,1),\n        \"V1\": (0,2), \"V2\": (1,2), \"V3\": (2,2),\n        \"V4\": (0,3), \"V5\": (1,3), \"V6\": (2,3)\n    }\n    short_len = int(fs * 2.5)\n    \n    for name, (r, c) in grid_map.items():\n        y1, y2 = r*row_h, (r+1)*row_h\n        x1, x2 = c*col_w, (c+1)*col_w\n        leads_data[name] = process_lead(img[y1:y2, x1:x2], short_len)\n\n    # Long Lead (II)\n    leads_data[\"II\"] = process_lead(img[main_h:h, :], sig_len_long)\n    \n    return leads_data\n\n# =============================\n# 5. SUBMISSION\n# =============================\nif __name__ == \"__main__\":\n    if os.path.exists(f\"{WORK}/test.csv\"):\n        df = pd.read_csv(f\"{WORK}/test.csv\")\n        sample = pd.read_parquet(f\"{WORK}/sample_submission.parquet\")[[\"id\"]]\n        results = []\n        \n        for sid, group in df.groupby(\"id\"):\n            img_path = f\"{TEST_DIR}/{sid}.png\"\n            fs = int(group.fs.iloc[0])\n            \n            # Determine length for Long Lead II\n            try:\n                sig_len = int(group[group.lead == \"II\"].number_of_rows.iloc[0])\n            except:\n                sig_len = int(fs * 10)\n            \n            if os.path.exists(img_path):\n                img = cv2.imread(img_path)\n                try:\n                    leads = generate_leads(img, fs, sig_len)\n                    \n                    # Construct DataFrame Rows\n                    rows = []\n                    for lead in LEADS_ORDER:\n                        if lead in leads:\n                            y = leads[lead]\n                            ids = [f\"{sid}_{i}_{lead}\" for i in range(len(y))]\n                            rows.append(pd.DataFrame({\"id\": ids, \"value\": y}))\n                    results.append(pd.concat(rows))\n                    \n                except Exception as e:\n                    print(f\"Err {sid}: {e}\")\n            gc.collect()\n            \n        if results:\n            sub = pd.concat(results).set_index(\"id\").reindex(sample.id).reset_index()\n            sub[\"value\"] = sub[\"value\"].fillna(0)\n            sub.to_csv(\"submission.csv\", index=False)\n            print(\"Submission Saved. Shape:\", sub.shape)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-23T05:56:19.652001Z","iopub.execute_input":"2026-01-23T05:56:19.652646Z","iopub.status.idle":"2026-01-23T05:56:20.347561Z","shell.execute_reply.started":"2026-01-23T05:56:19.652614Z","shell.execute_reply":"2026-01-23T05:56:20.346735Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}