{"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":"# 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":"2025-12-08T02:28:33.657301Z","iopub.execute_input":"2025-12-08T02:28:33.657580Z","iopub.status.idle":"2025-12-08T02:28:35.095607Z","shell.execute_reply.started":"2025-12-08T02:28:33.657560Z","shell.execute_reply":"2025-12-08T02:28:35.094675Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nimport cv2\nimport scipy.signal as signal\nfrom scipy.ndimage import rotate\nimport random\n\n# ==========================================\n# 1. CONFIGURATION & SETUP\n# ==========================================\nOUTPUT_DIR = \"synthetic_dataset\"\nIMG_DIR = os.path.join(OUTPUT_DIR, \"images\")\nMASK_DIR = os.path.join(OUTPUT_DIR, \"masks\") # Optional: useful for segmentation training\nLABEL_DIR = os.path.join(OUTPUT_DIR, \"labels\")\n\nos.makedirs(IMG_DIR, exist_ok=True)\nos.makedirs(MASK_DIR, exist_ok=True)\nos.makedirs(LABEL_DIR, exist_ok=True)\n\n# ==========================================\n# 2. PURE NUMPY ECG GENERATOR (Replaces NeuroKit2)\n# ==========================================\nclass SyntheticECGGenerator:\n    \"\"\"\n    Generates realistic looking synthetic ECG signals using \n    sum of Gaussians to mimic P-QRS-T complexes.\n    \"\"\"\n    def __init__(self, fs=500):\n        self.fs = fs\n        \n    def _gaussian(self, x, mu, sigma, amp):\n        return amp * np.exp(-0.5 * ((x - mu) / sigma)**2)\n\n    def generate_heartbeat(self, duration_sec=1.0):\n        t = np.linspace(0, duration_sec, int(self.fs * duration_sec))\n        # Standard P-QRS-T parameters (approximate)\n        # P wave\n        p_wave = self._gaussian(t, 0.2, 0.02, 0.15)\n        # Q wave\n        q_wave = self._gaussian(t, 0.43, 0.01, -0.15)\n        # R wave (The main spike)\n        r_wave = self._gaussian(t, 0.45, 0.01, 1.0)\n        # S wave\n        s_wave = self._gaussian(t, 0.47, 0.01, -0.25)\n        # T wave\n        t_wave = self._gaussian(t, 0.7, 0.05, 0.3)\n        \n        heartbeat = p_wave + q_wave + r_wave + s_wave + t_wave\n        return heartbeat\n\n    def generate_lead(self, duration_sec=10, heart_rate=60, noise_level=0.05):\n        \"\"\"\n        Generates a full lead signal with random heart rate variability and noise.\n        \"\"\"\n        # Calculate samples per beat\n        bps = heart_rate / 60.0\n        beat_duration = 1.0 / bps\n        \n        num_samples = int(duration_sec * self.fs)\n        full_signal = np.zeros(num_samples)\n        \n        current_time = 0\n        \n        while current_time < duration_sec:\n            # Add random HRV (Heart Rate Variability)\n            variability = random.uniform(-0.1, 0.1) * beat_duration\n            actual_beat_dur = beat_duration + variability\n            \n            beat = self.generate_heartbeat(actual_beat_dur)\n            \n            start_idx = int(current_time * self.fs)\n            end_idx = start_idx + len(beat)\n            \n            if end_idx < num_samples:\n                full_signal[start_idx:end_idx] += beat\n            else:\n                # Truncate if it goes past end\n                cutoff = num_samples - start_idx\n                full_signal[start_idx:] += beat[:cutoff]\n                break\n                \n            current_time += actual_beat_dur\n\n        # Add Baseline Wander (Low freq sinusoid)\n        t = np.linspace(0, duration_sec, num_samples)\n        wander = 0.1 * np.sin(2 * np.pi * 0.2 * t)  # 0.2 Hz wander\n        \n        # Add High freq noise (Muscle noise)\n        noise = np.random.normal(0, noise_level, num_samples)\n        \n        return full_signal + wander + noise\n\n# ==========================================\n# 3. THE \"PLOTTER\" (Matplotlib -> Image)\n# ==========================================\ndef create_ecg_image(signal_data, filename, fs=500):\n    \"\"\"\n    Plots the signal to resemble a scanned paper ECG.\n    \"\"\"\n    # Random Grid Color (Pink, Red, Orange, or faint Grey)\n    grid_colors = ['#ffccd5', '#ff9999', '#ffb380', '#e0e0e0']\n    selected_grid_color = random.choice(grid_colors)\n    \n    # Plotting setup\n    fig = plt.figure(figsize=(20, 4)) # Wide aspect ratio\n    ax = fig.add_subplot(111)\n    \n    # Minor Grid (1mm) - High density\n    ax.minorticks_on()\n    ax.grid(which='major', linestyle='-', linewidth='0.8', color=selected_grid_color)\n    ax.grid(which='minor', linestyle=':', linewidth='0.5', color=selected_grid_color)\n    \n    # Plot Signal\n    # Randomize line thickness to mimic different pen/printer types\n    line_width = random.uniform(0.8, 2.0)\n    ax.plot(signal_data, color='black', linewidth=line_width)\n    \n    # Remove borders/ticks to look like raw paper\n    ax.tick_params(axis='both', which='both', bottom=False, top=False, \n                   left=False, right=False, labelbottom=False, labelleft=False)\n    for spine in ax.spines.values():\n        spine.set_visible(False)\n        \n    plt.tight_layout(pad=0)\n    \n    # Save to buffer first to apply OpenCV augmentations\n    fig.canvas.draw()\n    img_data = np.frombuffer(fig.canvas.tostring_rgb(), dtype=np.uint8)\n    img_data = img_data.reshape(fig.canvas.get_width_height()[::-1] + (3,))\n    plt.close(fig)\n    \n    # Convert RGB to BGR for OpenCV\n    img_bgr = cv2.cvtColor(img_data, cv2.COLOR_RGB2BGR)\n    return img_bgr\n\n# ==========================================\n# 4. THE \"CORRUPTOR\" (Artifact Injection)\n# ==========================================\ndef apply_augmentations(image):\n    \"\"\"\n    Mimics scanning artifacts: blur, shadows, skew, noise.\n    \"\"\"\n    rows, cols, _ = image.shape\n    \n    # 1. Random Rotate/Skew (Paper misalignment)\n    angle = random.uniform(-2, 2)\n    M = cv2.getRotationMatrix2D((cols/2, rows/2), angle, 1)\n    image = cv2.warpAffine(image, M, (cols, rows), borderValue=(255,255,255))\n    \n    # 2. Gaussian Blur (Scanner focus issue)\n    if random.random() > 0.5:\n        ksize = random.choice([3, 5])\n        image = cv2.GaussianBlur(image, (ksize, ksize), 0)\n        \n    # 3. Salt and Pepper Noise (Dust on scanner)\n    if random.random() > 0.3:\n        noise = np.random.randint(0, 256, (rows, cols, 3), dtype=np.uint8)\n        # Only apply noise with low probability per pixel\n        mask = np.random.rand(rows, cols) > 0.98 \n        image[mask] = 0 # Pepper\n        \n    # 4. Shadow/Lighting Gradient (Mobile phone photo effect)\n    if random.random() > 0.6:\n        # Create a gradient mask\n        X = np.linspace(0, 1, cols)\n        Y = np.linspace(0, 1, rows)\n        X, Y = np.meshgrid(X, Y)\n        gradient = 0.5 + 0.5 * (X * random.choice([-1, 1]) + Y * random.choice([-1, 1]))\n        gradient = np.clip(gradient, 0.4, 1.0)\n        gradient = np.stack([gradient]*3, axis=2)\n        image = (image * gradient).astype(np.uint8)\n        \n    return image\n\n# ==========================================\n# 5. EXECUTION PIPELINE\n# ==========================================\ndef generate_dataset(num_samples=5):\n    \"\"\"\n    Generates N synthetic pairs (Image, CSV).\n    \"\"\"\n    print(f\"Generating {num_samples} synthetic samples...\")\n    gen = SyntheticECGGenerator(fs=500)\n    \n    data_log = []\n    \n    for i in range(num_samples):\n        sample_id = f\"synth_{i:04d}\"\n        \n        # 1. Generate Signal\n        sig = gen.generate_lead(duration_sec=10, heart_rate=random.randint(50, 100))\n        \n        # 2. Render Image\n        raw_img = create_ecg_image(sig, sample_id)\n        \n        # 3. Augment\n        final_img = apply_augmentations(raw_img)\n        \n        # 4. Save Image\n        img_path = os.path.join(IMG_DIR, f\"{sample_id}.png\")\n        cv2.imwrite(img_path, final_img)\n        \n        # 5. Save Ground Truth Labels (CSV)\n        # We save the exact voltage array. \n        # In the competition, you predict this array from the image.\n        label_path = os.path.join(LABEL_DIR, f\"{sample_id}.csv\")\n        pd.DataFrame({'voltage': sig}).to_csv(label_path, index=False)\n        \n        data_log.append({'id': sample_id, 'img_path': img_path, 'label_path': label_path})\n        \n        if (i+1) % 10 == 0:\n            print(f\"Generated {i+1}/{num_samples}\")\n\n    # Save Master CSV\n    pd.DataFrame(data_log).to_csv(os.path.join(OUTPUT_DIR, \"train.csv\"), index=False)\n    print(f\"\\nDone! Dataset saved to {OUTPUT_DIR}/\")\n    print(f\"Images: {len(os.listdir(IMG_DIR))}\")\n    print(f\"Labels: {len(os.listdir(LABEL_DIR))}\")\n\n# Run generation\nif __name__ == \"__main__\":\n    generate_dataset(num_samples=10) # Change to 1000+ for real training","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-08T02:28:35.096728Z","iopub.execute_input":"2025-12-08T02:28:35.097208Z","iopub.status.idle":"2025-12-08T02:28:37.327825Z","shell.execute_reply.started":"2025-12-08T02:28:35.097186Z","shell.execute_reply":"2025-12-08T02:28:37.326790Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport io\nimport random\nimport numpy as np\nimport pandas as pd\nimport cv2\nimport matplotlib.pyplot as plt\nfrom scipy.ndimage import gaussian_filter\n\n# ==========================================\n# 1. CONFIGURATION\n# ==========================================\n# Generates data suitable for training a model to predict voltage from image\nCONFIG = {\n    \"output_dir\": \"synthetic_data\",\n    \"samples_to_generate\": 20,  # Set to 10,000+ for actual training\n    \"fs\": 500,                  # Sampling Frequency (Hz)\n    \"duration\": 10,             # Duration of strip (seconds)\n    \"img_dpi\": 200              # Quality of the \"scanned\" image\n}\n\n# Directory Setup\nIMG_PATH = os.path.join(CONFIG[\"output_dir\"], \"images\")\nLBL_PATH = os.path.join(CONFIG[\"output_dir\"], \"labels\")\nos.makedirs(IMG_PATH, exist_ok=True)\nos.makedirs(LBL_PATH, exist_ok=True)\n\n# ==========================================\n# 2. SIGNAL GENERATOR (Physics-Based)\n# ==========================================\ndef generate_synthetic_signal(fs, duration):\n    \"\"\"\n    Generates a realistic ECG signal using a series of \n    synthetic heartbeats (P-QRS-T complex simulation).\n    \"\"\"\n    total_samples = fs * duration\n    t = np.linspace(0, duration, total_samples)\n    signal = np.zeros(total_samples)\n    \n    # Random Heart Rate (60 - 100 bpm)\n    bpm = np.random.randint(60, 100)\n    beat_interval = 60.0 / bpm\n    \n    current_time = 0.0\n    while current_time < duration:\n        # P-QRS-T approx location relative to beat start (in seconds)\n        # We model the QRS complex as a sharp R-peak with smaller Q and S dips\n        \n        # Center of the beat\n        center = current_time + (beat_interval * 0.1) \n        \n        # R-peak (The big spike)\n        width = 0.02\n        signal += 1.2 * np.exp(-((t - (center + 0.0))**2) / (2 * width**2))\n        \n        # Q-wave (dip before)\n        signal -= 0.25 * np.exp(-((t - (center - 0.04))**2) / (2 * width**2))\n        \n        # S-wave (dip after)\n        signal -= 0.25 * np.exp(-((t - (center + 0.04))**2) / (2 * width**2))\n        \n        # T-wave (broad bump after)\n        signal += 0.3 * np.exp(-((t - (center + 0.2))**2) / (2 * (width*4)**2))\n        \n        # P-wave (small bump before)\n        signal += 0.15 * np.exp(-((t - (center - 0.15))**2) / (2 * (width*2)**2))\n        \n        # Move to next beat with slight irregularity (Arrhythmia sim)\n        current_time += beat_interval + np.random.uniform(-0.05, 0.05)\n\n    # 1. Add Baseline Wander (Breathing artifact)\n    wander_freq = np.random.uniform(0.1, 0.5)\n    signal += 0.15 * np.sin(2 * np.pi * wander_freq * t)\n    \n    # 2. Add High Frequency Noise (Muscle noise)\n    signal += np.random.normal(0, 0.02, total_samples)\n    \n    return t, signal\n\n# ==========================================\n# 3. IMAGE RENDERER (The \"Scanner\")\n# ==========================================\ndef render_ecg_image(signal, filename):\n    \"\"\"\n    Plots the signal using Matplotlib and converts it to an OpenCV image\n    simulating a paper scan.\n    \"\"\"\n    # Randomize visual style\n    grid_color = random.choice(['#ffcccc', '#ff9999', '#e6e6e6', '#ffe6cc']) # Pink, Red, Grey, Orange\n    line_width = random.uniform(0.8, 1.5)\n    \n    # Setup Figure\n    # Aspect ratio mimic typical ECG strip\n    fig = plt.figure(figsize=(12, 3), dpi=CONFIG['img_dpi'])\n    ax = fig.add_subplot(111)\n    \n    # Setup Grid (The main enemy in this competition)\n    ax.set_xlim(0, len(signal))\n    ax.set_ylim(-2, 2)\n    \n    # Major grid\n    ax.xaxis.set_major_locator(plt.MultipleLocator(CONFIG['fs'] * 0.2)) # 0.2s boxes\n    ax.yaxis.set_major_locator(plt.MultipleLocator(0.5)) # 0.5mV boxes\n    ax.grid(which='major', color=grid_color, linestyle='-', linewidth=0.8)\n    \n    # Minor grid\n    ax.minorticks_on()\n    ax.xaxis.set_minor_locator(plt.MultipleLocator(CONFIG['fs'] * 0.04)) # 0.04s boxes\n    ax.yaxis.set_minor_locator(plt.MultipleLocator(0.1)) # 0.1mV boxes\n    ax.grid(which='minor', color=grid_color, linestyle=':', linewidth=0.5)\n    \n    # Plot Signal\n    ax.plot(signal, color='black', linewidth=line_width)\n    \n    # Hide axis labels/spines\n    ax.set_xticklabels([])\n    ax.set_yticklabels([])\n    for spine in ax.spines.values():\n        spine.set_visible(False)\n    ax.tick_params(left=False, bottom=False)\n\n    plt.tight_layout(pad=0)\n    \n    # Save to memory buffer (Robust method)\n    buf = io.BytesIO()\n    plt.savefig(buf, format='png', bbox_inches='tight', pad_inches=0)\n    buf.seek(0)\n    plt.close(fig)\n    \n    # Decode buffer to OpenCV image\n    file_bytes = np.asarray(bytearray(buf.read()), dtype=np.uint8)\n    img = cv2.imdecode(file_bytes, cv2.IMREAD_COLOR)\n    \n    return img\n\n# ==========================================\n# 4. AUGMENTATION ENGINE\n# ==========================================\ndef augment_scan(image):\n    \"\"\"\n    Applies 'Real World' defects: Rotations, Shadows, Blur.\n    \"\"\"\n    rows, cols, _ = image.shape\n    \n    # 1. Skew / Rotation (Paper not straight)\n    if random.random() > 0.3:\n        angle = random.uniform(-2, 2)\n        M = cv2.getRotationMatrix2D((cols/2, rows/2), angle, 1)\n        image = cv2.warpAffine(image, M, (cols, rows), borderValue=(255,255,255))\n    \n    # 2. Lighting Gradients (Mobile phone flash/shadow)\n    # This creates a dark corner or side\n    if random.random() > 0.5:\n        decay = random.uniform(0.3, 0.8)\n        direction = random.choice([0, 1]) # 0 = vertical, 1 = horizontal\n        \n        gradient = np.linspace(decay, 1.0, rows if direction==0 else cols)\n        gradient = np.tile(gradient, (cols if direction==0 else rows, 1))\n        if direction == 0: gradient = gradient.T\n        \n        # Expand to 3 channels\n        gradient = np.dstack([gradient] * 3)\n        image = (image * gradient).astype(np.uint8)\n\n    # 3. Gaussian Blur (Poor focus)\n    if random.random() > 0.5:\n        k = random.choice([3, 5])\n        image = cv2.GaussianBlur(image, (k, k), 0)\n        \n    return image\n\n# ==========================================\n# 5. MAIN EXECUTION\n# ==========================================\ndef main():\n    print(f\"Starting generation of {CONFIG['samples_to_generate']} samples...\")\n    \n    metadata = []\n    \n    for i in range(CONFIG['samples_to_generate']):\n        sample_id = f\"syn_{i:05d}\"\n        \n        # 1. Generate Math Signal\n        time_steps, voltage = generate_synthetic_signal(CONFIG['fs'], CONFIG['duration'])\n        \n        # 2. Render to Image\n        clean_img = render_ecg_image(voltage, sample_id)\n        \n        # 3. Corrupt Image (Augmentation)\n        final_img = augment_scan(clean_img)\n        \n        # 4. Save\n        img_filename = f\"{sample_id}.png\"\n        csv_filename = f\"{sample_id}.csv\"\n        \n        cv2.imwrite(os.path.join(IMG_PATH, img_filename), final_img)\n        \n        # Save Ground Truth as DataFrame\n        df = pd.DataFrame({'time': time_steps, 'voltage': voltage})\n        df.to_csv(os.path.join(LBL_PATH, csv_filename), index=False)\n        \n        metadata.append({\n            'id': sample_id, \n            'image_file': img_filename,\n            'label_file': csv_filename\n        })\n        \n        if (i+1) % 5 == 0:\n            print(f\"Generated {i+1} samples...\")\n\n    # Save manifest\n    pd.DataFrame(metadata).to_csv(os.path.join(CONFIG[\"output_dir\"], \"train_manifest.csv\"), index=False)\n    print(\"Generation Complete.\")\n\nif __name__ == \"__main__\":\n    main()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-08T02:28:37.328673Z","iopub.execute_input":"2025-12-08T02:28:37.328938Z","iopub.status.idle":"2025-12-08T02:29:04.701583Z","shell.execute_reply.started":"2025-12-08T02:28:37.328917Z","shell.execute_reply":"2025-12-08T02:29:04.700937Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport io\nimport random\nimport numpy as np\nimport cv2\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nfrom torch.utils.data import Dataset, DataLoader\nimport matplotlib.pyplot as plt\n\n# ==========================================\n# 1. CONFIGURATION\n# ==========================================\nCONFIG = {\n    \"root_dir\": \"seg_dataset\",\n    \"img_size\": (256, 512),  # (Height, Width) - optimized for GPU memory\n    \"batch_size\": 8,\n    \"num_samples\": 50,       # Increase for real training\n    \"device\": torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n}\n\nos.makedirs(os.path.join(CONFIG[\"root_dir\"], \"images\"), exist_ok=True)\nos.makedirs(os.path.join(CONFIG[\"root_dir\"], \"masks\"), exist_ok=True)\n\n# ==========================================\n# 2. PAIRED DATA GENERATOR (Image + Mask)\n# ==========================================\ndef render_pair(signal, filename):\n    \"\"\"\n    Renders two versions of the same signal:\n    1. Input: Noisy, Grid, Random Color\n    2. Target: Binary Mask (White signal on Black background)\n    \"\"\"\n    # Common Plot Parameters\n    figsize = (10, 5)\n    dpi = 100\n    ylim = (-2, 2)\n    \n    # --- A. Render INPUT (The Noisy Image) ---\n    fig = plt.figure(figsize=figsize, dpi=dpi)\n    ax = fig.add_subplot(111)\n    ax.set_ylim(ylim)\n    ax.set_xlim(0, len(signal))\n    \n    # Grid setup (The noise)\n    ax.minorticks_on()\n    grid_color = random.choice(['#ffcccc', '#ff9999', '#e0e0e0'])\n    ax.grid(which='major', linestyle='-', linewidth=0.8, color=grid_color)\n    ax.grid(which='minor', linestyle=':', linewidth=0.5, color=grid_color)\n    \n    # Signal\n    ax.plot(signal, color='black', linewidth=random.uniform(1.0, 2.0))\n    \n    # Cleanup\n    ax.axis('off')\n    plt.tight_layout(pad=0)\n    \n    # Save Input\n    buf = io.BytesIO()\n    plt.savefig(buf, format='png', bbox_inches='tight', pad_inches=0)\n    buf.seek(0)\n    img_bytes = np.asarray(bytearray(buf.read()), dtype=np.uint8)\n    input_img = cv2.imdecode(img_bytes, cv2.IMREAD_COLOR)\n    plt.close(fig)\n\n    # --- B. Render MASK (The Ground Truth) ---\n    fig_m = plt.figure(figsize=figsize, dpi=dpi)\n    ax_m = fig_m.add_subplot(111)\n    ax_m.set_ylim(ylim)\n    ax_m.set_xlim(0, len(signal))\n    \n    # No Grid. Black Background. White Line.\n    fig_m.patch.set_facecolor('black')\n    ax_m.set_facecolor('black')\n    ax_m.plot(signal, color='white', linewidth=2.0) # Slightly thicker for easier learning\n    \n    # Cleanup\n    ax_m.axis('off')\n    plt.tight_layout(pad=0)\n    \n    # Save Mask\n    buf_m = io.BytesIO()\n    plt.savefig(buf_m, format='png', bbox_inches='tight', pad_inches=0)\n    buf_m.seek(0)\n    mask_bytes = np.asarray(bytearray(buf_m.read()), dtype=np.uint8)\n    # Read as Grayscale\n    target_mask = cv2.imdecode(mask_bytes, cv2.IMREAD_GRAYSCALE)\n    plt.close(fig_m)\n    \n    # Resize both to ensure exact match for U-Net\n    input_img = cv2.resize(input_img, (CONFIG[\"img_size\"][1], CONFIG[\"img_size\"][0]))\n    target_mask = cv2.resize(target_mask, (CONFIG[\"img_size\"][1], CONFIG[\"img_size\"][0]))\n    \n    # Binarize mask (ensure strict 0 or 255)\n    _, target_mask = cv2.threshold(target_mask, 127, 255, cv2.THRESH_BINARY)\n    \n    return input_img, target_mask\n\ndef generate_segmentation_data():\n    print(f\"Generating {CONFIG['num_samples']} segmentation pairs...\")\n    \n    # Simple sine wave generator for speed (Replace with ECG gen from Task 1.1 for real run)\n    x = np.linspace(0, 20, 1000)\n    \n    for i in range(CONFIG['num_samples']):\n        # Random complex wave\n        f1, f2 = random.uniform(0.5, 2), random.uniform(2, 5)\n        y = np.sin(f1 * x) + 0.5 * np.cos(f2 * x)\n        \n        img, mask = render_pair(y, f\"sample_{i}\")\n        \n        cv2.imwrite(os.path.join(CONFIG[\"root_dir\"], \"images\", f\"{i}.png\"), img)\n        cv2.imwrite(os.path.join(CONFIG[\"root_dir\"], \"masks\", f\"{i}.png\"), mask)\n        \n    print(\"Data Generation Complete.\")\n\n# ==========================================\n# 3. U-NET ARCHITECTURE (From Scratch)\n# ==========================================\nclass DoubleConv(nn.Module):\n    def __init__(self, in_channels, out_channels):\n        super().__init__()\n        self.double_conv = nn.Sequential(\n            nn.Conv2d(in_channels, out_channels, kernel_size=3, padding=1),\n            nn.BatchNorm2d(out_channels),\n            nn.ReLU(inplace=True),\n            nn.Conv2d(out_channels, out_channels, kernel_size=3, padding=1),\n            nn.BatchNorm2d(out_channels),\n            nn.ReLU(inplace=True)\n        )\n\n    def forward(self, x):\n        return self.double_conv(x)\n\nclass UNet(nn.Module):\n    def __init__(self, n_channels=3, n_classes=1):\n        super(UNet, self).__init__()\n        self.n_channels = n_channels\n        self.n_classes = n_classes\n\n        # Encoder (Downsampling)\n        self.inc = DoubleConv(n_channels, 64)\n        self.down1 = nn.Sequential(nn.MaxPool2d(2), DoubleConv(64, 128))\n        self.down2 = nn.Sequential(nn.MaxPool2d(2), DoubleConv(128, 256))\n        self.down3 = nn.Sequential(nn.MaxPool2d(2), DoubleConv(256, 512))\n        self.down4 = nn.Sequential(nn.MaxPool2d(2), DoubleConv(512, 1024))\n\n        # Decoder (Upsampling)\n        self.up1 = nn.ConvTranspose2d(1024, 512, kernel_size=2, stride=2)\n        self.conv1 = DoubleConv(1024, 512)\n        self.up2 = nn.ConvTranspose2d(512, 256, kernel_size=2, stride=2)\n        self.conv2 = DoubleConv(512, 256)\n        self.up3 = nn.ConvTranspose2d(256, 128, kernel_size=2, stride=2)\n        self.conv3 = DoubleConv(256, 128)\n        self.up4 = nn.ConvTranspose2d(128, 64, kernel_size=2, stride=2)\n        self.conv4 = DoubleConv(128, 64)\n\n        # Output Layer\n        self.outc = nn.Conv2d(64, n_classes, kernel_size=1)\n\n    def forward(self, x):\n        # Down\n        x1 = self.inc(x)\n        x2 = self.down1(x1)\n        x3 = self.down2(x2)\n        x4 = self.down3(x3)\n        x5 = self.down4(x4)\n        \n        # Up (with skip connections)\n        x = self.up1(x5)\n        # Handle padding issues if dimensions are odd\n        diffY = x4.size()[2] - x.size()[2]\n        diffX = x4.size()[3] - x.size()[3]\n        x = F.pad(x, [diffX // 2, diffX - diffX // 2, diffY // 2, diffY - diffY // 2])\n        \n        x = torch.cat([x4, x], dim=1)\n        x = self.conv1(x)\n\n        x = self.up2(x)\n        diffY = x3.size()[2] - x.size()[2]\n        diffX = x3.size()[3] - x.size()[3]\n        x = F.pad(x, [diffX // 2, diffX - diffX // 2, diffY // 2, diffY - diffY // 2])\n        x = torch.cat([x3, x], dim=1)\n        x = self.conv2(x)\n\n        x = self.up3(x)\n        diffY = x2.size()[2] - x.size()[2]\n        diffX = x2.size()[3] - x.size()[3]\n        x = F.pad(x, [diffX // 2, diffX - diffX // 2, diffY // 2, diffY - diffY // 2])\n        x = torch.cat([x2, x], dim=1)\n        x = self.conv3(x)\n\n        x = self.up4(x)\n        diffY = x1.size()[2] - x.size()[2]\n        diffX = x1.size()[3] - x.size()[3]\n        x = F.pad(x, [diffX // 2, diffX - diffX // 2, diffY // 2, diffY - diffY // 2])\n        x = torch.cat([x1, x], dim=1)\n        x = self.conv4(x)\n\n        logits = self.outc(x)\n        return logits\n\n# ==========================================\n# 4. DATASET & TRAINING LOOP\n# ==========================================\nclass ECGMaskDataset(Dataset):\n    def __init__(self, root_dir):\n        self.root_dir = root_dir\n        self.ids = [f.split('.')[0] for f in os.listdir(os.path.join(root_dir, \"images\"))]\n        \n    def __len__(self):\n        return len(self.ids)\n    \n    def __getitem__(self, idx):\n        file_id = self.ids[idx]\n        img_path = os.path.join(self.root_dir, \"images\", f\"{file_id}.png\")\n        mask_path = os.path.join(self.root_dir, \"masks\", f\"{file_id}.png\")\n        \n        # Load and Normalize\n        img = cv2.imread(img_path) / 255.0  # (H, W, 3)\n        mask = cv2.imread(mask_path, cv2.IMREAD_GRAYSCALE) / 255.0 # (H, W)\n        \n        # To Tensor (C, H, W)\n        img = torch.from_numpy(img).permute(2, 0, 1).float()\n        mask = torch.from_numpy(mask).unsqueeze(0).float()\n        \n        return img, mask\n\ndef run_training_check():\n    # 1. Generate Data\n    generate_segmentation_data()\n    \n    # 2. Setup\n    dataset = ECGMaskDataset(CONFIG[\"root_dir\"])\n    loader = DataLoader(dataset, batch_size=CONFIG[\"batch_size\"], shuffle=True)\n    model = UNet(n_channels=3, n_classes=1).to(CONFIG[\"device\"])\n    \n    # 3. Loss & Opt\n    # Combine BCE and Dice for better line segmentation\n    criterion = nn.BCEWithLogitsLoss() \n    optimizer = torch.optim.Adam(model.parameters(), lr=1e-4)\n    \n    # 4. Mock Epoch\n    model.train()\n    print(\"Starting Training Loop Check...\")\n    for images, masks in loader:\n        images = images.to(CONFIG[\"device\"])\n        masks = masks.to(CONFIG[\"device\"])\n        \n        optimizer.zero_grad()\n        outputs = model(images)\n        loss = criterion(outputs, masks)\n        loss.backward()\n        optimizer.step()\n        \n        print(f\"Batch Loss: {loss.item():.4f}\")\n        break # Just run one batch to prove it works\n        \n    print(\"Training pipeline is functional.\")\n\nif __name__ == \"__main__\":\n    run_training_check()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-08T02:29:04.702975Z","iopub.execute_input":"2025-12-08T02:29:04.703351Z","iopub.status.idle":"2025-12-08T02:29:53.094010Z","shell.execute_reply.started":"2025-12-08T02:29:04.703306Z","shell.execute_reply":"2025-12-08T02:29:53.093053Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport random\nimport numpy as np\nimport cv2\nimport torch\nimport torch.nn as nn\nimport torch.optim as optim\nfrom torch.utils.data import Dataset, DataLoader\nimport matplotlib.pyplot as plt\n\n# ==========================================\n# 1. CONFIGURATION\n# ==========================================\nCONFIG = {\n    \"root_dir\": \"lead_detection_data\",\n    \"img_size\": (256, 256),    # Input size for the Regressor\n    \"num_samples\": 50,         # Increase for real training\n    \"num_leads\": 12,           # Standard 12-lead ECG\n    \"batch_size\": 8,\n    \"device\": torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n}\n\nos.makedirs(CONFIG[\"root_dir\"], exist_ok=True)\n\n# ==========================================\n# 2. FULL PAGE LAYOUT GENERATOR\n# ==========================================\ndef create_dummy_lead_image(width, height):\n    \"\"\"\n    Creates a single lead image (white background, black signal)\n    using simple drawing for speed (mimics Task 1.1 output).\n    \"\"\"\n    img = np.ones((height, width, 3), dtype=np.uint8) * 255\n    \n    # Draw random grid\n    grid_color = (200, 200, 200)\n    for x in range(0, width, 20): cv2.line(img, (x, 0), (x, height), grid_color, 1)\n    for y in range(0, height, 20): cv2.line(img, (0, y), (width, y), grid_color, 1)\n    \n    # Draw random signal\n    points = []\n    for x in range(0, width, 5):\n        y = height//2 + int(np.random.randint(-height//3, height//3))\n        points.append((x, y))\n    \n    # Smooth curve\n    if len(points) > 2:\n        pts = np.array(points, np.int32)\n        cv2.polylines(img, [pts], False, (0, 0, 0), 2)\n        \n    return img\n\ndef generate_full_page_layout():\n    \"\"\"\n    Simulates a 12-lead ECG page (3 rows x 4 columns layout).\n    Returns:\n        full_page_img: The composite image\n        boxes: List of [x1, y1, x2, y2] normalized coordinates (0-1)\n    \"\"\"\n    # Canvas size (Standard A4 ish aspect ratio)\n    W, H = 2000, 1500\n    full_page = np.ones((H, W, 3), dtype=np.uint8) * 255\n    \n    # Layout Config: 3 Rows, 4 Columns\n    rows = 3\n    cols = 4\n    \n    pad_x = 50\n    pad_y = 50\n    \n    lead_w = (W - (cols+1)*pad_x) // cols\n    lead_h = (H - (rows+1)*pad_y) // rows\n    \n    boxes = []\n    \n    for r in range(rows):\n        for c in range(cols):\n            # Calculate coordinates\n            x1 = pad_x + c * (lead_w + pad_x)\n            y1 = pad_y + r * (lead_h + pad_y)\n            x2 = x1 + lead_w\n            y2 = y1 + lead_h\n            \n            # Generate Lead Content\n            lead_img = create_dummy_lead_image(lead_w, lead_h)\n            \n            # Paste into canvas\n            full_page[y1:y2, x1:x2] = lead_img\n            \n            # Store Normalized Coordinates (x1, y1, x2, y2)\n            norm_box = [x1/W, y1/H, x2/W, y2/H]\n            boxes.append(norm_box)\n            \n            # Add Label Text (I, II, III, etc.)\n            cv2.putText(full_page, f\"L{r*4+c}\", (x1+10, y1+30), \n                        cv2.FONT_HERSHEY_SIMPLEX, 1, (0,0,150), 2)\n\n    return full_page, np.array(boxes, dtype=np.float32).flatten()\n\ndef generate_dataset():\n    print(f\"Generating {CONFIG['num_samples']} full-page layouts...\")\n    data_log = []\n    \n    for i in range(CONFIG['num_samples']):\n        img, boxes = generate_full_page_layout()\n        \n        # Resize for Model Input (to save disk space/memory)\n        # We keep aspect ratio in mind usually, but for bbox regression \n        # simple resizing is often sufficient if we normalize coords.\n        img_resized = cv2.resize(img, CONFIG['img_size'])\n        \n        filename = f\"{i:04d}.png\"\n        path = os.path.join(CONFIG[\"root_dir\"], filename)\n        cv2.imwrite(path, img_resized)\n        \n        # Store box data\n        data_log.append({'filename': filename, 'boxes': boxes})\n        \n    print(\"Dataset Generation Complete.\")\n    return data_log\n\n# ==========================================\n# 3. CUSTOM CNN REGRESSOR (Mini-ResNet Style)\n# ==========================================\nclass ConvBlock(nn.Module):\n    def __init__(self, in_c, out_c):\n        super().__init__()\n        self.conv = nn.Sequential(\n            nn.Conv2d(in_c, out_c, 3, padding=1),\n            nn.BatchNorm2d(out_c),\n            nn.ReLU(),\n            nn.MaxPool2d(2)\n        )\n    def forward(self, x): return self.conv(x)\n\nclass BoxRegNet(nn.Module):\n    def __init__(self, num_leads=12):\n        super().__init__()\n        # Input: 3 x 256 x 256\n        self.features = nn.Sequential(\n            ConvBlock(3, 32),    # -> 128\n            ConvBlock(32, 64),   # -> 64\n            ConvBlock(64, 128),  # -> 32\n            ConvBlock(128, 256), # -> 16\n            ConvBlock(256, 512), # -> 8\n        )\n        \n        self.flatten_dim = 512 * 8 * 8\n        \n        # Output: 12 leads * 4 coords (x1,y1,x2,y2) = 48 values\n        self.regressor = nn.Sequential(\n            nn.Flatten(),\n            nn.Linear(self.flatten_dim, 1024),\n            nn.ReLU(),\n            nn.Dropout(0.5),\n            nn.Linear(1024, num_leads * 4), \n            nn.Sigmoid() # Force output to be 0-1 (Normalized coordinates)\n        )\n        \n    def forward(self, x):\n        x = self.features(x)\n        x = self.regressor(x)\n        return x\n\n# ==========================================\n# 4. DATA LOADER & TRAINING\n# ==========================================\nclass PageDataset(Dataset):\n    def __init__(self, data_list, root_dir):\n        self.data = data_list\n        self.root_dir = root_dir\n        \n    def __len__(self): return len(self.data)\n    \n    def __getitem__(self, idx):\n        item = self.data[idx]\n        img = cv2.imread(os.path.join(self.root_dir, item['filename']))\n        img = cv2.resize(img, CONFIG['img_size']) # Ensure size\n        img = img / 255.0\n        \n        # To Tensor (C, H, W)\n        img_t = torch.from_numpy(img).permute(2,0,1).float()\n        boxes_t = torch.tensor(item['boxes'], dtype=torch.float32)\n        \n        return img_t, boxes_t\n\ndef train_detector():\n    # 1. Prepare Data\n    data_log = generate_dataset()\n    dataset = PageDataset(data_log, CONFIG[\"root_dir\"])\n    loader = DataLoader(dataset, batch_size=CONFIG[\"batch_size\"], shuffle=True)\n    \n    # 2. Model Setup\n    model = BoxRegNet().to(CONFIG[\"device\"])\n    \n    # SmoothL1Loss is robust to outliers (better for coordinates)\n    criterion = nn.SmoothL1Loss()\n    optimizer = optim.Adam(model.parameters(), lr=0.001)\n    \n    # 3. Train Loop\n    print(\"\\nStarting Training...\")\n    model.train()\n    \n    # Just 1 epoch demonstration\n    for batch_idx, (imgs, targets) in enumerate(loader):\n        imgs, targets = imgs.to(CONFIG[\"device\"]), targets.to(CONFIG[\"device\"])\n        \n        optimizer.zero_grad()\n        preds = model(imgs)\n        \n        loss = criterion(preds, targets)\n        loss.backward()\n        optimizer.step()\n        \n        if batch_idx % 2 == 0:\n            print(f\"Batch {batch_idx} Loss: {loss.item():.5f}\")\n            \n    print(\"Detector Trained.\")\n    return model\n\n# ==========================================\n# 5. INFERENCE / VISUALIZATION\n# ==========================================\ndef visualize_prediction(model):\n    # Load a test image\n    test_img_raw, test_boxes = generate_full_page_layout()\n    \n    # Preprocess\n    img_rs = cv2.resize(test_img_raw, CONFIG['img_size'])\n    img_t = torch.from_numpy(img_rs/255.0).permute(2,0,1).float().unsqueeze(0).to(CONFIG[\"device\"])\n    \n    # Predict\n    model.eval()\n    with torch.no_grad():\n        preds = model(img_t).cpu().numpy()[0]\n    \n    # Draw Predictions on original image\n    H, W, _ = test_img_raw.shape\n    \n    # Reshape flattened predictions to (12, 4)\n    pred_boxes = preds.reshape(-1, 4)\n    \n    plt.figure(figsize=(10, 8))\n    plt.imshow(test_img_raw)\n    ax = plt.gca()\n    \n    for box in pred_boxes:\n        # Denormalize\n        x1, y1, x2, y2 = box[0]*W, box[1]*H, box[2]*W, box[3]*H\n        \n        # Draw Rectangle (Red)\n        rect = plt.Rectangle((x1, y1), x2-x1, y2-y1, \n                             fill=False, edgecolor='red', linewidth=3)\n        ax.add_patch(rect)\n        \n    plt.title(\"Red: Predicted ROIs (Lead Detection)\")\n    plt.axis('off')\n    plt.show()\n\nif __name__ == \"__main__\":\n    trained_model = train_detector()\n    visualize_prediction(trained_model)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-08T02:29:53.095224Z","iopub.execute_input":"2025-12-08T02:29:53.095512Z","iopub.status.idle":"2025-12-08T02:30:01.052642Z","shell.execute_reply.started":"2025-12-08T02:29:53.095497Z","shell.execute_reply":"2025-12-08T02:30:01.052019Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import numpy as np\nimport cv2\nimport matplotlib.pyplot as plt\nimport pandas as pd\nimport scipy.interpolate as interp\nimport random\n\n# ==========================================\n# 1. CONFIGURATION & SYNTHETIC MASK GEN\n# ==========================================\n# This section generates a \"perfect\" binary mask output \n# similar to what your U-Net from Task 1.2 should produce.\n\ndef generate_synthetic_mask(width=512, height=256, gaps=False):\n    \"\"\"\n    Generates a clean binary mask of a synthetic ECG signal.\n    Returns: HxW numpy array (uint8, 0 corresponds to background, 255 to signal)\n    \"\"\"\n    mask = np.zeros((height, width), dtype=np.uint8)\n    \n    t = np.linspace(0, 4*np.pi, width)\n    # Complex-ish wave\n    signal_y = np.sin(t) + 0.5*np.cos(3*t) + 0.2*np.sin(10*t)\n    \n    # Normalize signal to fit in image height with padding\n    signal_norm = (signal_y - signal_y.min()) / (signal_y.max() - signal_y.min())\n    # Scale to pixel coordinates (leave 10% padding top/bottom)\n    pixel_y = (signal_norm * (height * 0.8) + height * 0.1).astype(np.int32)\n    \n    # Invert Y because image coordinates start at top\n    pixel_y = height - 1 - pixel_y\n    \n    # Draw the signal\n    pts = np.array(list(zip(range(width), pixel_y)), np.int32)\n    # Thickness 3 to simulate U-Net output uncertainty\n    cv2.polylines(mask, [pts], False, 255, thickness=3) \n    \n    if gaps:\n        # Simulate segmentation failure (broken lines)\n        for _ in range(3):\n            gap_start = random.randint(0, width-20)\n            mask[:, gap_start:gap_start+random.randint(5, 20)] = 0\n            \n    return mask, signal_y # Return ground truth for comparison\n\n# ==========================================\n# 2. THE EXTRACTOR (Column-wise Center of Mass)\n# ==========================================\ndef extract_raw_signal(binary_mask):\n    \"\"\"\n    Iterates through image columns to find the Y-position of the signal.\n    \"\"\"\n    H, W = binary_mask.shape\n    raw_signal = np.full(W, np.nan) # Initialize with NaNs for gaps\n    \n    for col_idx in range(W):\n        column_pixels = binary_mask[:, col_idx]\n        \n        # Find indices of signal pixels (white dots)\n        # We use a threshold of 127 to handle edges if mask isn't purely binary\n        signal_indices = np.where(column_pixels > 127)[0]\n        \n        if len(signal_indices) > 0:\n            # Calculate Center of Mass for this column\n            # This handles thick lines robustly.\n            avg_y_pixel = np.mean(signal_indices)\n            \n            # Transform pixel coordinate to normalized signal coordinate [-1, 1]\n            # 1. Invert Y axis (Image 0 is top, Signal +ve is top)\n            inverted_y = H - 1 - avg_y_pixel\n            \n            # 2. Normalize based on image height assuming signal is centered\n            # 0 becomes -1, H becomes +1, H/2 becomes 0\n            norm_y = (inverted_y / (H / 2.0)) - 1.0\n            \n            raw_signal[col_idx] = norm_y\n            \n    return raw_signal\n\n# ==========================================\n# 3. POST-PROCESSING (Gap Filling & Smoothing)\n# ==========================================\ndef process_signal(raw_signal_array, calibration_factor=2.0):\n    \"\"\"\n    Handles missing data and scales to millivolts.\n    \"\"\"\n    # 1. Interpolate Gaps (NaNs)\n    # Pandas offers the easiest, most robust linear interpolation for this\n    s_series = pd.Series(raw_signal_array)\n    \n    # 'limit_direction=\"both\"' fills gaps at edges too\n    interpolated_signal = s_series.interpolate(method='linear', limit_direction='both').to_numpy()\n    \n    # Handle case where entire signal might be NaN (empty mask)\n    if np.isnan(interpolated_signal).all():\n        return np.zeros_like(interpolated_signal)\n\n    # 2. Smoothing (Optional but recommended)\n    # Removes pixel quantization noise. Savitzky-Golay filter preserves peaks well.\n    # window_length must be odd. Polyorder usually 2 or 3.\n    try:\n        smoothed_signal = signal.savgol_filter(interpolated_signal, window_length=11, polyorder=3)\n    except:\n        # Fallback if signal is too short for the filter window\n        smoothed_signal = interpolated_signal\n\n    # 3. Voltage Calibration\n    # Convert normalized [-1, 1] range to actual millivolts.\n    # You need to determine this factor based on the grid lines detected in Task 2.1.\n    # For now, we assume the image height spans +/- 2.0 mV.\n    final_mv_signal = smoothed_signal * calibration_factor\n    \n    return final_mv_signal\n\n# ==========================================\n# 4. PIPELINE EXECUTION & VISUALIZATION\n# ==========================================\ndef run_extraction_pipeline():\n    # A. Generate Synthetic Data (Simulating U-Net Output)\n    # Create a mask with intentional gaps to test robustness\n    mask_img, gt_signal = generate_synthetic_mask(width=1000, height=300, gaps=True)\n    \n    # B. Run Extraction Logic\n    print(\"Extracting raw data points from image columns...\")\n    raw_extracted = extract_raw_signal(mask_img)\n    \n    # C. Run Post-Processing\n    print(\"Interpolating gaps and calibrating to mV...\")\n    # We normalize GT for comparison, so we use calibration=1.0 here for visualization\n    final_signal = process_signal(raw_extracted, calibration_factor=1.0)\n    \n    # D. Visualization\n    fig, ax = plt.subplots(3, 1, figsize=(12, 10))\n    \n    # Plot 1: The Input Mask\n    ax[0].set_title(\"Input: Binary Segmentation Mask (with simulated gaps)\")\n    ax[0].imshow(mask_img, cmap='gray')\n    ax[0].axis('off')\n    \n    # Plot 2: Raw Extraction vs Ground Truth\n    ax[1].set_title(\"Step 1: Raw Column-wise Extraction (Note NaNs detected as gaps)\")\n    # Normalize GT to [-1, 1] for visual comparison\n    gt_norm = (gt_signal - gt_signal.min()) / (gt_signal.max() - gt_signal.min()) * 2 - 1\n    ax[1].plot(gt_norm, 'g-', label='Ground Truth Math', alpha=0.5)\n    ax[1].plot(raw_extracted, 'rx', markersize=2, label='Extracted Points (NaNs missing)')\n    ax[1].legend(loc='upper right')\n    ax[1].set_ylim(-1.2, 1.2)\n    \n    # Plot 3: Final Interpolated Output\n    ax[2].set_title(\"Step 2: Final Interpolated & Smoothed Signal\")\n    ax[2].plot(gt_norm, 'g-', linewidth=2, label='Ground Truth', alpha=0.6)\n    ax[2].plot(final_signal, 'b--', linewidth=1, label='Final Reconstructed Signal')\n    ax[2].legend(loc='upper right')\n    ax[2].set_ylim(-1.2, 1.2)\n    \n    plt.tight_layout()\n    plt.show()\n    \n    # Simple metric check (Mean Squared Error on valid points)\n    valid_mask = ~np.isnan(raw_extracted)\n    mse = np.mean((gt_norm[valid_mask] - final_signal[valid_mask])**2)\n    print(f\"Reconstruction MSE on valid segments: {mse:.6f}\")\n\nif __name__ == \"__main__\":\n    # Import needed here for the smoothing function in the demo\n    import scipy.signal as signal \n    run_extraction_pipeline()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-08T02:30:01.053330Z","iopub.execute_input":"2025-12-08T02:30:01.053537Z","iopub.status.idle":"2025-12-08T02:30:01.450952Z","shell.execute_reply.started":"2025-12-08T02:30:01.053517Z","shell.execute_reply":"2025-12-08T02:30:01.450085Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Example Resampling Snippet (to be used in final submission loop)\nfrom scipy.interpolate import interp1d\n\ndef resample_signal(signal_array, target_length):\n    current_length = len(signal_array)\n    x_current = np.linspace(0, 1, current_length)\n    x_target = np.linspace(0, 1, target_length)\n    \n    # Create interpolator function\n    f = interp1d(x_current, signal_array, kind='cubic')\n    \n    # Generate new resampled array\n    return f(x_target)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-08T02:30:01.451640Z","iopub.execute_input":"2025-12-08T02:30:01.451822Z","iopub.status.idle":"2025-12-08T02:30:01.456461Z","shell.execute_reply.started":"2025-12-08T02:30:01.451802Z","shell.execute_reply":"2025-12-08T02:30:01.455791Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport numpy as np\nimport pandas as pd\nimport cv2\nimport scipy.interpolate as interp\nfrom tqdm import tqdm\nimport gc\n\n# ==========================================\n# 1. CONFIGURATION\n# ==========================================\nSUBMISSION_CONFIG = {\n    \"test_dir\": \"lead_detection_data\", \n    \"output_file\": \"submission.csv\",\n    \"fs\": 500,               # 500 Hz\n    \"output_duration\": 10.0, # 10 seconds\n    \"mv_scale_factor\": 2.0,  # Calibration\n    # Exact lead order is critical\n    \"leads_order\": ['I', 'II', 'III', 'aVR', 'aVL', 'aVF', 'V1', 'V2', 'V3', 'V4', 'V5', 'V6']\n}\n\n# ==========================================\n# 2. HELPER FUNCTIONS\n# ==========================================\ndef process_1d_signal(raw_signal, target_len, mv_scale):\n    # 1. Fill NaNs\n    if np.isnan(raw_signal).all(): return np.zeros(target_len)\n    \n    nans = np.isnan(raw_signal)\n    not_nans = ~nans\n    if np.sum(not_nans) < 2: return np.zeros(target_len)\n    \n    x_idxs = np.arange(len(raw_signal))\n    raw_signal[nans] = np.interp(x_idxs[nans], x_idxs[not_nans], raw_signal[not_nans])\n    \n    # 2. Resample\n    current_len = len(raw_signal)\n    if current_len != target_len:\n        f = interp.interp1d(np.linspace(0, 1, current_len), raw_signal, kind='linear', fill_value=\"extrapolate\")\n        resampled_signal = f(np.linspace(0, 1, target_len))\n    else:\n        resampled_signal = raw_signal\n        \n    return resampled_signal * mv_scale\n\n# ==========================================\n# 3. MAIN GENERATOR (Chunked Writing)\n# ==========================================\ndef generate_submission():\n    # A. Initialize CSV with Headers\n    # We write headers first, then append data row by row to save RAM\n    with open(SUBMISSION_CONFIG['output_file'], 'w') as f:\n        f.write(\"id,value\\n\")\n    \n    print(f\"initialized {SUBMISSION_CONFIG['output_file']}...\")\n    \n    # Get sorted file list to keep submission organized\n    test_files = sorted([f for f in os.listdir(SUBMISSION_CONFIG[\"test_dir\"]) if f.endswith(('.png', '.jpg'))])\n    \n    # Model placeholders (Replace with your actual loaded models)\n    # ---------------------------------------------------------\n    # model_roi = YourTrainedROIModel() \n    # model_seg = YourTrainedUNet()\n    # ---------------------------------------------------------\n\n    target_samples = int(SUBMISSION_CONFIG[\"output_duration\"] * SUBMISSION_CONFIG[\"fs\"])\n    \n    print(f\"Processing {len(test_files)} images...\")\n    \n    for filename in tqdm(test_files):\n        record_id = filename.split('.')[0]\n        img_path = os.path.join(SUBMISSION_CONFIG[\"test_dir\"], filename)\n        img = cv2.imread(img_path)\n        \n        if img is None: continue\n        \n        # --- LOGIC: ROTATION CHECK ---\n        # \"Hidden Test Set\" often has rotated images. \n        # If Height > Width, it's likely rotated 90 deg.\n        if img.shape[0] > img.shape[1]:\n            img = cv2.rotate(img, cv2.ROTATE_90_CLOCKWISE)\n\n        H, W, _ = img.shape\n\n        # --- LOGIC: ROI PREDICTION ---\n        # (Using simple grid logic for demo, replace with Task 2.1 Inference)\n        # We assume 3x4 layout\n        boxes = []\n        box_w, box_h = W // 4, H // 3\n        for r in range(3):\n            for c in range(4):\n                boxes.append([c*box_w, r*box_h, (c+1)*box_w, (r+1)*box_h])\n        \n        # --- PROCESS LEADS ---\n        batch_rows = [] # Buffer for this image\n        \n        for idx, lead_name in enumerate(SUBMISSION_CONFIG[\"leads_order\"]):\n            if idx >= len(boxes): break\n            \n            # Crop\n            x1, y1, x2, y2 = boxes[idx]\n            crop = img[y1:y2, x1:x2]\n            \n            # Segment (Task 1.2 Logic placeholder)\n            gray = cv2.cvtColor(crop, cv2.COLOR_BGR2GRAY)\n            _, mask = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY_INV) # Simple invert\n            \n            # Extract\n            col_means = []\n            if mask.shape[1] > 0:\n                for c in range(mask.shape[1]):\n                    indices = np.where(mask[:, c] > 127)[0]\n                    if len(indices) > 0:\n                        # Normalize 0..H to 1..-1\n                        norm_y = -1.0 * ((np.mean(indices) - (mask.shape[0]/2)) / (mask.shape[0]/2))\n                        col_means.append(norm_y)\n                    else:\n                        col_means.append(np.nan)\n                raw_signal = np.array(col_means)\n            else:\n                raw_signal = np.zeros(10) # Fallback\n\n            # Resample\n            final_signal = process_1d_signal(raw_signal, target_samples, SUBMISSION_CONFIG[\"mv_scale_factor\"])\n            \n            # --- CRITICAL FIX: STRING FORMATTING ---\n            # Format: '62_0_I' (With literal single quotes)\n            for t, val in enumerate(final_signal):\n                # We format the ID with single quotes INSIDE the string\n                row_id = f\"'{record_id}_{t}_{lead_name}'\"\n                \n                # Check for float validity\n                val_float = float(val) if not np.isnan(val) else 0.0\n                \n                batch_rows.append(f\"{row_id},{val_float:.4f}\\n\")\n        \n        # Write batch to disk immediately to free memory\n        with open(SUBMISSION_CONFIG['output_file'], 'a') as f:\n            f.writelines(batch_rows)\n            \n        # Clean up\n        del batch_rows, img, boxes\n        gc.collect()\n\n    print(\"Submission generation complete.\")\n\nif __name__ == \"__main__\":\n    generate_submission()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-08T02:30:01.457107Z","iopub.execute_input":"2025-12-08T02:30:01.457294Z","iopub.status.idle":"2025-12-08T02:30:16.598491Z","shell.execute_reply.started":"2025-12-08T02:30:01.457277Z","shell.execute_reply":"2025-12-08T02:30:16.597542Z"}},"outputs":[],"execution_count":null}]}