{"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":true,"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},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nimport seaborn as sns\nfrom pathlib import Path\nimport cv2\nfrom PIL import Image\nfrom scipy import signal as sp_signal\nfrom scipy import interpolate\nfrom scipy.ndimage import gaussian_filter1d\nfrom tqdm.notebook import tqdm\nimport warnings\nwarnings.filterwarnings('ignore')\n\n# Config\nplt.style.use('seaborn-v0_8-darkgrid')\nsns.set_palette(\"husl\")\n\n# Paths\nDATA_PATH = Path('/kaggle/input/physionet-ecg-image-digitization')\nTRAIN_PATH = DATA_PATH / 'train'\nTEST_PATH = DATA_PATH / 'test'\n\nprint(\"🚀 PhysioNet ECG - BASELINE V3: Adaptive Detection\")\nprint(\"=\" * 70)\nprint(f\"📁 Data loaded: {DATA_PATH.exists()}\")\n\n# Load metadata\ntrain_meta = pd.read_csv(DATA_PATH / 'train.csv')\ntest_meta = pd.read_csv(DATA_PATH / 'test.csv')\n\nprint(f\"📊 Train samples: {len(train_meta)}\")\nprint(f\"📊 Test samples: {len(test_meta['id'].unique())}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-23T15:49:23.922006Z","iopub.execute_input":"2025-10-23T15:49:23.922551Z","iopub.status.idle":"2025-10-23T15:49:25.038322Z","shell.execute_reply.started":"2025-10-23T15:49:23.922524Z","shell.execute_reply":"2025-10-23T15:49:25.037249Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"CALIBRATION = {\n    'px_per_mm': 39,\n    'px_per_second': 975,\n    'px_per_mv': 390,\n}\n\nECG_LEADS_ORDER = ['I', 'aVR', 'V1', 'V4', 'II', 'aVL', 'V2', 'V5', \n                   'III', 'aVF', 'V3', 'V6']\n\nprint(\"\\n\" + \"=\"*70)\nprint(\"⚙️  CONFIGURATION\")\nprint(\"=\"*70)\nprint(f\"📐 Calibration: {CALIBRATION['px_per_mv']} px/mV\")\nprint(f\"📋 Lead order: {ECG_LEADS_ORDER}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-23T15:49:31.898024Z","iopub.execute_input":"2025-10-23T15:49:31.898379Z","iopub.status.idle":"2025-10-23T15:49:31.904554Z","shell.execute_reply.started":"2025-10-23T15:49:31.898354Z","shell.execute_reply":"2025-10-23T15:49:31.903545Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(\"\\n\" + \"=\"*70)\nprint(\"🔧 STEP 1: ROBUST DESKEW\")\nprint(\"=\"*70)\n\ndef detect_skew_angle(img, visualize=False):\n    \"\"\"\n    Detect rotation angle using Hough Line Transform\n    \n    Args:\n        img: Grayscale image\n        visualize: Show detection process\n    \n    Returns:\n        angle: Rotation angle in degrees\n        debug_info: Dict with visualization data\n    \"\"\"\n    # Edge detection\n    edges = cv2.Canny(img, 50, 150, apertureSize=3)\n    \n    # Hough Line Transform\n    lines = cv2.HoughLines(edges, 1, np.pi/180, threshold=200)\n    \n    if lines is None:\n        return 0.0, {'edges': edges, 'lines': []}\n    \n    # Extract angles\n    angles = []\n    line_coords = []\n    \n    for rho, theta in lines[:, 0]:\n        angle = np.degrees(theta) - 90\n        # Keep only near-horizontal lines\n        if -10 < angle < 10:\n            angles.append(angle)\n            # Store line coordinates for visualization\n            a = np.cos(theta)\n            b = np.sin(theta)\n            x0 = a * rho\n            y0 = b * rho\n            line_coords.append((x0, y0, theta))\n    \n    # Calculate median angle (robust to outliers)\n    median_angle = np.median(angles) if len(angles) > 0 else 0.0\n    \n    debug_info = {\n        'edges': edges,\n        'lines': line_coords,\n        'angles': angles,\n        'median_angle': median_angle\n    }\n    \n    return median_angle, debug_info\n\n\ndef deskew_image(img, angle, min_angle_threshold=1.0):\n    \"\"\"\n    Rotate image to correct skew ONLY if significant\n    \n    Args:\n        img: Input image\n        angle: Rotation angle in degrees\n        min_angle_threshold: Minimum angle to bother rotating\n    \n    Returns:\n        Deskewed image (or original if angle too small)\n    \"\"\"\n    # Don't rotate if angle is negligible\n    if abs(angle) < min_angle_threshold:\n        print(f\"   ℹ️  Skew angle ({angle:.3f}°) below threshold, keeping original\")\n        return img\n    \n    h, w = img.shape[:2]\n    center = (w // 2, h // 2)\n    M = cv2.getRotationMatrix2D(center, angle, 1.0)\n    \n    # Calculate new image size to avoid cropping\n    cos = np.abs(M[0, 0])\n    sin = np.abs(M[0, 1])\n    new_w = int((h * sin) + (w * cos))\n    new_h = int((h * cos) + (w * sin))\n    \n    # Adjust transformation matrix\n    M[0, 2] += (new_w / 2) - center[0]\n    M[1, 2] += (new_h / 2) - center[1]\n    \n    deskewed = cv2.warpAffine(img, M, (new_w, new_h),\n                              flags=cv2.INTER_LINEAR,\n                              borderMode=cv2.BORDER_CONSTANT,\n                              borderValue=255)\n    \n    print(f\"   ✅ Rotated {angle:.3f}° to correct skew\")\n    return deskewed\n\n\ndef visualize_deskew(img_original, img_deskewed, debug_info):\n    \"\"\"Visualize deskew process\"\"\"\n    fig, axes = plt.subplots(2, 2, figsize=(16, 10))\n    \n    # Original\n    axes[0, 0].imshow(img_original, cmap='gray')\n    axes[0, 0].set_title('1. Original Image', fontweight='bold', fontsize=12)\n    axes[0, 0].axis('off')\n    \n    # Edges\n    axes[0, 1].imshow(debug_info['edges'], cmap='gray')\n    axes[0, 1].set_title('2. Edge Detection (Canny)', fontweight='bold', fontsize=12)\n    axes[0, 1].axis('off')\n    \n    # Detected lines\n    img_lines = cv2.cvtColor(img_original, cv2.COLOR_GRAY2BGR)\n    for x0, y0, theta in debug_info['lines'][:20]:  # Show first 20 lines\n        length = 1000\n        x1 = int(x0 + length * (-np.sin(theta)))\n        y1 = int(y0 + length * (np.cos(theta)))\n        x2 = int(x0 - length * (-np.sin(theta)))\n        y2 = int(y0 - length * (np.cos(theta)))\n        cv2.line(img_lines, (x1, y1), (x2, y2), (0, 255, 0), 2)\n    \n    axes[1, 0].imshow(img_lines)\n    axes[1, 0].set_title(f'3. Detected Lines (angle: {debug_info[\"median_angle\"]:.2f}°)', \n                        fontweight='bold', fontsize=12)\n    axes[1, 0].axis('off')\n    \n    # Deskewed\n    axes[1, 1].imshow(img_deskewed, cmap='gray')\n    axes[1, 1].set_title('4. Deskewed Image', fontweight='bold', fontsize=12)\n    axes[1, 1].axis('off')\n    \n    plt.tight_layout()\n    plt.show()\n    \n    print(f\"✅ Detected skew: {debug_info['median_angle']:.3f}°\")\n    print(f\"   Original size: {img_original.shape}\")\n    print(f\"   Deskewed size: {img_deskewed.shape}\")\n\nprint(\"✅ Deskew functions defined\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-23T15:56:31.628317Z","iopub.execute_input":"2025-10-23T15:56:31.628563Z","iopub.status.idle":"2025-10-23T15:56:31.652564Z","shell.execute_reply.started":"2025-10-23T15:56:31.628543Z","shell.execute_reply":"2025-10-23T15:56:31.651573Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(\"\\n\" + \"=\"*70)\nprint(\"🔧 STEP 2: ADAPTIVE ROW DETECTION\")\nprint(\"=\"*70)\n\ndef detect_content_start(img, threshold=0.03):\n    \"\"\"\n    Detect where ECG content starts (skip header region)\n    \n    Args:\n        img: Grayscale image\n        threshold: Min ratio of dark pixels to consider as content\n    \n    Returns:\n        content_start_y: Y-coordinate where content begins\n        debug_info: Visualization data\n    \"\"\"\n    h, w = img.shape\n    \n    # Analyze each horizontal strip (10px high)\n    strip_height = 10\n    n_strips = h // strip_height\n    \n    content_ratios = []\n    strip_positions = []\n    \n    for i in range(n_strips):\n        y_start = i * strip_height\n        y_end = min((i + 1) * strip_height, h)\n        strip = img[y_start:y_end, :]\n        \n        # Calculate ratio of dark pixels (ECG signal)\n        dark_pixels = np.sum(strip < 200)\n        total_pixels = strip.size\n        dark_ratio = dark_pixels / total_pixels\n        \n        content_ratios.append(dark_ratio)\n        strip_positions.append(y_start)\n    \n    content_ratios = np.array(content_ratios)\n    strip_positions = np.array(strip_positions)\n    \n    # Find first strip with significant content\n    content_mask = content_ratios > threshold\n    \n    if np.any(content_mask):\n        first_content_idx = np.argmax(content_mask)\n        content_start_y = strip_positions[first_content_idx]\n    else:\n        content_start_y = 0\n    \n    debug_info = {\n        'content_ratios': content_ratios,\n        'strip_positions': strip_positions,\n        'threshold': threshold,\n        'content_start': content_start_y\n    }\n    \n    return content_start_y, debug_info\n\n\ndef detect_and_filter_text_regions(img):\n    \"\"\"\n    Detect and mask text regions (labels like 'aVR', 'V1', etc.)\n    Text has different characteristics than ECG signals\n    \n    Args:\n        img: Grayscale image\n    \n    Returns:\n        mask: Binary mask (0 = text, 255 = keep)\n    \"\"\"\n    # Text detection using morphological operations\n    # Text has small, dense connected components\n    \n    # Threshold to get dark pixels\n    _, binary = cv2.threshold(img, 150, 255, cv2.THRESH_BINARY_INV)\n    \n    # Find connected components\n    n_labels, labels, stats, centroids = cv2.connectedComponentsWithStats(binary, connectivity=8)\n    \n    # Create mask (start with all white = keep everything)\n    mask = np.ones_like(img) * 255\n    \n    # Filter out small, compact regions (likely text)\n    for i in range(1, n_labels):  # Skip background (0)\n        x, y, w, h, area = stats[i]\n        \n        # Text characteristics:\n        # - Small area (< 500 pixels)\n        # - Compact shape (w and h both small)\n        # - Usually in margins (x < 200 or x > img.shape[1] - 200)\n        \n        aspect_ratio = max(w, h) / (min(w, h) + 1)\n        \n        is_likely_text = (\n            area < 500 and                    # Small area\n            w < 80 and h < 50 and            # Compact size\n            aspect_ratio < 5                  # Not too elongated (ECG is very elongated)\n        )\n        \n        if is_likely_text:\n            # Mark this region as text (set to 0 in mask)\n            mask[labels == i] = 0\n    \n    return mask\n\n\ndef detect_rows_smart_v2(img, n_expected_rows=4):\n    \"\"\"\n    Improved smart row detection with text filtering\n    \n    Args:\n        img: Deskewed image\n        n_expected_rows: Expected number of content rows (typically 3-4)\n    \n    Returns:\n        rows: List of (y_start, y_end, row_type)\n        debug_info: Visualization data\n    \"\"\"\n    h, w = img.shape\n    \n    # Step 1: Filter out text regions\n    text_mask = detect_and_filter_text_regions(img)\n    img_filtered = cv2.bitwise_and(img, img, mask=text_mask)\n    \n    # Step 2: Find where content starts (skip header)\n    content_start, debug_content = detect_content_start(img_filtered, threshold=0.03)\n    \n    # Step 3: Analyze content region WITHOUT text\n    content_img = img_filtered[content_start:, :]\n    content_h = content_img.shape[0]\n    \n    # Project to y-axis\n    y_projection = np.mean(content_img, axis=1)\n    y_projection_smooth = gaussian_filter1d(y_projection, sigma=30)  # Heavy smoothing\n    \n    # Invert\n    y_projection_inverted = 255 - y_projection_smooth\n    y_proj_norm = (y_projection_inverted - y_projection_inverted.min()) / \\\n                  (y_projection_inverted.max() - y_projection_inverted.min())\n    \n    # Find peaks with very strict parameters\n    from scipy.signal import find_peaks\n    peaks, properties = find_peaks(y_proj_norm, \n                                   distance=200,      # Very far apart\n                                   prominence=0.2,    # Very prominent\n                                   width=100)         # Very wide\n    \n    # If we got too many or too few peaks, use proportional division\n    if len(peaks) < 2 or len(peaks) > 5:\n        print(f\"   ⚠️  Peak detection gave {len(peaks)} peaks, using proportional division instead\")\n        \n        # Use proportional division based on expected structure\n        # Typically: 3 equal rows + 1 larger rhythm strip\n        # Or: 3 equal rows (if rhythm integrated)\n        \n        if n_expected_rows == 4:\n            # 3 rows of ~20% each + 1 rhythm of ~40%\n            proportions = [0.20, 0.20, 0.20, 0.40]\n        else:\n            # 3 equal rows\n            proportions = [1/3, 1/3, 1/3]\n        \n        boundaries = [0]\n        cumulative = 0\n        for prop in proportions:\n            cumulative += prop\n            boundaries.append(int(content_h * cumulative))\n        boundaries[-1] = content_h  # Ensure last goes to end\n        \n        rows = []\n        for i in range(len(boundaries) - 1):\n            y_start = content_start + boundaries[i]\n            y_end = content_start + boundaries[i + 1]\n            row_height = y_end - y_start\n            \n            # Last row is typically rhythm if it's significantly larger\n            if i == len(boundaries) - 2 and row_height > content_h * 0.35:\n                row_type = 'rhythm'\n            else:\n                row_type = 'standard'\n            \n            rows.append((y_start, y_end, row_type))\n    \n    else:\n        # Use detected peaks\n        boundaries = [0]\n        for i in range(len(peaks) - 1):\n            valley_region = y_proj_norm[peaks[i]:peaks[i+1]]\n            valley_idx = np.argmin(valley_region)\n            valley_pos = peaks[i] + valley_idx\n            boundaries.append(valley_pos)\n        boundaries.append(content_h)\n        \n        rows = []\n        for i in range(len(boundaries) - 1):\n            y_start = content_start + boundaries[i]\n            y_end = content_start + boundaries[i + 1]\n            row_height = y_end - y_start\n            \n            # Determine row type\n            if row_height > content_h * 0.35:\n                row_type = 'rhythm'\n            else:\n                row_type = 'standard'\n            \n            rows.append((y_start, y_end, row_type))\n    \n    debug_info = {\n        'content_start': content_start,\n        'y_projection': y_proj_norm,\n        'peaks': peaks,\n        'content_ratios': debug_content['content_ratios'],\n        'strip_positions': debug_content['strip_positions'],\n        'text_mask': text_mask\n    }\n    \n    return rows, debug_info\n\n\ndef visualize_row_detection(img, rows, debug_info):\n    \"\"\"Visualize smart row detection process\"\"\"\n    fig = plt.figure(figsize=(20, 10))\n    gs = fig.add_gridspec(2, 3, hspace=0.3, wspace=0.3)\n    \n    # 1. Content detection\n    ax1 = fig.add_subplot(gs[0, 0])\n    strip_positions = debug_info['strip_positions']\n    content_ratios = debug_info['content_ratios']\n    content_start = debug_info['content_start']\n    \n    ax1.plot(content_ratios, strip_positions, linewidth=2, color='blue')\n    ax1.axhline(content_start, color='red', linestyle='--', linewidth=2, \n               label=f'Content starts at y={content_start}')\n    ax1.invert_yaxis()\n    ax1.set_xlabel('Content Ratio', fontweight='bold')\n    ax1.set_ylabel('Y Position (pixels)', fontweight='bold')\n    ax1.set_title('1. Header Detection', fontweight='bold')\n    ax1.legend()\n    ax1.grid(alpha=0.3)\n    \n    # 2. Y-projection with peaks\n    ax2 = fig.add_subplot(gs[0, 1])\n    y_proj = debug_info['y_projection']\n    peaks = debug_info['peaks']\n    \n    y_positions = np.arange(len(y_proj)) + content_start\n    ax2.plot(y_proj, y_positions, linewidth=2, color='darkgreen')\n    \n    # Mark peaks\n    if len(peaks) > 0:\n        peak_positions = peaks + content_start\n        peak_values = y_proj[peaks]\n        ax2.plot(peak_values, peak_positions, 'ro', markersize=10, \n                label=f'{len(peaks)} peaks detected')\n    \n    ax2.invert_yaxis()\n    ax2.set_xlabel('Content Density (normalized)', fontweight='bold')\n    ax2.set_ylabel('Y Position (pixels)', fontweight='bold')\n    ax2.set_title('2. Content Density Analysis', fontweight='bold')\n    ax2.legend()\n    ax2.grid(alpha=0.3)\n    \n    # 3. Detected rows overlaid\n    ax3 = fig.add_subplot(gs[0, 2])\n    img_rows = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)\n    \n    colors = {\n        'standard': (0, 255, 0),  # Green\n        'rhythm': (255, 0, 0)     # Red\n    }\n    \n    for idx, (y_start, y_end, row_type) in enumerate(rows):\n        color = colors.get(row_type, (0, 255, 255))\n        cv2.rectangle(img_rows, (0, y_start), (img.shape[1], y_end), color, 3)\n        \n        label = f'Row {idx+1} ({row_type})'\n        cv2.putText(img_rows, label, (10, y_start + 30),\n                   cv2.FONT_HERSHEY_SIMPLEX, 0.8, color, 2)\n    \n    # Mark content start\n    cv2.line(img_rows, (0, content_start), (img.shape[1], content_start), \n            (0, 0, 255), 2)\n    cv2.putText(img_rows, 'Content Start', (10, content_start - 10),\n               cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 0, 255), 2)\n    \n    ax3.imshow(img_rows)\n    ax3.set_title(f'3. Detected Rows ({len(rows)})', fontweight='bold')\n    ax3.axis('off')\n    \n    # 4. Individual rows (bottom row, spans all columns)\n    ax4 = fig.add_subplot(gs[1, :])\n    \n    # Create composite showing all rows side by side\n    row_heights = [y_end - y_start for y_start, y_end, _ in rows]\n    max_height = max(row_heights)\n    \n    composite_width = 0\n    row_images = []\n    \n    for y_start, y_end, row_type in rows:\n        row_img = img[y_start:y_end, :]\n        # Resize to same height for display\n        scale = max_height / row_img.shape[0]\n        new_width = int(row_img.shape[1] * scale)\n        row_resized = cv2.resize(row_img, (new_width, max_height))\n        row_images.append(row_resized)\n        composite_width += new_width\n    \n    # Concatenate horizontally\n    composite = np.hstack(row_images)\n    \n    ax4.imshow(composite, cmap='gray')\n    ax4.set_title('4. All Rows (normalized height for comparison)', fontweight='bold')\n    ax4.axis('off')\n    \n    plt.tight_layout()\n    plt.show()\n    \n    print(f\"\\n✅ Smart Row Detection Summary:\")\n    print(f\"   Header region: y=0-{content_start}\")\n    print(f\"   Content region: y={content_start}-{img.shape[0]}\")\n    print(f\"   Rows detected: {len(rows)}\")\n    for idx, (y_start, y_end, row_type) in enumerate(rows):\n        height = y_end - y_start\n        print(f\"   Row {idx+1} ({row_type:>8}): y={y_start:>4}-{y_end:>4} (height: {height:>3}px)\")\n    \n    # Calculate expected number of leads\n    standard_rows = sum(1 for _, _, t in rows if t == 'standard')\n    rhythm_rows = sum(1 for _, _, t in rows if t == 'rhythm')\n    expected_leads = standard_rows * 4 + rhythm_rows\n    print(f\"\\n📊 Expected structure:\")\n    print(f\"   Standard rows: {standard_rows} (4 leads each = {standard_rows * 4} leads)\")\n    print(f\"   Rhythm strips: {rhythm_rows}\")\n    print(f\"   Total leads to extract: {expected_leads}\")\n\n\nprint(\"✅ Row detection functions defined\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-23T15:56:31.653542Z","iopub.execute_input":"2025-10-23T15:56:31.653794Z","iopub.status.idle":"2025-10-23T15:56:31.691185Z","shell.execute_reply.started":"2025-10-23T15:56:31.653770Z","shell.execute_reply":"2025-10-23T15:56:31.690185Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(\"\\n\" + \"=\"*70)\nprint(\"🔧 STEP 3: COLUMN SEGMENTATION\")\nprint(\"=\"*70)\n\ndef crop_lead_region_smart(lead_img, margin_top=50, margin_sides=20):\n    \"\"\"\n    Crop lead region to remove labels and margins\n    \n    Labels are typically in top-left of each cell, so we crop:\n    - Top: Remove first 50px (where labels like \"I\", \"aVR\" are)\n    - Sides: Remove 20px each side (margins)\n    \n    Args:\n        lead_img: Image of a single lead region\n        margin_top: Pixels to crop from top (remove labels)\n        margin_sides: Pixels to crop from each side\n    \n    Returns:\n        Cropped image with only ECG signal\n    \"\"\"\n    h, w = lead_img.shape\n    \n    # Ensure margins don't exceed image size\n    margin_top = min(margin_top, h // 4)\n    margin_sides = min(margin_sides, w // 10)\n    \n    # Crop\n    cropped = lead_img[margin_top:h-5, margin_sides:w-margin_sides]\n    \n    return cropped\n\n\ndef segment_row_into_leads(row_img, n_leads=4):\n    \"\"\"\n    Segment a row into individual lead columns WITH smart cropping\n    \n    Args:\n        row_img: Image of a single row\n        n_leads: Expected number of leads in this row\n    \n    Returns:\n        lead_regions: List of (x_start, x_end, cropped_img)\n    \"\"\"\n    h, w = row_img.shape\n    lead_width = w // n_leads\n    \n    regions = []\n    for i in range(n_leads):\n        x_start = i * lead_width\n        x_end = (i + 1) * lead_width if i < n_leads - 1 else w\n        \n        # Extract lead region\n        lead_img = row_img[:, x_start:x_end]\n        \n        # Smart crop to remove labels\n        lead_cropped = crop_lead_region_smart(lead_img, margin_top=50, margin_sides=20)\n        \n        regions.append((x_start, x_end, lead_cropped))\n    \n    return regions\n\n\ndef visualize_lead_segmentation(img, rows, lead_assignments):\n    \"\"\"Visualize final lead segmentation WITH cropped regions\"\"\"\n    fig, axes = plt.subplots(2, 1, figsize=(20, 14))\n    \n    # Top: Full image with bounding boxes\n    img_vis = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)\n    colors = [(255, 0, 0), (0, 255, 0), (0, 0, 255), (255, 255, 0)]\n    \n    for row_idx, (y_start, y_end, row_type) in enumerate(rows):\n        if row_idx >= len(lead_assignments):\n            continue\n            \n        n_leads, lead_names = lead_assignments[row_idx]\n        row_img = img[y_start:y_end, :]\n        lead_regions = segment_row_into_leads(row_img, n_leads=n_leads)\n        \n        for col_idx, (x_start, x_end, lead_cropped) in enumerate(lead_regions):\n            if col_idx >= len(lead_names):\n                continue\n                \n            color = colors[col_idx % len(colors)]\n            \n            # Draw FULL region box\n            cv2.rectangle(img_vis, (x_start, y_start), (x_end, y_end), color, 2)\n            \n            # Draw CROPPED region box (inner)\n            margin_top = 50\n            margin_sides = 20\n            cv2.rectangle(img_vis, \n                         (x_start + margin_sides, y_start + margin_top), \n                         (x_end - margin_sides, y_end - 5), \n                         (0, 255, 255), 2)  # Cyan for cropped region\n            \n            # Add label\n            lead_name = lead_names[col_idx]\n            cv2.putText(img_vis, lead_name, (x_start + 10, y_start + 25),\n                       cv2.FONT_HERSHEY_SIMPLEX, 0.7, color, 2)\n            cv2.putText(img_vis, '(cropped)', (x_start + 10, y_end - 10),\n                       cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 255), 1)\n    \n    axes[0].imshow(img_vis)\n    axes[0].set_title('Full Segmentation (Colored boxes = full region, Cyan boxes = cropped signal area)', \n                     fontweight='bold', fontsize=12)\n    axes[0].axis('off')\n    \n    # Bottom: Show some cropped examples\n    # Take first 4 leads from row 1 as examples\n    if len(rows) > 0 and len(lead_assignments) > 0:\n        y_start, y_end, _ = rows[0]\n        n_leads, lead_names = lead_assignments[0]\n        row_img = img[y_start:y_end, :]\n        lead_regions = segment_row_into_leads(row_img, n_leads=min(4, n_leads))\n        \n        # Concatenate cropped leads horizontally\n        cropped_leads = [lead_cropped for _, _, lead_cropped in lead_regions[:4]]\n        if cropped_leads:\n            composite = np.hstack(cropped_leads)\n            axes[1].imshow(composite, cmap='gray')\n            axes[1].set_title('Example: Cropped Lead Regions (Row 1, first 4 leads) - Labels removed', \n                             fontweight='bold', fontsize=12)\n            axes[1].axis('off')\n    \n    plt.tight_layout()\n    plt.show()\n    \n    print(\"\\n💡 Cropping Strategy:\")\n    print(\"  - Top margin: 50px removed (eliminates labels like 'I', 'aVR', etc.)\")\n    print(\"  - Side margins: 20px each side removed\")\n    print(\"  - Bottom margin: 5px removed\")\n    print(\"  - Cyan boxes show actual signal extraction area\")\n\nprint(\"✅ Column segmentation functions defined\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-23T15:56:31.692945Z","iopub.execute_input":"2025-10-23T15:56:31.693352Z","iopub.status.idle":"2025-10-23T15:56:31.720473Z","shell.execute_reply.started":"2025-10-23T15:56:31.693321Z","shell.execute_reply":"2025-10-23T15:56:31.719412Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(\"\\n\" + \"=\"*70)\nprint(\"🔄 COMPLETE ADAPTIVE PIPELINE\")\nprint(\"=\"*70)\n\ndef preprocess_adaptive(image_path, target_size=(1400, 2000), visualize=True):\n    \"\"\"\n    Complete adaptive preprocessing pipeline\n    \n    Returns:\n        dict with processed image and metadata\n    \"\"\"\n    # Load\n    img = cv2.imread(str(image_path), cv2.IMREAD_GRAYSCALE)\n    if img is None:\n        img = np.array(Image.open(image_path).convert('L'))\n    \n    # Resize\n    if target_size:\n        img = cv2.resize(img, (target_size[1], target_size[0]), \n                        interpolation=cv2.INTER_AREA)\n    \n    print(\"\\n🔧 STEP 1: DESKEWING...\")\n    # Detect and correct skew\n    angle, debug_skew = detect_skew_angle(img)\n    img_deskewed = deskew_image(img, angle)\n    \n    if visualize:\n        visualize_deskew(img, img_deskewed, debug_skew)\n    \n    print(\"\\n🔧 STEP 2: ROW DETECTION...\")\n    # Detect rows with improved smart method (v2 with text filtering)\n    rows, debug_rows = detect_rows_smart_v2(img_deskewed, n_expected_rows=4)\n    \n    if visualize:\n        visualize_row_detection(img_deskewed, rows, debug_rows)\n    \n    return {\n        'original': img,\n        'deskewed': img_deskewed,\n        'rows': rows,  # Now includes row_type\n        'calibration': CALIBRATION,\n        'skew_angle': angle\n    }\n\nprint(\"✅ Adaptive pipeline defined\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-23T15:56:31.721494Z","iopub.execute_input":"2025-10-23T15:56:31.721744Z","iopub.status.idle":"2025-10-23T15:56:31.743191Z","shell.execute_reply.started":"2025-10-23T15:56:31.721725Z","shell.execute_reply":"2025-10-23T15:56:31.742303Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(\"\\n\" + \"=\"*70)\nprint(\"🧪 TESTING ADAPTIVE PIPELINE\")\nprint(\"=\"*70)\n\nsample_id = train_meta['id'].iloc[0]\nsample_img_path = TRAIN_PATH / str(sample_id) / f\"{sample_id}-0001.png\"\n\nprint(f\"Testing on: {sample_id}\")\nprint(f\"Image: {sample_img_path.name}\")\n\nif sample_img_path.exists():\n    print(\"\\n\" + \"=\"*70)\n    print(\"🎬 RUNNING ADAPTIVE PIPELINE WITH VISUALIZATION\")\n    print(\"=\"*70)\n    \n    # Run adaptive preprocessing\n    result = preprocess_adaptive(sample_img_path, visualize=True)\n    \n    print(\"\\n✅ Adaptive preprocessing complete!\")\n    print(f\"\\n📊 Results:\")\n    print(f\"   Skew corrected: {result['skew_angle']:.3f}°\")\n    print(f\"   Content rows detected: {len(result['rows'])}\")\n    \n    # Visualize lead assignment (based on detected rows)\n    print(\"\\n🔧 STEP 3: LEAD ASSIGNMENT...\")\n    \n    # Determine lead assignment based on number of rows\n    n_rows = len(result['rows'])\n    \n    if n_rows == 3:\n        # Standard: 2 rows of 4 leads + 1 rhythm strip\n        lead_assignments = [\n            (4, ['I', 'aVR', 'V1', 'V4']),\n            (4, ['II', 'aVL', 'V2', 'V5']),\n            (4, ['III', 'aVF', 'V3', 'V6']),\n        ]\n    elif n_rows == 4:\n        # With rhythm strip separate\n        lead_assignments = [\n            (4, ['I', 'aVR', 'V1', 'V4']),\n            (4, ['II', 'aVL', 'V2', 'V5']),\n            (4, ['III', 'aVF', 'V3', 'V6']),\n            (1, ['II_rhythm'])\n        ]\n    else:\n        print(f\"⚠️  Unexpected number of rows: {n_rows}\")\n        lead_assignments = []\n    \n    if lead_assignments:\n        print(f\"\\n📋 Lead Assignment Plan:\")\n        for row_idx, (n_leads, names) in enumerate(lead_assignments):\n            print(f\"   Row {row_idx+1}: {n_leads} leads → {names}\")\n        \n        visualize_lead_segmentation(result['deskewed'], result['rows'], lead_assignments)\n\nprint(\"\\n\" + \"=\"*70)\nprint(\"✨ ADAPTIVE DETECTION V3 READY\")\nprint(\"=\"*70)\nprint(\"\\n💡 Key Improvements:\")\nprint(\"  ✅ Automatic skew detection and correction\")\nprint(\"  ✅ Adaptive row detection (no fixed layout)\")\nprint(\"  ✅ Content-aware filtering (skip empty regions)\")\nprint(\"  ✅ Visual debugging at each step\")\nprint(\"\\n🎯 Next: Implement signal extraction for adaptive layout\")\nprint(\"   Then we can complete the pipeline!\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-23T15:56:31.744232Z","iopub.execute_input":"2025-10-23T15:56:31.745356Z","iopub.status.idle":"2025-10-23T15:56:46.154642Z","shell.execute_reply.started":"2025-10-23T15:56:31.745324Z","shell.execute_reply":"2025-10-23T15:56:46.153714Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\"\"\"\nCELDA DE PRUEBA: Probar detección adaptativa en múltiples imágenes\n==================================================================\nUsa esta celda para probar el pipeline V3 en diferentes muestras\n\"\"\"\n\nimport numpy as np\nimport pandas as pd\nfrom pathlib import Path\n\n# ============================================================================\n# SELECCIÓN DE IMÁGENES PARA PROBAR\n# ============================================================================\n\nprint(\"🔍 PRUEBA DE DETECCIÓN EN MÚLTIPLES IMÁGENES\")\nprint(\"=\" * 70)\n\n# Cargar metadata\ntrain_meta = pd.read_csv(DATA_PATH / 'train.csv')\n\n# Mostrar primeras 20 imágenes disponibles\nprint(\"\\n📋 Imágenes disponibles para probar:\")\nprint(f\"{'ID':<12} {'fs (Hz)':<10} {'sig_len':<10}\")\nprint(\"-\" * 35)\nfor idx, row in train_meta.head(20).iterrows():\n    print(f\"{row['id']:<12} {row['fs']:<10} {row['sig_len']:<10}\")\n\nprint(\"\\n\" + \"=\" * 70)\nprint(\"💡 INSTRUCCIONES:\")\nprint(\"   1. Elige un ID de la lista arriba\")\nprint(\"   2. Copia y pega en test_image_id abajo\")\nprint(\"   3. Ejecuta la celda\")\nprint(\"   4. Revisa si la detección funciona bien\")\nprint(\"=\" * 70)\n\n# ============================================================================\n# CONFIGURACIÓN DE PRUEBA\n# ============================================================================\n\n# 🔧 CAMBIA ESTE ID PARA PROBAR DIFERENTES IMÁGENES\ntest_image_id = 32650710  # ← Cambia este número\n\n# Configuración\nuse_variant = '0001'  # Usar imagen limpia (-0001)\nvisualize_full = True  # Mostrar todas las visualizaciones\n\n# ============================================================================\n# EJECUTAR PRUEBA\n# ============================================================================\n\nprint(f\"\\n🧪 PROBANDO IMAGEN: {test_image_id}\")\nprint(\"=\" * 70)\n\n# Verificar que existe\nif test_image_id not in train_meta['id'].values:\n    print(f\"❌ ERROR: ID {test_image_id} no existe en el dataset\")\n    print(f\"   IDs disponibles: {train_meta['id'].values[:10]}...\")\nelse:\n    # Obtener info\n    img_info = train_meta[train_meta['id'] == test_image_id].iloc[0]\n    \n    print(f\"📊 Información de la imagen:\")\n    print(f\"   ID: {img_info['id']}\")\n    print(f\"   Sampling frequency: {img_info['fs']} Hz\")\n    print(f\"   Signal length: {img_info['sig_len']} samples\")\n    print(f\"   Duration: {img_info['sig_len'] / img_info['fs']:.1f} seconds\")\n    \n    # Path de imagen\n    img_path = TRAIN_PATH / str(test_image_id) / f\"{test_image_id}-{use_variant}.png\"\n    \n    if not img_path.exists():\n        print(f\"\\n❌ Imagen no encontrada: {img_path}\")\n        print(f\"   Verificando qué variantes existen...\")\n        img_dir = TRAIN_PATH / str(test_image_id)\n        if img_dir.exists():\n            available = sorted(img_dir.glob(f\"{test_image_id}-*.png\"))\n            print(f\"   Variantes disponibles: {[p.name for p in available]}\")\n    else:\n        print(f\"\\n✅ Imagen encontrada: {img_path.name}\")\n        \n        # ====================================================================\n        # EJECUTAR PIPELINE ADAPTATIVO\n        # ====================================================================\n        \n        print(\"\\n\" + \"=\" * 70)\n        print(\"🎬 EJECUTANDO PIPELINE ADAPTATIVO V3\")\n        print(\"=\" * 70)\n        \n        try:\n            # Ejecutar pipeline completo\n            result = preprocess_adaptive(img_path, visualize=visualize_full)\n            \n            print(\"\\n\" + \"=\" * 70)\n            print(\"✅ PRUEBA COMPLETADA\")\n            print(\"=\" * 70)\n            \n            # Resumen de resultados\n            print(f\"\\n📊 RESULTADOS:\")\n            print(f\"   Rotación detectada: {result['skew_angle']:.3f}°\")\n            print(f\"   Filas detectadas: {len(result['rows'])}\")\n            \n            for idx, (y_start, y_end, row_type) in enumerate(result['rows']):\n                height = y_end - y_start\n                print(f\"   Row {idx+1} ({row_type:>8}): y={y_start:>4}-{y_end:>4} (height: {height:>3}px)\")\n            \n            # Análisis de calidad\n            print(f\"\\n🔍 ANÁLISIS DE CALIDAD:\")\n            \n            # Check 1: Número de filas\n            n_rows = len(result['rows'])\n            if n_rows == 4:\n                print(f\"   ✅ Número de filas correcto: {n_rows}\")\n            elif n_rows == 3:\n                print(f\"   ⚠️  Número de filas: {n_rows} (esperado 4, pero 3 es válido si rhythm integrado)\")\n            else:\n                print(f\"   ❌ Número de filas inesperado: {n_rows} (esperado 3-4)\")\n            \n            # Check 2: Altura de filas\n            heights = [y_end - y_start for y_start, y_end, _ in result['rows']]\n            standard_heights = [h for (_, _, t), h in zip(result['rows'], heights) if t == 'standard']\n            rhythm_heights = [h for (_, _, t), h in zip(result['rows'], heights) if t == 'rhythm']\n            \n            if standard_heights:\n                avg_standard = np.mean(standard_heights)\n                std_standard = np.std(standard_heights)\n                print(f\"   Standard rows: avg height = {avg_standard:.1f}px ± {std_standard:.1f}px\")\n                \n                if std_standard < 50:\n                    print(f\"   ✅ Standard rows tienen altura consistente\")\n                else:\n                    print(f\"   ⚠️  Standard rows varían mucho en altura\")\n            \n            if rhythm_heights:\n                print(f\"   Rhythm strip: height = {rhythm_heights[0]:.1f}px\")\n                if rhythm_heights[0] > avg_standard * 1.5:\n                    print(f\"   ✅ Rhythm strip es significativamente más grande\")\n                else:\n                    print(f\"   ⚠️  Rhythm strip no es mucho más grande que standard rows\")\n            \n            # Check 3: Rotación\n            if abs(result['skew_angle']) < 0.5:\n                print(f\"   ✅ Imagen está bien alineada (rotación mínima)\")\n            elif abs(result['skew_angle']) < 2.0:\n                print(f\"   ✅ Rotación leve corregida exitosamente\")\n            else:\n                print(f\"   ⚠️  Rotación significativa: {result['skew_angle']:.2f}°\")\n            \n            print(\"\\n\" + \"=\" * 70)\n            print(\"💡 INTERPRETACIÓN:\")\n            print(\"   - Si ves 4 filas bien divididas → Detección exitosa\")\n            print(\"   - Si cajas cyan capturan solo señal (sin labels) → Crop exitoso\")\n            print(\"   - Si hay problemas, prueba con otro ID\")\n            print(\"=\" * 70)\n            \n        except Exception as e:\n            print(f\"\\n❌ ERROR durante la ejecución:\")\n            print(f\"   {type(e).__name__}: {str(e)}\")\n            import traceback\n            print(\"\\n📋 Traceback completo:\")\n            traceback.print_exc()\n\n# ============================================================================\n# PRUEBA RÁPIDA EN BATCH (OPCIONAL)\n# ============================================================================\n\nprint(\"\\n\" + \"=\" * 70)\nprint(\"🔬 PRUEBA RÁPIDA EN BATCH (OPCIONAL)\")\nprint(\"=\" * 70)\nprint(\"Descomenta el código abajo para probar automáticamente en 5 imágenes\")\n\n\"\"\"\n# Lista de IDs para probar\ntest_ids = train_meta['id'].head(5).values\n\nbatch_results = []\n\nfor test_id in test_ids:\n    img_path = TRAIN_PATH / str(test_id) / f\"{test_id}-0001.png\"\n    \n    if not img_path.exists():\n        continue\n    \n    try:\n        # Ejecutar sin visualización\n        result = preprocess_adaptive(img_path, visualize=False)\n        \n        batch_results.append({\n            'id': test_id,\n            'skew_angle': result['skew_angle'],\n            'n_rows': len(result['rows']),\n            'success': True\n        })\n        \n    except Exception as e:\n        batch_results.append({\n            'id': test_id,\n            'skew_angle': None,\n            'n_rows': None,\n            'success': False,\n            'error': str(e)\n        })\n\n# Mostrar resultados\nbatch_df = pd.DataFrame(batch_results)\nprint(\"\\n📊 Resultados de prueba en batch:\")\nprint(batch_df)\n\nsuccess_rate = (batch_df['success'].sum() / len(batch_df)) * 100\nprint(f\"\\n✅ Tasa de éxito: {success_rate:.1f}%\")\n\"\"\"\n\nprint(\"\\n✨ Listo para probar más imágenes!\")\nprint(\"   Cambia 'test_image_id' arriba y vuelve a ejecutar\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-23T16:01:05.468644Z","iopub.execute_input":"2025-10-23T16:01:05.469373Z","iopub.status.idle":"2025-10-23T16:01:18.387971Z","shell.execute_reply.started":"2025-10-23T16:01:05.469339Z","shell.execute_reply":"2025-10-23T16:01:18.387101Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}