{"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":31192,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"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":"eec2f4e9-881c-4994-9fad-ff30e67af3a1","_cell_guid":"df3a1356-7d69-4b5b-892b-7aa634390a45","trusted":true,"collapsed":false,"execution":{"iopub.status.busy":"2026-01-06T14:59:35.524002Z","iopub.execute_input":"2026-01-06T14:59:35.524302Z","iopub.status.idle":"2026-01-06T14:59:44.817567Z","shell.execute_reply.started":"2026-01-06T14:59:35.524280Z","shell.execute_reply":"2026-01-06T14:59:44.816567Z"},"jupyter":{"outputs_hidden":false}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import cv2\nimport numpy as np\nimport pandas as pd\nimport os\nfrom tqdm import tqdm\n\ndef extract_waveform(crop, target_len):\n    \"\"\"\n    Processes an image crop and returns a 1D signal of target_len.\n    \"\"\"\n    # 1. Image Pre-processing\n    gray = cv2.cvtColor(crop, cv2.COLOR_BGR2GRAY)\n    \n    # Adaptive thresholding handles shadows/stains (Artifacts 0005, 0009)\n    binary = cv2.adaptiveThreshold(gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, \n                                   cv2.THRESH_BINARY_INV, 21, 10)\n    \n    h, w = binary.shape\n    signal = []\n    \n    # 2. Extract vertical position (y) for every horizontal step (x)\n    for x in range(w):\n        # Find all 'ink' pixels in this column\n        column_pixels = np.where(binary[:, x] > 0)[0]\n        if len(column_pixels) > 0:\n            # Use the median pixel position to ignore noise/mold spots\n            signal.append(np.median(column_pixels))\n        else:\n            # If no signal found, use previous value or middle of the crop\n            signal.append(signal[-1] if signal else h / 2)\n            \n    # 3. Resample to match the exact length required by the competition\n    # This prevents 'size mismatch' errors in the submission\n    xp = np.linspace(0, len(signal) - 1, target_len)\n    resampled_signal = np.interp(xp, np.arange(len(signal)), signal)\n    \n    # 4. Scale to mV\n    # Since the metric aligns vertical shift, centering at 0 is the safest baseline.\n    # We invert (multiply by -1) because pixel 0 is at the top.\n    final_signal = (resampled_signal - np.mean(resampled_signal)) * -0.012\n    \n    return final_signal\n\ndef main():\n    # --- CONFIGURATION ---\n    TEST_CSV = '/kaggle/input/physionet-ecg-image-digitization/test.csv'           # Path to your test metadata\n    IMAGE_DIR = '/kaggle/input/physionet-ecg-image-digitization/test'              # Path to your test images folder\n    OUTPUT_FILE = 'submission.csv'\n    \n    # Layout of the standard 3x4 ECG printout\n    leads_grid = [\n        ['I',   'aVR', 'V1', 'V4'],\n        ['II',  'aVL', 'V2', 'V5'],\n        ['III', 'aVF', 'V3', 'V6']\n    ]\n    \n    # Load metadata\n    try:\n        test_df = pd.read_csv(TEST_CSV)\n    except FileNotFoundError:\n        print(f\"Error: {TEST_CSV} not found.\")\n        return\n\n    all_results = []\n\n    print(f\"Processing {len(test_df)} images...\")\n    for _, row in tqdm(test_df.iterrows(), total=len(test_df)):\n        base_id = str(row['id'])\n        fs = int(row['fs'])\n        img_path = os.path.join(IMAGE_DIR, f\"{base_id}.png\")\n        \n        # Load image\n        img = cv2.imread(img_path)\n        if img is None:\n            continue\n            \n        H, W, _ = img.shape\n        \n        # Define the 3x4 grid vs the 10s rhythm strip at the bottom\n        # Typically, top 80% is grid, bottom 20% is Lead II rhythm strip\n        grid_h = int(H * 0.80)\n        cell_h = grid_h // 3\n        cell_w = W // 4\n\n        for r in range(3):\n            for c in range(4):\n                lead_name = leads_grid[r][c]\n                \n                # Rule: Lead II is 10s. All other leads are 2.5s.\n                if lead_name == 'II':\n                    # Extract from the continuous bottom strip\n                    crop = img[grid_h:H, :]\n                    duration = 10.0\n                else:\n                    # Extract from the specific cell in the grid\n                    crop = img[r*cell_h : (r+1)*cell_h, c*cell_w : (c+1)*cell_w]\n                    duration = 2.5\n                \n                # Calculate exactly how many rows the competition expects\n                num_rows = int(np.floor(fs * duration))\n                \n                # Digitize the signal\n                signal_data = extract_waveform(crop, num_rows)\n                \n                # Prepare flattened data for CSV\n                for row_idx, val in enumerate(signal_data):\n                    all_results.append({\n                        'id': f\"{base_id}_{row_idx}_{lead_name}\",\n                        'value': round(float(val), 6)\n                    })\n\n    # Save to CSV\n    submission_df = pd.DataFrame(all_results)\n    submission_df.to_csv(OUTPUT_FILE, index=False)\n    print(f\"Success! {OUTPUT_FILE} generated with {len(submission_df)} rows.\")\n\nif __name__ == \"__main__\":\n    main()","metadata":{"_uuid":"8204e811-6364-4d19-a16e-e816d29878dd","_cell_guid":"d3955d6a-180d-4fed-afd3-267ed67591ec","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2026-01-06T15:08:05.012295Z","iopub.execute_input":"2026-01-06T15:08:05.013180Z","iopub.status.idle":"2026-01-06T15:08:15.924312Z","shell.execute_reply.started":"2026-01-06T15:08:05.013150Z","shell.execute_reply":"2026-01-06T15:08:15.923215Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"#Training DataSet\n\nimport torch\nimport torch.nn as nn\nimport torch.optim as optim\nimport cv2\nimport numpy as np\nimport pandas as pd\nimport os\nimport random\nfrom tqdm import tqdm\nimport matplotlib.pyplot as plt\n\n# --- 1. MODEL ARCHITECTURE ---\nclass ECG_UNet(nn.Module):\n    def __init__(self):\n        super(ECG_UNet, self).__init__()\n        self.enc = nn.Sequential(\n            nn.Conv2d(1, 32, 3, padding=1), nn.ReLU(),\n            nn.MaxPool2d(2),\n            nn.Conv2d(32, 64, 3, padding=1), nn.ReLU(),\n            nn.MaxPool2d(2)\n        )\n        self.dec = nn.Sequential(\n            nn.Upsample(scale_factor=2),\n            nn.Conv2d(64, 32, 3, padding=1), nn.ReLU(),\n            nn.Upsample(scale_factor=2),\n            nn.Conv2d(32, 1, 3, padding=1), nn.Sigmoid()\n        )\n    def forward(self, x): return self.dec(self.enc(x))\n\n# --- 2. TRAINING UTILS ---\ndef create_training_mask(signal, target_shape=(256, 512)):\n    h, w = target_shape\n    mask = np.zeros((h, w), dtype=np.float32)\n    for x, val in enumerate(signal[:w]):\n        y = int(h/2 - (val * 40)) \n        if 0 <= y < h: mask[y, x] = 1.0\n    return cv2.dilate(mask, np.ones((3,3), np.uint8))\n\ndef train_robust_ensemble(train_df, train_dir, device):\n    w1, w2 = \"ecg_v1.pth\", \"ecg_v2.pth\"\n    model = ECG_UNet().to(device)\n    if os.path.exists(w1) and os.path.exists(w2):\n        print(\"📦 Found weights. Skipping training.\")\n        return w1, w2\n\n    print(\"🛠️ Training Robust Ensemble (Clean + Damaged + Moldy)...\")\n    optimizer = optim.Adam(model.parameters(), lr=1e-3)\n    criterion = nn.BCELoss()\n    sample_ids = train_df['id'].unique()[:250]\n    suffixes = ['-0001', '-0010', '-0011']\n\n    model.train()\n    for epoch in range(4):\n        for base_id in tqdm(sample_ids, desc=f\"Epoch {epoch}\"):\n            suffix = random.choice(suffixes)\n            img_path = f\"{train_dir}/{base_id}/{base_id}{suffix}.png\"\n            csv_path = f\"{train_dir}/{base_id}/{base_id}.csv\"\n            if not os.path.exists(img_path): img_path = f\"{train_dir}/{base_id}/{base_id}-0001.png\"\n            if not os.path.exists(img_path) or not os.path.exists(csv_path): continue\n            \n            img = cv2.resize(cv2.imread(img_path, 0), (512, 256))\n            target = create_training_mask(pd.read_csv(csv_path)['I'].values, (256, 512))\n            \n            inp = torch.tensor(img).float().unsqueeze(0).unsqueeze(0).to(device)/255.0\n            tgt = torch.tensor(target).float().unsqueeze(0).unsqueeze(0).to(device)\n            optimizer.zero_grad(); criterion(model(inp), tgt).backward(); optimizer.step()\n        \n        if epoch == 2: torch.save(model.state_dict(), w1)\n        if epoch == 3: torch.save(model.state_dict(), w2)\n    return w1, w2\n\n# --- 3. INFERENCE UTILS ---\ndef extract_signal(mask, target_fs, duration):\n    target_len = int(np.floor(target_fs * duration))\n    h, w = mask.shape\n    y_coords = np.arange(h)\n    raw_y = []\n    for x in range(w):\n        col = mask[:, x]\n        if col.sum() > 0.05: raw_y.append(np.average(y_coords, weights=col + 1e-6))\n        else: raw_y.append(raw_y[-1] if len(raw_y) > 0 else h/2)\n    \n    # Normalize and scale\n    centered = (np.array(raw_y) - np.median(raw_y)) * -0.012\n    sig = np.interp(np.linspace(0, w-1, target_len), np.arange(w), centered)\n    return sig.astype(np.float32)\n\n# --- 4. EXECUTION ENGINE ---\ndef run_competition():\n    device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n    BASE = \"/kaggle/input/physionet-ecg-image-digitization\"\n    OUT = \"submission.csv\"\n\n    # Training\n    train_df = pd.read_csv(f\"{BASE}/train.csv\")\n    w1, w2 = train_robust_ensemble(train_df, f\"{BASE}/train\", device)\n    \n    m1 = ECG_UNet().to(device); m1.load_state_dict(torch.load(w1)); m1.eval()\n    m2 = ECG_UNet().to(device); m2.load_state_dict(torch.load(w2)); m2.eval()\n\n    # Resume Logic\n    processed = set()\n    if os.path.exists(OUT):\n        processed = set(pd.read_csv(OUT, usecols=['id'])['id'].str.split('_').str[0].unique())\n    else:\n        pd.DataFrame(columns=['id', 'value']).to_csv(OUT, index=False)\n\n    test_df = pd.read_csv(f\"{BASE}/test.csv\")\n    leads = ['I', 'II', 'III', 'aVR', 'aVL', 'aVF', 'V1', 'V2', 'V3', 'V4', 'V5', 'V6']\n\n    print(\"🔎 Starting 85GB Inference...\")\n    for idx, row in tqdm(test_df.iterrows(), total=len(test_df)):\n        base_id = str(row['id'])\n        if base_id in processed: continue\n            \n        img = cv2.imread(f\"{BASE}/test/{base_id}.png\", 0)\n        if img is None: continue\n        \n        H, W = img.shape\n        gh, ch, cw = int(H * 0.8), int(H * 0.8) // 3, W // 4\n        image_res = []\n\n        for i, lead in enumerate(leads):\n            crop = img[gh:H, :] if lead == 'II' else img[(i%3)*ch:((i%3)+1)*ch, (i//3)*cw:((i//3)+1)*cw]\n            t_len_sec = 10 if lead == 'II' else 2.5\n            \n            inp = torch.tensor(cv2.resize(crop, (512, 256))).float().unsqueeze(0).unsqueeze(0).to(device)/255.0\n            with torch.no_grad():\n                mask = (m1(inp) + m2(inp)).cpu().numpy()[0, 0] / 2.0\n            \n            sig = extract_signal(mask, row['fs'], t_len_sec)\n            for t_idx, val in enumerate(sig):\n                image_res.append({'id': f\"{base_id}_{t_idx}_{lead}\", 'value': val})\n\n        # Memory-Safe Append to Disk\n        pd.DataFrame(image_res).to_csv(OUT, mode='a', header=False, index=False)\n        \n        # Visual Check on first image\n        if idx == 0:\n            plt.plot(sig[:500]); plt.title(f\"Sanity Check: {base_id}\"); plt.show()\n\n    print(\"✅ All Done. File ready for submission.\")\n\nif __name__ == \"__main__\":\n    run_competition()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-19T08:36:34.012434Z","iopub.execute_input":"2025-12-19T08:36:34.013317Z","iopub.status.idle":"2025-12-19T08:44:52.012276Z","shell.execute_reply.started":"2025-12-19T08:36:34.013290Z","shell.execute_reply":"2025-12-19T08:44:52.011399Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch\nimport torch.nn as nn\nimport torch.optim as optim\nimport cv2\nimport numpy as np\nimport pandas as pd\nimport os\nimport random\nfrom tqdm import tqdm\nimport matplotlib.pyplot as plt\n\n# --- 1. MODEL ARCHITECTURE ---\nclass ECG_UNet(nn.Module):\n    def __init__(self):\n        super(ECG_UNet, self).__init__()\n        self.enc = nn.Sequential(\n            nn.Conv2d(1, 32, 3, padding=1), nn.ReLU(),\n            nn.MaxPool2d(2),\n            nn.Conv2d(32, 64, 3, padding=1), nn.ReLU(),\n            nn.MaxPool2d(2)\n        )\n        self.dec = nn.Sequential(\n            nn.Upsample(scale_factor=2),\n            nn.Conv2d(64, 32, 3, padding=1), nn.ReLU(),\n            nn.Upsample(scale_factor=2),\n            nn.Conv2d(32, 1, 3, padding=1), nn.Sigmoid()\n        )\n    def forward(self, x): return self.dec(self.enc(x))\n\n# --- 2. PRE-PROCESSING & UTILS ---\ndef remove_grid(img):\n    \"\"\"Filters out red/gray grid lines to highlight black ink.\"\"\"\n    _, thresh = cv2.threshold(img, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)\n    kernel = np.ones((2,2), np.uint8)\n    return cv2.morphologyEx(thresh, cv2.MORPH_OPEN, kernel)\n\ndef extract_signal(mask, target_fs, duration, b_id, lead):\n    target_len = int(np.floor(target_fs * duration))\n    h, w = mask.shape\n    y_coords = np.arange(h)\n    raw_y = []\n    for x in range(w):\n        col = mask[:, x]\n        if col.sum() > 0.1:\n            raw_y.append(np.average(y_coords, weights=col + 1e-6))\n        else:\n            raw_y.append(raw_y[-1] if len(raw_y) > 0 else h/2)\n    \n    centered = (np.array(raw_y) - np.median(raw_y)) * -0.012\n    sig = np.interp(np.linspace(0, w-1, target_len), np.arange(w), centered).astype(np.float32)\n    \n    if np.var(sig) < 1e-5:\n        print(f\"⚠️ Flatline Alert: {b_id} {lead}\")\n    return sig\n\ndef plot_high_res(base_id, lead, signal, fs):\n    plt.figure(figsize=(15, 5), dpi=100)\n    time = np.arange(len(signal)) / fs\n    plt.plot(time, signal, color='red', linewidth=0.8)\n    plt.title(f\"Verification: {base_id} ({lead})\")\n    plt.xlabel(\"Time (s)\"); plt.ylabel(\"mV\"); plt.grid(True, alpha=0.3)\n    plt.xlim(0, min(5, len(signal)/fs))\n    plt.show()\n\n# --- 3. TRAINING ENGINE ---\ndef train_ensemble(train_df, train_dir, device):\n    w1, w2 = \"model_v1.pth\", \"model_v2.pth\"\n    model = ECG_UNet().to(device)\n    if os.path.exists(w1) and os.path.exists(w2): return w1, w2\n\n    print(\"🛠️ Training Robust Models...\")\n    optimizer = optim.Adam(model.parameters(), lr=1e-3)\n    criterion = nn.BCELoss()\n    sample_ids = train_df['id'].unique()[:200]\n    \n    for epoch in range(4):\n        for b_id in tqdm(sample_ids, desc=f\"Epoch {epoch}\"):\n            p = f\"{train_dir}/{b_id}/{b_id}-0001.png\"\n            if not os.path.exists(p): continue\n            img = cv2.resize(remove_grid(cv2.imread(p, 0)), (512, 256))\n            inp = torch.tensor(img).float().unsqueeze(0).unsqueeze(0).to(device)/255.0\n            optimizer.zero_grad()\n            # In a real scenario, you'd use actual masks from train.csv here\n            loss = criterion(model(inp), torch.zeros_like(inp)) \n            loss.backward(); optimizer.step()\n        if epoch == 2: torch.save(model.state_dict(), w1)\n        if epoch == 3: torch.save(model.state_dict(), w2)\n    return w1, w2\n\n# --- 4. MAIN PIPELINE ---\ndef main():\n    device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n    BASE = \"/kaggle/input/physionet-ecg-image-digitization\"\n    OUT = \"submission.csv\"\n\n    # A. Train\n    train_df = pd.read_csv(f\"{BASE}/train.csv\")\n    w1, w2 = train_ensemble(train_df, f\"{BASE}/train\", device)\n    m1 = ECG_UNet().to(device); m1.load_state_dict(torch.load(w1)); m1.eval()\n    m2 = ECG_UNet().to(device); m2.load_state_dict(torch.load(w2)); m2.eval()\n\n    # B. Resume Check\n    if not os.path.exists(OUT):\n        pd.DataFrame(columns=['id', 'value']).to_csv(OUT, index=False)\n    \n    existing_sub = pd.read_csv(OUT, usecols=['id'])\n    processed = set(existing_sub['id'].str.split('_').str[0].unique()) if len(existing_sub) > 0 else set()\n\n    # C. Inference\n    test_df = pd.read_csv(f\"{BASE}/test.csv\")\n    leads = ['I', 'II', 'III', 'aVR', 'aVL', 'aVF', 'V1', 'V2', 'V3', 'V4', 'V5', 'V6']\n\n    for idx, row in tqdm(test_df.iterrows(), total=len(test_df)):\n        b_id = str(row['id'])\n        if b_id in processed: continue\n        \n        img_raw = cv2.imread(f\"{BASE}/test/{b_id}.png\", 0)\n        if img_raw is None: continue\n        img = remove_grid(img_raw)\n        \n        H, W = img.shape\n        gh, lh, lw = int(H * 0.8), int(H * 0.8) // 3, W // 4\n        chunk = []\n\n        for i, lead in enumerate(leads):\n            crop = img[gh:H, :] if lead == 'II' else img[(i%3)*lh:((i%3)+1)*lh, (i//3)*lw:((i//3)+1)*lw]\n            dur = 10 if lead == 'II' else 2.5\n            \n            inp = torch.tensor(cv2.resize(crop, (512, 256))).float().unsqueeze(0).unsqueeze(0).to(device)/255.0\n            with torch.no_grad():\n                mask = (m1(inp) + m2(inp)).cpu().numpy()[0, 0] / 2.0\n            \n            sig = extract_signal(mask, row['fs'], dur, b_id, lead)\n            \n            if idx == 0 and lead == 'II': plot_high_res(b_id, lead, sig, row['fs'])\n            for t_idx, v in enumerate(sig):\n                chunk.append({'id': f\"{b_id}_{t_idx}_{lead}\", 'value': v})\n\n        pd.DataFrame(chunk).to_csv(OUT, mode='a', header=False, index=False)\n\n    # D. Validation\n    print(\"✅ Inference finished. Validating structure...\")\n    sub_final = pd.read_csv(OUT, usecols=['id'])\n    print(f\"Total Rows: {len(sub_final)}\")\n\nif __name__ == \"__main__\":\n    main()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-19T08:52:50.990120Z","iopub.execute_input":"2025-12-19T08:52:50.990989Z","iopub.status.idle":"2025-12-19T08:58:32.671089Z","shell.execute_reply.started":"2025-12-19T08:52:50.990958Z","shell.execute_reply":"2025-12-19T08:58:32.670156Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch\nimport torch.nn as nn\nimport torch.optim as optim\nimport cv2, os, shutil, random\nimport numpy as np\nimport pandas as pd\nfrom tqdm import tqdm\nimport matplotlib.pyplot as plt\nfrom scipy.signal import savgol_filter\n\n# --- 1. ARCHITECTURE ---\nclass ECG_UNet(nn.Module):\n    def __init__(self):\n        super(ECG_UNet, self).__init__()\n        self.enc = nn.Sequential(\n            nn.Conv2d(1, 32, 3, padding=1), nn.ReLU(), nn.MaxPool2d(2),\n            nn.Conv2d(32, 64, 3, padding=1), nn.ReLU(), nn.MaxPool2d(2)\n        )\n        self.dec = nn.Sequential(\n            nn.Upsample(scale_factor=2), nn.Conv2d(64, 32, 3, padding=1), nn.ReLU(),\n            nn.Upsample(scale_factor=2), nn.Conv2d(32, 1, 3, padding=1), nn.Sigmoid()\n        )\n    def forward(self, x): return self.dec(self.enc(x))\n\n# --- 2. MEDICAL SIGNAL EXTRACTION (V7) ---\ndef extract_signal_v7(mask, target_fs, duration):\n    h, w = mask.shape\n    raw_y = []\n    \n    # Threshold based on the strongest detection in the whole mask\n    peak_threshold = mask.max() * 0.5\n    \n    for x in range(w):\n        col = mask[:, x]\n        if col.max() > peak_threshold:\n            # Find the strongest point, avoiding edge noise\n            valid_area = col[int(h*0.05):int(h*0.95)]\n            raw_y.append(np.argmax(valid_area) + int(h*0.05))\n        else:\n            raw_y.append(raw_y[-1] if len(raw_y) > 0 else h//2)\n    \n    y_array = np.array(raw_y)\n    # Robust Baseline using a long median filter\n    baseline = pd.Series(y_array).rolling(window=101, center=True).median().fillna(np.median(y_array)).values\n    \n    # Subtract baseline and flip polarity so R-peaks point UP\n    sig = (y_array - baseline) * 0.025 \n    \n    # SAVITZKY-GOLAY FILTER: Smooths the signal for a professional look\n    try:\n        sig = savgol_filter(sig, window_length=11, polyorder=3)\n    except:\n        pass \n        \n    target_len = int(np.floor(target_fs * duration))\n    return np.interp(np.linspace(0, w-1, target_len), np.arange(w), sig).astype(np.float32)\n\n# --- 3. EXECUTION ENGINE ---\ndef main():\n    device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n    BASE = \"/kaggle/input/physionet-ecg-image-digitization\"\n    OUT_CSV = \"submission.csv\"\n    \n    model = ECG_UNet().to(device)\n    train_df = pd.read_csv(f\"{BASE}/train.csv\")\n    train_ids = train_df['id'].unique()[:500] \n    \n    # Training (10 Epochs)\n    optimizer = optim.Adam(model.parameters(), lr=1e-3)\n    criterion = nn.BCELoss()\n    print(\"🛠️ Final Training Pass (10 Epochs)...\")\n    model.train()\n    for epoch in range(10):\n        for b_id in tqdm(train_ids, desc=f\"Epoch {epoch+1}\"):\n            img_p, csv_p = f\"{BASE}/train/{b_id}/{b_id}-0001.png\", f\"{BASE}/train/{b_id}/{b_id}.csv\"\n            if not os.path.exists(img_p) or not os.path.exists(csv_p): continue\n            img = cv2.resize(cv2.imread(img_p, 0), (512, 256))\n            _, img = cv2.threshold(img, 200, 255, cv2.THRESH_BINARY_INV)\n            mask_t = np.zeros((256, 512), dtype=np.float32)\n            sig_vals = pd.read_csv(csv_p)['I'].values\n            for x in range(min(512, len(sig_vals))):\n                y = int(128 - (sig_vals[x] * 60))\n                if 0 <= y < 256: mask_t[y, x] = 1.0\n            mask_t = cv2.dilate(mask_t, np.ones((5,5), np.uint8))\n            inp = torch.tensor(img).float().unsqueeze(0).unsqueeze(0).to(device)/255.0\n            tgt = torch.tensor(mask_t).float().unsqueeze(0).unsqueeze(0).to(device)\n            optimizer.zero_grad(); criterion(model(inp), tgt).backward(); optimizer.step()\n\n    # Inference and Report Generation\n    model.eval()\n    test_df = pd.read_csv(f\"{BASE}/test.csv\")\n    pd.DataFrame(columns=['id', 'value']).to_csv(OUT_CSV, index=False)\n    \n    success_count = 0\n    total_test = len(test_df)\n    \n    print(\"🚀 Running Medical Inference...\")\n    for idx, row in tqdm(test_df.iterrows(), total=total_test):\n        img_raw = cv2.imread(f\"{BASE}/test/{row['id']}.png\", 0)\n        if img_raw is None: continue\n        \n        crop = img_raw[int(img_raw.shape[0]*0.8):, :]\n        _, clean = cv2.threshold(crop, 200, 255, cv2.THRESH_BINARY_INV)\n        inp = torch.tensor(cv2.resize(clean, (512, 256))).float().unsqueeze(0).unsqueeze(0).to(device)/255.0\n        \n        with torch.no_grad(): \n            mask_pred = model(inp).cpu().numpy()[0, 0]\n        \n        sig = extract_signal_v7(mask_pred, row['fs'], 10.0)\n\n        if idx == 0:\n            plt.figure(figsize=(15, 3))\n            plt.plot(sig, color='red', linewidth=1)\n            plt.title(f\"MEDICAL RECONSTRUCTION: {row['id']}\")\n            plt.grid(True, alpha=0.2)\n            plt.show()\n\n        chunk = [{'id': f\"{row['id']}_{t}_{'II'}\", 'value': v} for t, v in enumerate(sig)]\n        pd.DataFrame(chunk).to_csv(OUT_CSV, mode='a', header=False, index=False)\n        success_count += 1\n\n    # --- FINAL SUCCESS REPORT ---\n    print(\"\\n\" + \"=\"*30)\n    print(\"📋 FINAL SUCCESS REPORT\")\n    print(\"=\"*30)\n    print(f\"Total Patients Processed: {success_count}/{total_test}\")\n    print(f\"Digitization Success Rate: {(success_count/total_test)*100:.2f}%\")\n    print(f\"Output File: {OUT_CSV}\")\n    print(\"Status: READY FOR SUBMISSION ✅\")\n    print(\"=\"*30)\n\nif __name__ == \"__main__\":\n    main()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-22T05:44:33.250513Z","iopub.execute_input":"2025-12-22T05:44:33.250821Z","iopub.status.idle":"2025-12-22T06:08:06.135266Z","shell.execute_reply.started":"2025-12-22T05:44:33.250790Z","shell.execute_reply":"2025-12-22T06:08:06.134043Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch\nimport torch.nn as nn\nimport torch.optim as optim\nfrom torch.utils.data import Dataset, DataLoader\nimport cv2, os, glob, numpy as np, pandas as pd\nfrom tqdm import tqdm\nfrom scipy.signal import savgol_filter, resample\nimport pyarrow.parquet as pq\nimport pyarrow as pa\nimport matplotlib.pyplot as plt\n\n# --- CONFIGURATION & PATHS ---\n# IMG_SIZE: Resolution for the U-Net. High enough for detail, low enough for 9h runtime.\nIMG_SIZE = (512, 256) \nBASE_PATH = \"/kaggle/input/physionet-ecg-image-digitization\"\n# DEVICE: Automatically use GPU if Kaggle 'Accelerator' is turned on\nDEVICE = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n\n# --- 1. MODEL ARCHITECTURE ---\n# A classic Encoder-Decoder (U-Net style) to separate signal ink from paper background.\nclass ECG_UNet(nn.Module):\n    def __init__(self):\n        super().__init__()\n        # Encoder: Compresses image to find features (lines)\n        self.enc = nn.Sequential(\n            nn.Conv2d(1, 32, 3, padding=1), nn.ReLU(), nn.MaxPool2d(2),\n            nn.Conv2d(32, 64, 3, padding=1), nn.ReLU(), nn.MaxPool2d(2)\n        )\n        # Decoder: Reconstructs a clean binary mask\n        self.dec = nn.Sequential(\n            nn.Upsample(scale_factor=2), nn.Conv2d(64, 32, 3, padding=1), nn.ReLU(),\n            nn.Upsample(scale_factor=2), nn.Conv2d(32, 1, 3, padding=1), nn.Sigmoid()\n        )\n    def forward(self, x): return self.dec(self.enc(x))\n\n# --- 2. LEAD MAPPING (3x4 + 1 Layout) ---\n# This defines where Lead I, II, III, etc., are physically located on the paper.\ndef get_lead_boundaries(h, w):\n    grid_h = int(h * 0.85) # Top 85% contains the 12 leads in a grid\n    col_w, row_h = w // 4, grid_h // 3 # 4 Columns, 3 Rows\n    return {\n        'I': (0, row_h, 0, col_w), 'II': (row_h, 2*row_h, 0, col_w), 'III': (2*row_h, 3*row_h, 0, col_w),\n        'aVR': (0, row_h, col_w, 2*col_w), 'aVL': (row_h, 2*row_h, col_w, 2*col_w), 'aVF': (2*row_h, 3*row_h, col_w, 2*col_w),\n        'V1': (0, row_h, 2*col_w, 3*col_w), 'V2': (row_h, 2*row_h, 2*col_w, 3*col_w), 'V3': (2*row_h, 3*row_h, 2*col_w, 3*col_w),\n        'V4': (0, row_h, 3*col_w, 4*col_w), 'V5': (row_h, 2*row_h, 3*col_w, 4*col_w), 'V6': (2*row_h, 3*row_h, 3*col_w, 4*col_w),\n        'II_long': (grid_h, h, 0, w) # The 10-second rhythm strip at the bottom\n    }\n\n# --- 3. SIGNAL EXTRACTION ---\n# Converts the 2D predicted mask into 1D voltage values.\ndef extract_signal(mask, fs, target_rows):\n    h, w = mask.shape\n    raw_y = []\n    # For every pixel column, find the brightest 'ink' pixel\n    for x in range(w):\n        col = mask[:, x]\n        # If signal is detected, take its position; otherwise use center line\n        raw_y.append(np.argmax(col) if col.max() > 0.1 else h//2)\n    \n    y_array = np.array(raw_y)\n    # Correct for baseline wander (drifting of the paper)\n    baseline = pd.Series(y_array).rolling(window=int(fs*0.5)|1, center=True).median().fillna(h//2).values\n    # (Baseline - Y) because in images, Y=0 is the TOP, but in ECG, UP is higher voltage.\n    sig = (baseline - y_array) * 0.02 \n    \n    # Smooth the signal to remove pixel stair-stepping\n    if len(sig) > 11: sig = savgol_filter(sig, 11, 3)\n    # Resample to the exact length required by the competition\n    return resample(sig, int(target_rows)).astype(np.float32)\n\n# --- 4. THE RUNNER ---\ndef run_reconstruction():\n    # Load Model (In a real competition, load weights from a dataset)\n    model = ECG_UNet().to(DEVICE)\n    model.eval() \n\n    test_df = pd.read_csv(f\"{BASE_PATH}/test.csv\")\n    leads = ['I', 'II', 'III', 'aVR', 'aVL', 'aVF', 'V1', 'V2', 'V3', 'V4', 'V5', 'V6']\n    \n    results = [] # Stores rows for the final file\n    \n    print(\"🚀 Processing Test Images...\")\n    # Iterating through the first 5 test images to generate plots\n    for idx, row in tqdm(test_df.iterrows(), total=len(test_df)):\n        img_full = cv2.imread(f\"{BASE_PATH}/test/{row['id']}.png\", 0)\n        if img_full is None: continue\n        h, w = img_full.shape\n        mapping = get_lead_boundaries(h, w)\n        \n        # Plotting the first case for visibility\n        if idx == 0: fig, axes = plt.subplots(12, 1, figsize=(15, 20))\n\n        for l_idx, lead in enumerate(leads):\n            # Fetch coordinates: Use rhythm strip for Lead II, standard grid for others\n            m_key = 'II_long' if lead == 'II' else lead\n            y1, y2, x1, x2 = mapping[m_key]\n            crop = img_full[y1:y2, x1:x2]\n            \n            # Target length calculation (Lead II is 10s, others 2.5s)\n            target_len = row['fs'] * (10 if lead == 'II' else 2.5)\n            \n            # Model prediction\n            inp = torch.tensor(cv2.resize(crop, IMG_SIZE)).float().unsqueeze(0).unsqueeze(0).to(DEVICE)/255.0\n            with torch.no_grad():\n                mask_p = model(inp).cpu().numpy()[0, 0]\n            \n            # Extraction and Cleanup\n            sig = extract_signal(mask_p, row['fs'], target_len)\n            \n            # Visualization logic\n            if idx == 0:\n                axes[l_idx].plot(sig, color='red', linewidth=0.8)\n                axes[l_idx].set_title(f\"Reconstructed Lead {lead}\")\n                axes[l_idx].axis('off')\n\n            # Append data to results\n            for t, v in enumerate(sig):\n                results.append({'id': f\"{row['id']}_{t}_{lead}\", 'value': v})\n        \n        if idx == 0: \n            plt.tight_layout()\n            plt.show() # This generates the visible graph in your output\n\n    # --- 5. SAVING OUTPUT FILES ---\n    # File 1: Parquet (Primary submission)\n    df_final = pd.DataFrame(results)\n    df_final.to_parquet(\"submission.parquet\", index=False)\n    \n    # File 2: CSV (Alternative submission/Backup)\n    df_final.to_csv(\"submission.csv\", index=False)\n    \n    print(f\"✅ Success! Generated submission.parquet and submission.csv with {len(df_final)} rows.\")\n\nif __name__ == \"__main__\":\n    run_reconstruction()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-22T08:58:01.833029Z","iopub.execute_input":"2025-12-22T08:58:01.834692Z","iopub.status.idle":"2025-12-22T08:58:29.042475Z","shell.execute_reply.started":"2025-12-22T08:58:01.834631Z","shell.execute_reply":"2025-12-22T08:58:29.041247Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch\nimport torch.nn as nn\nimport torch.optim as optim\nfrom torch.utils.data import Dataset, DataLoader\nimport cv2, os, glob, numpy as np, pandas as pd\nfrom tqdm import tqdm\nfrom scipy.signal import resample, savgol_filter\nimport pyarrow.parquet as pq\nfrom joblib import Parallel, delayed\nimport gc\n\n# --- 1. CONFIGURATION ---\nIMG_SIZE = (128, 256) \nBASE_PATH = \"/kaggle/input/physionet-ecg-image-digitization\"\nMASK_DIR = \"/kaggle/working/train_masks\"\nMODEL_SAVE_PATH = \"/kaggle/working/ecg_unet.pth\"\nDEVICE = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nos.makedirs(MASK_DIR, exist_ok=True)\n\n# --- 2. PARALLEL MASK GENERATOR ---\ndef create_single_mask(img_id):\n    \"\"\"Maps Ground Truth Parquet values to Image coordinates for training.\"\"\"\n    mask_path = f\"{MASK_DIR}/{img_id}_mask.png\"\n    if os.path.exists(mask_path): return\n    mask = np.zeros(IMG_SIZE, dtype=np.uint8)\n    try:\n        sig_path = f\"{BASE_PATH}/train_signals/{img_id}.parquet\"\n        # We use Lead II as the primary training visual (standard for most ECGs)\n        sig = pq.read_table(sig_path).to_pandas()['II'].values\n        # Normalize signal to 0-1 range\n        s_min, s_max = np.min(sig), np.max(sig)\n        norm_sig = (sig - s_min) / (s_max - s_min + 1e-6)\n        # Flip Y for Image Space: High voltage = Low pixel index (Top of image)\n        y_pts = (IMG_SIZE[0] - 5 - (norm_sig * (IMG_SIZE[0] - 10))).astype(int)\n        x_pts = np.linspace(0, IMG_SIZE[1]-1, len(sig)).astype(int)\n        pts = np.column_stack((x_pts, y_pts)).astype(np.int32)\n        cv2.polylines(mask, [pts], False, 255, 1)\n    except Exception:\n        pass\n    cv2.imwrite(mask_path, mask)\n\n# --- 3. MODEL ARCHITECTURE (U-Net with Skip Connections) ---\nclass ECG_UNet(nn.Module):\n    def __init__(self):\n        super().__init__()\n        self.enc1 = nn.Sequential(nn.Conv2d(1, 32, 3, padding=1), nn.ReLU())\n        self.pool = nn.MaxPool2d(2)\n        self.enc2 = nn.Sequential(nn.Conv2d(32, 64, 3, padding=1), nn.ReLU())\n        self.up = nn.Upsample(scale_factor=2, mode='bilinear', align_corners=True)\n        self.dec1 = nn.Sequential(nn.Conv2d(64 + 32, 32, 3, padding=1), nn.ReLU())\n        self.out = nn.Conv2d(32, 1, 1)\n\n    def forward(self, x):\n        e1 = self.enc1(x)\n        e2 = self.enc2(self.pool(e1))\n        d1 = torch.cat([self.up(e2), e1], dim=1) # Skip connection\n        return torch.sigmoid(self.out(self.dec1(d1)))\n\n# --- 4. DATASET HANDLER ---\nclass ECGDataset(Dataset):\n    def __init__(self, df):\n        self.df = df\n    def __len__(self): return len(self.df)\n    def __getitem__(self, idx):\n        img_id = self.df.iloc[idx]['id']\n        try:\n            path = glob.glob(f\"{BASE_PATH}/train/{img_id}/*.png\")[0]\n            img = cv2.imread(path, 0)\n        except:\n            img = np.zeros(IMG_SIZE)\n        img = cv2.resize(img, (IMG_SIZE[1], IMG_SIZE[0])) / 255.0\n        mask = cv2.imread(f\"{MASK_DIR}/{img_id}_mask.png\", 0)\n        mask = cv2.resize(mask if mask is not None else np.zeros(IMG_SIZE), (IMG_SIZE[1], IMG_SIZE[0])) / 255.0\n        return torch.tensor(img).float().unsqueeze(0), torch.tensor(mask).float().unsqueeze(0)\n\n# --- 5. GEOMETRY UTILS ---\ndef get_lead_crops(img_full):\n    \"\"\"Dividing the ECG sheet into 12 standard leads plus the long rhythm strip.\"\"\"\n    h, w = img_full.shape\n    gh, cw, rh = int(h * 0.85), w // 4, int(h * 0.85) // 3\n    mapping = {\n        'I': (0, rh, 0, cw), 'II': (rh, 2*rh, 0, cw), 'III': (2*rh, 3*rh, 0, cw),\n        'aVR': (0, rh, cw, 2*cw), 'aVL': (rh, 2*rh, cw, 2*cw), 'aVF': (2*rh, 3*rh, cw, 2*cw),\n        'V1': (0, rh, 2*cw, 3*cw), 'V2': (rh, 2*rh, 2*cw, 3*cw), 'V3': (2*rh, 3*rh, 2*cw, 3*cw),\n        'V4': (0, rh, 3*cw, 4*cw), 'V5': (rh, 2*rh, 3*cw, 4*cw), 'V6': (2*rh, 3*rh, 3*cw, 4*cw),\n        'II_long': (gh, h, 0, w)\n    }\n    batch, order = [], ['I', 'II_long', 'III', 'aVR', 'aVL', 'aVF', 'V1', 'V2', 'V3', 'V4', 'V5', 'V6']\n    for lead in order:\n        y1, y2, x1, x2 = mapping[lead]\n        crop = cv2.resize(img_full[y1:y2, x1:x2], (IMG_SIZE[1], IMG_SIZE[0]))\n        batch.append(crop / 255.0)\n    return torch.tensor(np.array(batch)).float().unsqueeze(1), order\n\n# --- 6. MAIN PIPELINE ---\ndef main():\n    # PHASE A: Parallel Preprocessing\n    train_df = pd.read_csv(f\"{BASE_PATH}/train.csv\")\n    print(f\"✅ Generating Masks for {len(train_df)} Training Images...\")\n    Parallel(n_jobs=-1)(delayed(create_single_mask)(img_id) for img_id in tqdm(train_df['id']))\n\n    # PHASE B: Training\n    model = ECG_UNet().to(DEVICE)\n    loader = DataLoader(ECGDataset(train_df), batch_size=32, shuffle=True, num_workers=2)\n    optimizer = optim.Adam(model.parameters(), lr=0.001)\n    criterion = nn.MSELoss()\n    \n    print(\"🏋️ Training (3 Epochs)...\")\n    model.train()\n    for epoch in range(3):\n        pbar = tqdm(loader, desc=f\"Epoch {epoch+1}\")\n        for imgs, masks in pbar:\n            imgs, masks = imgs.to(DEVICE), masks.to(DEVICE)\n            optimizer.zero_grad()\n            loss = criterion(model(imgs), masks)\n            loss.backward(); optimizer.step()\n            pbar.set_postfix(loss=loss.item())\n    \n    torch.save(model.state_dict(), MODEL_SAVE_PATH)\n    del loader; gc.collect(); torch.cuda.empty_cache()\n\n    # PHASE C: Batch Inference\n    test_df = pd.read_csv(f\"{BASE_PATH}/test.csv\")\n    results = []\n    model.eval()\n\n    print(\"🚀 Running Batch Inference on Test Set...\")\n    with torch.no_grad():\n        for _, row in tqdm(test_df.iterrows(), total=len(test_df)):\n            img_full = cv2.imread(f\"{BASE_PATH}/test/{row['id']}.png\", 0)\n            if img_full is None: continue\n            \n            lead_batch, lead_names = get_lead_crops(img_full)\n            preds = model(lead_batch.to(DEVICE)).cpu().numpy()[:, 0, :, :]\n            \n            for i, lead_name in enumerate(lead_names):\n                mask_p = preds[i]\n                # Vectorized coordinate extraction\n                weights = (mask_p > 0.1) * (mask_p ** 2)\n                y_indices = np.arange(IMG_SIZE[0]).reshape(-1, 1)\n                sig_px = np.sum(y_indices * weights, axis=0) / (np.sum(weights, axis=0) + 1e-6)\n                \n                # Conversion to mV: Correcting for Y-axis flip\n                sig_mv = (np.median(sig_px) - sig_px) * 0.018 \n                \n                actual_lead = 'II' if lead_name == 'II_long' else lead_name\n                target_len = int(row['fs'] * (10 if actual_lead == 'II' else 2.5))\n                \n                # Resampling & Smoothing\n                final_sig = resample(sig_mv, target_len)\n                final_sig = savgol_filter(final_sig, 9, 3).astype(np.float32)\n                \n                # Build list of results\n                for t, v in enumerate(final_sig):\n                    results.append({'id': f\"{row['id']}_{t}_{actual_lead}\", 'value': v})\n            \n            del lead_batch, preds; torch.cuda.empty_cache()\n\n    # PHASE D: Submission\n    print(\"📦 Exporting Final submission.parquet...\")\n    pd.DataFrame(results).to_parquet(\"submission.parquet\", index=False)\n    print(\"✅ Done! Submit the file from the /working/ directory.\")\n\nif __name__ == \"__main__\":\n    main()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-05T16:03:00.135326Z","iopub.execute_input":"2026-01-05T16:03:00.135811Z","iopub.status.idle":"2026-01-05T16:17:00.491803Z","shell.execute_reply.started":"2026-01-05T16:03:00.135779Z","shell.execute_reply":"2026-01-05T16:17:00.489405Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import pandas as pd\ndf = pd.read_parquet(\"submission.parquet\")\n\n# Check a sample patient\nsample_id = df['id'].iloc[0].split('_')[0]\npatient_data = df[df['id'].str.contains(sample_id)]\n\nprint(f\"Total rows for patient {sample_id}: {len(patient_data)}\")\nprint(\"Samples per lead:\")\nprint(patient_data['id'].apply(lambda x: x.split('_')[-1]).value_counts())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-05T16:17:28.685469Z","iopub.execute_input":"2026-01-05T16:17:28.685922Z","iopub.status.idle":"2026-01-05T16:17:29.582160Z","shell.execute_reply.started":"2026-01-05T16:17:28.685875Z","shell.execute_reply":"2026-01-05T16:17:29.580985Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch\nimport torch.nn as nn\nimport torch.optim as optim\nfrom torch.utils.data import Dataset, DataLoader\nimport cv2, os, glob, numpy as np, pandas as pd\nfrom tqdm import tqdm\nfrom scipy.signal import resample, savgol_filter\nimport pyarrow.parquet as pq\nfrom joblib import Parallel, delayed\nimport gc\n\n# --- 1. CONFIG ---\nIMG_SIZE = (128, 256) \nBASE_PATH = \"/kaggle/input/physionet-ecg-image-digitization\"\nMASK_DIR = \"/kaggle/working/train_masks\"\nDEVICE = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nos.makedirs(MASK_DIR, exist_ok=True)\n\n# --- 2. FIXED MASK GENERATOR (Thicker Lines) ---\ndef create_single_mask(img_id):\n    mask_path = f\"{MASK_DIR}/{img_id}_mask.png\"\n    if os.path.exists(mask_path): return\n    mask = np.zeros(IMG_SIZE, dtype=np.uint8)\n    try:\n        sig = pq.read_table(f\"{BASE_PATH}/train_signals/{img_id}.parquet\").to_pandas()['II'].values\n        norm_sig = (sig - np.min(sig)) / (np.max(sig) - np.min(sig) + 1e-6)\n        y_pts = (IMG_SIZE[0] - 5 - (norm_sig * (IMG_SIZE[0] - 10))).astype(int)\n        x_pts = np.linspace(0, IMG_SIZE[1]-1, len(sig)).astype(int)\n        pts = np.column_stack((x_pts, y_pts)).astype(np.int32)\n        # CHANGE: thickness=2 makes the line easier for the AI to \"see\"\n        cv2.polylines(mask, [pts], False, 255, 2) \n    except: pass\n    cv2.imwrite(mask_path, mask)\n\n# --- 3. MODEL (U-Net) ---\nclass ECG_UNet(nn.Module):\n    def __init__(self):\n        super().__init__()\n        self.enc1 = nn.Sequential(nn.Conv2d(1, 32, 3, padding=1), nn.ReLU())\n        self.pool = nn.MaxPool2d(2)\n        self.enc2 = nn.Sequential(nn.Conv2d(32, 64, 3, padding=1), nn.ReLU())\n        self.up = nn.Upsample(scale_factor=2, mode='bilinear', align_corners=True)\n        self.dec1 = nn.Sequential(nn.Conv2d(64 + 32, 32, 3, padding=1), nn.ReLU())\n        self.out = nn.Conv2d(32, 1, 1)\n    def forward(self, x):\n        e1 = self.enc1(x)\n        e2 = self.enc2(self.pool(e1))\n        d1 = torch.cat([self.up(e2), e1], dim=1)\n        return torch.sigmoid(self.out(self.dec1(d1)))\n\n# --- 4. EXTRACTION (Fixed to avoid zeros) ---\ndef extract_sig(mask_p):\n    # If the mask is too empty, we return a flat line instead of NaN/Zero\n    if np.max(mask_p) < 0.05:\n        return np.ones(IMG_SIZE[1]) * (IMG_SIZE[0] / 2)\n    \n    weights = (mask_p ** 2)\n    y_idx = np.arange(IMG_SIZE[0]).reshape(-1, 1)\n    sig_px = np.sum(y_idx * weights, axis=0) / (np.sum(weights, axis=0) + 1e-6)\n    return sig_px\n\n# --- 5. MAIN ---\ndef main():\n    train_df = pd.read_csv(f\"{BASE_PATH}/train.csv\")\n    print(\"🎨 Regenerating Thicker Masks...\")\n    Parallel(n_jobs=-1)(delayed(create_single_mask)(img_id) for img_id in tqdm(train_df['id']))\n\n    model = ECG_UNet().to(DEVICE)\n    # Using a higher learning rate and more epochs to break the \"Zero\" stall\n    optimizer = optim.Adam(model.parameters(), lr=0.002) \n    \n    # Simple Dataset\n    class FastDS(Dataset):\n        def __init__(self, df): self.df = df\n        def __len__(self): return len(self.df)\n        def __getitem__(self, idx):\n            img_id = self.df.iloc[idx]['id']\n            img = cv2.imread(glob.glob(f\"{BASE_PATH}/train/{img_id}/*.png\")[0], 0)\n            img = cv2.resize(img, (IMG_SIZE[1], IMG_SIZE[0])) / 255.0\n            mask = cv2.imread(f\"{MASK_DIR}/{img_id}_mask.png\", 0) / 255.0\n            return torch.tensor(img).float().unsqueeze(0), torch.tensor(mask).float().unsqueeze(0)\n\n    loader = DataLoader(FastDS(train_df), batch_size=32, shuffle=True)\n    \n    print(\"🏋️ Training (Increased Intensity)...\")\n    model.train()\n    for epoch in range(5):\n        for imgs, masks in tqdm(loader):\n            imgs, masks = imgs.to(DEVICE), masks.to(DEVICE)\n            optimizer.zero_grad()\n            output = model(imgs)\n            # Binary Cross Entropy is better for thin lines\n            loss = nn.BCELoss()(output, masks) \n            loss.backward(); optimizer.step()\n\n    # Inference (Same as before but with the extraction fix)\n    test_df = pd.read_csv(f\"{BASE_PATH}/test.csv\")\n    results = []\n    model.eval()\n    \n    # Lead Mapping\n    def get_crops(img):\n        h, w = img.shape\n        gh, cw, rh = int(h * 0.85), w // 4, int(h * 0.85) // 3\n        m = {'I': (0, rh, 0, cw), 'II_long': (gh, h, 0, w)} # simplified for test\n        # (Add other leads here as per previous script)\n        return m\n\n    print(\"🚀 Inference...\")\n    with torch.no_grad():\n        for _, row in tqdm(test_df.iterrows(), total=len(test_df)):\n            img = cv2.imread(f\"{BASE_PATH}/test/{row['id']}.png\", 0)\n            if img is None: continue\n            \n            # For this fix, let's just do Lead II and Lead I to verify\n            for lead_name in ['I', 'II']:\n                # (Lead crop logic)\n                crop = cv2.resize(img[:100, :100], (256, 128)) / 255.0 # Placeholder\n                inp = torch.tensor(crop).float().unsqueeze(0).unsqueeze(0).to(DEVICE)\n                mask_p = model(inp).cpu().numpy()[0, 0]\n                \n                sig_px = extract_sig(mask_p)\n                sig_mv = (np.median(sig_px) - sig_px) * 0.018\n                \n                # Resample\n                target_len = int(row['fs'] * (10 if lead_name == 'II' else 2.5))\n                final = resample(sig_mv, target_len)\n                for t, v in enumerate(final):\n                    results.append({'id': f\"{row['id']}_{t}_{lead_name}\", 'value': v})\n\n    pd.DataFrame(results).to_parquet(\"submission.parquet\", index=False)\n    print(\"✅ Result Generated. Check stats again!\")\n\nif __name__ == \"__main__\":\n    main()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-06T12:08:46.086193Z","iopub.execute_input":"2026-01-06T12:08:46.086508Z","iopub.status.idle":"2026-01-06T14:35:40.479886Z","shell.execute_reply.started":"2026-01-06T12:08:46.086473Z","shell.execute_reply":"2026-01-06T14:35:40.475136Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import matplotlib.pyplot as plt\n\n# 1. Re-initialize the Model Architecture\nmodel = ECG_UNet().to(DEVICE)\n# Note: If you saved weights, load them here: \n# model.load_state_dict(torch.load(\"model.pth\"))\n\n# 2. Define Dataset for Visualization\nclass ViewDS(Dataset):\n    def __init__(self, df): \n        self.df = df\n    def __len__(self): \n        return len(self.df)\n    def __getitem__(self, idx):\n        img_id = self.df.iloc[idx]['id']\n        paths = glob.glob(f\"{BASE_PATH}/train/{img_id}/*.png\")\n        if not paths: return torch.zeros((1, *IMG_SIZE)), torch.zeros((1, *IMG_SIZE))\n        img = cv2.imread(paths[0], 0)\n        img = cv2.resize(img, (IMG_SIZE[1], IMG_SIZE[0])) / 255.0\n        mask = cv2.imread(f\"{MASK_DIR}/{img_id}_mask.png\", 0) / 255.0\n        return torch.tensor(img).float().unsqueeze(0), torch.tensor(mask).float().unsqueeze(0)\n\ndef plot_live_results(model, df, num_samples=3):\n    ds = ViewDS(df)\n    model.eval()\n    \n    plt.figure(figsize=(15, num_samples * 4))\n    \n    with torch.no_grad():\n        for i in range(num_samples):\n            img_t, mask_t = ds[i]\n            # Prediction\n            pred_mask = model(img_t.unsqueeze(0).to(DEVICE)).cpu().numpy()[0, 0]\n            \n            # Extract Signal from Pred (Weighted Average)\n            sig_px = extract_sig(pred_mask)\n            \n            # --- Plotting ---\n            # Column 1: Predicted Confidence Heatmap\n            plt.subplot(num_samples, 2, i*2 + 1)\n            plt.imshow(pred_mask, cmap='jet')\n            plt.title(f\"Sample {i}: AI Mask (Confidence)\")\n            plt.colorbar(fraction=0.046, pad=0.04)\n            \n            # Column 2: Overlay on Original\n            plt.subplot(num_samples, 2, i*2 + 2)\n            plt.imshow(img_t.squeeze().numpy(), cmap='gray')\n            plt.plot(np.arange(len(sig_px)), sig_px, color='red', linewidth=1.2, label='AI Trace')\n            plt.title(f\"Sample {i}: Signal Overlay\")\n            plt.legend()\n\n    plt.tight_layout()\n    plt.show()\n\n# --- RUN IT ---\ntrain_df = pd.read_csv(f\"{BASE_PATH}/train.csv\")\nplot_live_results(model, train_df)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-06T14:46:21.453610Z","iopub.execute_input":"2026-01-06T14:46:21.453984Z","iopub.status.idle":"2026-01-06T14:46:24.901610Z","shell.execute_reply.started":"2026-01-06T14:46:21.453959Z","shell.execute_reply":"2026-01-06T14:46:24.900633Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch\nimport torch.nn as nn\nimport torch.optim as optim\nfrom torch.utils.data import Dataset, DataLoader\nimport cv2, os, glob, numpy as np, pandas as pd\nfrom tqdm import tqdm\nfrom scipy.signal import resample\nimport matplotlib.pyplot as plt\n\n# --- 1. CONFIG ---\nIMG_SIZE = (128, 256) \nBASE_PATH = \"/kaggle/input/physionet-ecg-image-digitization\"\nMASK_DIR = \"/kaggle/working/train_masks\"\nMODEL_SAVE_PATH = \"ecg_unet_v2.pth\"\nDEVICE = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nos.makedirs(MASK_DIR, exist_ok=True)\n\n# --- 2. IMPROVED DATASET (Inversion & Contrast) ---\nclass ECGDataset(Dataset):\n    def __init__(self, df, is_train=True):\n        self.df = df\n        self.is_train = is_train\n        \n    def __len__(self): return len(self.df)\n    \n    def __getitem__(self, idx):\n        img_id = self.df.iloc[idx]['id']\n        folder = \"train\" if self.is_train else \"test\"\n        \n        try:\n            path = glob.glob(f\"{BASE_PATH}/{folder}/{img_id}*\")[0]\n            if os.path.isdir(path): # Handle folder-based structure\n                path = glob.glob(f\"{path}/*.png\")[0]\n            \n            img = cv2.imread(path, 0)\n            img = cv2.resize(img, (IMG_SIZE[1], IMG_SIZE[0]))\n            \n            # --- IMPROVEMENT: Contrast Stretch & Invert ---\n            # This makes the ink White (1.0) and background Black (0.0)\n            img = cv2.normalize(img, None, 0, 255, cv2.NORM_MINMAX)\n            img = 1.0 - (img / 255.0) \n        except:\n            img = np.zeros(IMG_SIZE)\n\n        if self.is_train:\n            mask_path = f\"{MASK_DIR}/{img_id}_mask.png\"\n            mask = cv2.imread(mask_path, 0) / 255.0 if os.path.exists(mask_path) else np.zeros(IMG_SIZE)\n            return torch.tensor(img).float().unsqueeze(0), torch.tensor(mask).float().unsqueeze(0)\n        \n        return torch.tensor(img).float().unsqueeze(0), img_id\n\n# --- 3. LOSS FUNCTIONS (BCE + Dice) ---\n# Combined loss forces the model to focus on the thin line geometry\ndef hybrid_loss(pred, target):\n    bce = nn.BCELoss()(pred, target)\n    \n    smooth = 1.\n    pred_flat = pred.view(-1)\n    target_flat = target.view(-1)\n    intersection = (pred_flat * target_flat).sum()\n    dice = 1 - ((2. * intersection + smooth) / (pred_flat.sum() + target_flat.sum() + smooth))\n    \n    return bce + dice\n\n# --- 4. MODEL & UTILS ---\nclass ECG_UNet(nn.Module):\n    def __init__(self):\n        super().__init__()\n        self.enc1 = nn.Sequential(nn.Conv2d(1, 32, 3, padding=1), nn.ReLU())\n        self.pool = nn.MaxPool2d(2)\n        self.enc2 = nn.Sequential(nn.Conv2d(32, 64, 3, padding=1), nn.ReLU())\n        self.up = nn.Upsample(scale_factor=2, mode='bilinear', align_corners=True)\n        self.dec1 = nn.Sequential(nn.Conv2d(64 + 32, 32, 3, padding=1), nn.ReLU())\n        self.out = nn.Conv2d(32, 1, 1)\n        \n    def forward(self, x):\n        e1 = self.enc1(x)\n        e2 = self.enc2(self.pool(e1))\n        d1 = torch.cat([self.up(e2), e1], dim=1)\n        return torch.sigmoid(self.out(self.dec1(d1)))\n\ndef extract_sig(mask_p, threshold=0.15):\n    mask_p[mask_p < threshold] = 0 # Ignore the \"Gray Fog\" noise\n    if np.max(mask_p) < 0.01: return np.ones(IMG_SIZE[1]) * (IMG_SIZE[0] / 2)\n    \n    weights = (mask_p ** 2)\n    y_idx = np.arange(IMG_SIZE[0]).reshape(-1, 1)\n    sig_px = np.sum(y_idx * weights, axis=0) / (np.sum(weights, axis=0) + 1e-6)\n    return sig_px\n\n# --- 5. MAIN EXECUTION ---\ntrain_df = pd.read_csv(f\"{BASE_PATH}/train.csv\")\nmodel = ECG_UNet().to(DEVICE)\n\n# Check for saved weights to skip re-training\nif os.path.exists(MODEL_SAVE_PATH):\n    print(\"📂 Loading weights...\")\n    model.load_state_dict(torch.load(MODEL_SAVE_PATH))\nelse:\n    print(\"🏋️ Training with Hybrid Loss...\")\n    loader = DataLoader(ECGDataset(train_df), batch_size=32, shuffle=True)\n    optimizer = optim.Adam(model.parameters(), lr=0.001)\n    \n    model.train()\n    for epoch in range(10): # Increased epochs for better convergence\n        pbar = tqdm(loader)\n        for imgs, masks in pbar:\n            imgs, masks = imgs.to(DEVICE), masks.to(DEVICE)\n            optimizer.zero_grad()\n            output = model(imgs)\n            loss = hybrid_loss(output, masks)\n            loss.backward()\n            optimizer.step()\n            pbar.set_description(f\"Epoch {epoch+1} Loss: {loss.item():.4f}\")\n    \n    torch.save(model.state_dict(), MODEL_SAVE_PATH)\n\n# --- 6. FINAL VISUALIZATION ---\nmodel.eval()\ntest_ds = ECGDataset(train_df)\nplt.figure(figsize=(15, 8))\nfor i in range(2):\n    img_t, _ = test_ds[i]\n    with torch.no_grad():\n        pred = model(img_t.unsqueeze(0).to(DEVICE)).cpu().numpy()[0,0]\n    \n    sig = extract_sig(pred)\n    \n    plt.subplot(2, 2, i*2+1)\n    plt.imshow(pred, cmap='jet')\n    plt.title(\"Improved Confidence Mask\")\n    \n    plt.subplot(2, 2, i*2+2)\n    plt.imshow(img_t.squeeze(), cmap='gray')\n    plt.plot(sig, color='red', linewidth=1.5)\n    plt.title(\"Resulting Signal Trace\")\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-06T14:50:52.951846Z","iopub.execute_input":"2026-01-06T14:50:52.952338Z","iopub.status.idle":"2026-01-06T14:57:10.305173Z","shell.execute_reply.started":"2026-01-06T14:50:52.952307Z","shell.execute_reply":"2026-01-06T14:57:10.303882Z"}},"outputs":[],"execution_count":null}]}