{"metadata":{"kernelspec":{"name":"python3","display_name":"Python 3","language":"python"},"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":[{"id":"074d073f","cell_type":"markdown","source":"# ECG Image Digitization: A Complete Guide\n\n## PhysioNet Challenge - Transforming ECG Images to Digital Signals\n\n\n[![OpenCV](https://img.shields.io/badge/OpenCV-5C3EE8?style=for-the-badge&logo=opencv&logoColor=white)](https://opencv.org/)\n\n---\n&#x2B06;  If you're wondering how to make a logo, definitely check out https://shields.io/.\n### Table of Contents\n1. **Introduction & Problem Understanding**\n2. **Dataset Exploration**\n3. **Image Preprocessing & Analysis**\n4. **Grid Detection & Signal Extraction**\n5. **Advanced Techniques & Denoising**\n6. **Evaluation & Visualization**\n\nPS : 6. There are some of bugs in **Evaluation & Visualization**. I would appreciate it if you could help. Thanks in advance.\n\n### Challenge Goal\nConvert **scanned ECG images** (photos/scans of paper ECG reports) back into **digital time-series signals** \n\n---","metadata":{}},{"id":"33279b88","cell_type":"markdown","source":"---\n## 1. Environment Setup & Imports\n\nFirst, let's import all necessary libraries and set up our visualization preferences.","metadata":{}},{"id":"222ca9fd","cell_type":"code","source":"# Core libraries\nimport numpy as np\nimport pandas as pd\nimport cv2\nfrom pathlib import Path\nimport warnings\nwarnings.filterwarnings('ignore')\n\n# Visualization\nimport matplotlib.pyplot as plt\nimport matplotlib.gridspec as gridspec\nfrom matplotlib.patches import Rectangle\nimport seaborn as sns\n\n# Image processing\nfrom scipy import signal, ndimage\nfrom skimage import filters, morphology, measure\nfrom scipy.interpolate import interp1d, UnivariateSpline\n\n# Deep learning (optional for advanced methods)\ntry:\n import torch\n import torch.nn.functional as F\n TORCH_AVAILABLE = True\nexcept ImportError:\n TORCH_AVAILABLE = False\n print(\" PyTorch not available. Some advanced features will be disabled.\")\n\n# Set plotting style\nplt.style.use('seaborn-v0_8-darkgrid')\nsns.set_palette(\"husl\")\n\n# Display settings\n%matplotlib inline\n%config InlineBackend.figure_format = 'retina'\nplt.rcParams['figure.figsize'] = (16, 8)\nplt.rcParams['font.size'] = 10\n\nprint(\" All libraries imported successfully!\")\nprint(f\" NumPy version: {np.__version__}\")\nprint(f\" OpenCV version: {cv2.__version__}\")\nprint(f\" PyTorch available: {TORCH_AVAILABLE}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-04T19:33:33.192994Z","iopub.execute_input":"2025-11-04T19:33:33.193418Z","iopub.status.idle":"2025-11-04T19:33:36.581204Z","shell.execute_reply.started":"2025-11-04T19:33:33.193376Z","shell.execute_reply":"2025-11-04T19:33:36.580177Z"}},"outputs":[],"execution_count":null},{"id":"c050b882","cell_type":"markdown","source":"### Configure Data Paths","metadata":{}},{"id":"de9dda2e","cell_type":"code","source":"# Configure paths - adjust these to your environment\nDATA_DIR = Path('/kaggle/input/physionet-ecg-image-digitization')\nTRAIN_DIR = DATA_DIR / 'train'\nTEST_DIR = DATA_DIR / 'test'\n\n# Verify paths exist\nassert DATA_DIR.exists(), f\" Data directory not found: {DATA_DIR}\"\nassert TRAIN_DIR.exists(), f\" Train directory not found: {TRAIN_DIR}\"\n\nprint(f\" Data directory: {DATA_DIR}\")\nprint(f\" All paths verified!\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-04T19:33:36.582745Z","iopub.execute_input":"2025-11-04T19:33:36.583209Z","iopub.status.idle":"2025-11-04T19:33:36.589942Z","shell.execute_reply.started":"2025-11-04T19:33:36.583185Z","shell.execute_reply":"2025-11-04T19:33:36.588491Z"}},"outputs":[],"execution_count":null},{"id":"82f8e4c5","cell_type":"markdown","source":"---\n## 2. Dataset Exploration\n\nLet's explore the dataset structure and understand what we're working with.","metadata":{}},{"id":"01af6cbc","cell_type":"code","source":"# Load metadata\ntrain_df = pd.read_csv(DATA_DIR / 'train.csv')\ntest_df = pd.read_csv(DATA_DIR / 'test.csv')\n\nprint(\" Dataset Overview\")\nprint(\"=\" * 60)\nprint(f\" Training samples: {len(train_df):,}\")\nprint(f\" Test samples: {len(test_df):,}\")\nprint(f\"\\n Training data columns: {list(train_df.columns)}\")\nprint(f\"\\n First few samples:\")\ndisplay(train_df.head(10))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-04T19:33:36.590975Z","iopub.execute_input":"2025-11-04T19:33:36.591329Z","iopub.status.idle":"2025-11-04T19:33:36.628940Z","shell.execute_reply.started":"2025-11-04T19:33:36.591268Z","shell.execute_reply":"2025-11-04T19:33:36.628167Z"}},"outputs":[],"execution_count":null},{"id":"80f2a4a1","cell_type":"code","source":"# Statistical analysis\nprint(\"\\n Statistical Summary\")\nprint(\"=\" * 60)\nprint(train_df.describe())\n\n# Analyze sampling frequencies\nprint(\"\\n Sampling Frequency Distribution:\")\nprint(train_df['fs'].value_counts().sort_index())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-04T19:33:36.629961Z","iopub.execute_input":"2025-11-04T19:33:36.630296Z","iopub.status.idle":"2025-11-04T19:33:36.649515Z","shell.execute_reply.started":"2025-11-04T19:33:36.630250Z","shell.execute_reply":"2025-11-04T19:33:36.648452Z"}},"outputs":[],"execution_count":null},{"id":"a3d891ed","cell_type":"code","source":"# Visualize dataset statistics\nfig, axes = plt.subplots(2, 2, figsize=(16, 10))\n\n# 1. Sampling frequency distribution\ntrain_df['fs'].value_counts().sort_index().plot(kind='bar', ax=axes[0, 0], color='steelblue')\naxes[0, 0].set_title(' Sampling Frequency Distribution', fontsize=14, fontweight='bold')\naxes[0, 0].set_xlabel('Sampling Frequency (Hz)')\naxes[0, 0].set_ylabel('Count')\naxes[0, 0].grid(True, alpha=0.3)\n\n# 2. Signal length distribution\ntrain_df['sig_len'].plot(kind='hist', bins=50, ax=axes[0, 1], color='coral', alpha=0.7)\naxes[0, 1].set_title(' Signal Length Distribution', fontsize=14, fontweight='bold')\naxes[0, 1].set_xlabel('Signal Length (samples)')\naxes[0, 1].set_ylabel('Frequency')\naxes[0, 1].grid(True, alpha=0.3)\n\n# 3. Duration calculation and distribution\ntrain_df['duration_sec'] = train_df['sig_len'] / train_df['fs']\ntrain_df['duration_sec'].plot(kind='hist', bins=50, ax=axes[1, 0], color='mediumseagreen', alpha=0.7)\naxes[1, 0].set_title('⏱ ECG Duration Distribution', fontsize=14, fontweight='bold')\naxes[1, 0].set_xlabel('Duration (seconds)')\naxes[1, 0].set_ylabel('Frequency')\naxes[1, 0].grid(True, alpha=0.3)\n\n# 4. Relationship: fs vs sig_len\naxes[1, 1].scatter(train_df['fs'], train_df['sig_len'], alpha=0.5, s=30, color='purple')\naxes[1, 1].set_title(' Sampling Frequency vs Signal Length', fontsize=14, fontweight='bold')\naxes[1, 1].set_xlabel('Sampling Frequency (Hz)')\naxes[1, 1].set_ylabel('Signal Length (samples)')\naxes[1, 1].grid(True, alpha=0.3)\n\nplt.tight_layout()\nplt.show()\n\nprint(f\"\\n Key Insights:\")\nprint(f\" • Average ECG duration: {train_df['duration_sec'].mean():.2f} seconds\")\nprint(f\" • Most common sampling rate: {train_df['fs'].mode()[0]} Hz\")\nprint(f\" • Signal length range: {train_df['sig_len'].min()} - {train_df['sig_len'].max()} samples\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-04T19:33:36.651983Z","iopub.execute_input":"2025-11-04T19:33:36.652319Z","iopub.status.idle":"2025-11-04T19:33:37.955760Z","shell.execute_reply.started":"2025-11-04T19:33:36.652258Z","shell.execute_reply":"2025-11-04T19:33:37.954546Z"}},"outputs":[],"execution_count":null},{"id":"6e635b7f","cell_type":"markdown","source":"---\n## 3. ECG Image Analysis\n\nNow let's load and analyze actual ECG images to understand their structure.","metadata":{}},{"id":"75113793","cell_type":"code","source":"def load_ecg_images(sample_id, max_images=3):\n    \"\"\"\n    Load ECG images for a given sample ID.\n    \n    Parameters:\n    -----------\n    sample_id : str or int or float\n        The ID of the ECG sample\n    max_images : int\n        Maximum number of images to load\n    \n    Returns:\n    --------\n    list of tuples : (image_path, image_array)\n    \"\"\"\n    # Convert float to int if necessary (pandas sometimes returns float)\n    if isinstance(sample_id, float):   # I think \n        sample_id = int(sample_id)\n    \n    sample_dir = TRAIN_DIR / str(sample_id)\n    \n    if not sample_dir.exists():\n        print(f\"⚠️ Sample directory not found: {sample_dir}\")\n        return []\n    \n    image_files = sorted(sample_dir.glob('*.png'))[:max_images]\n    images = []\n    \n    for img_path in image_files:\n        img = cv2.imread(str(img_path))\n        if img is not None:  # Check if image loaded successfully\n            img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)\n            images.append((img_path.name, img_rgb))\n    \n    return images\n\n# Select a sample to analyze - CONVERT TO INT!\nsample_id = int(train_df.iloc[0]['id'])  \nprint(f\"🔍 Analyzing sample: {sample_id}\")\n\n# Load images\necg_images = load_ecg_images(sample_id, max_images=4)\nprint(f\"📸 Loaded {len(ecg_images)} images\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-04T19:33:37.957312Z","iopub.execute_input":"2025-11-04T19:33:37.957625Z","iopub.status.idle":"2025-11-04T19:33:38.780497Z","shell.execute_reply.started":"2025-11-04T19:33:37.957598Z","shell.execute_reply":"2025-11-04T19:33:38.779507Z"}},"outputs":[],"execution_count":null},{"id":"19ccf5a8","cell_type":"code","source":"# Visualize the ECG images\nif ecg_images:\n    n_images = len(ecg_images)\n    fig, axes = plt.subplots(n_images, 1, figsize=(18, 6 * n_images))\n    \n    if n_images == 1:\n        axes = [axes]\n    \n    for idx, (img_name, img) in enumerate(ecg_images):\n        axes[idx].imshow(img)\n        axes[idx].set_title(f'📄 {img_name} | Shape: {img.shape} | Size: {img.shape[0]*img.shape[1]:,} pixels', \n                          fontsize=12, fontweight='bold')\n        axes[idx].axis('off')\n        \n        # Add image statistics\n        mean_brightness = img.mean()\n        axes[idx].text(10, 30, f'Mean Brightness: {mean_brightness:.1f}', \n                      bbox=dict(boxstyle='round', facecolor='white', alpha=0.8),\n                      fontsize=10)\n    \n    plt.tight_layout()\n    plt.show()\nelse:\n    print(\"❌ No images found for this sample\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-04T19:33:38.781347Z","iopub.execute_input":"2025-11-04T19:33:38.781680Z","iopub.status.idle":"2025-11-04T19:33:46.061219Z","shell.execute_reply.started":"2025-11-04T19:33:38.781650Z","shell.execute_reply":"2025-11-04T19:33:46.059391Z"}},"outputs":[],"execution_count":null},{"id":"a0357570","cell_type":"markdown","source":"### Detailed Image Analysis","metadata":{}},{"id":"2d247ea4","cell_type":"code","source":"def analyze_ecg_image(image):\n \"\"\"\n Perform comprehensive analysis of an ECG image.\n \n Returns various image characteristics and processed versions.\n \"\"\"\n # Convert to grayscale\n gray = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY)\n \n # Calculate histogram\n hist = cv2.calcHist([gray], [0], None, [256], [0, 256])\n \n # Apply different edge detection methods\n edges_canny = cv2.Canny(gray, 50, 150)\n edges_sobel = filters.sobel(gray)\n \n # Adaptive thresholding\n thresh_adaptive = cv2.adaptiveThreshold(gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, \n cv2.THRESH_BINARY, 11, 2)\n \n # Otsu's thresholding\n _, thresh_otsu = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)\n \n return {\n 'gray': gray,\n 'histogram': hist,\n 'edges_canny': edges_canny,\n 'edges_sobel': edges_sobel,\n 'thresh_adaptive': thresh_adaptive,\n 'thresh_otsu': thresh_otsu\n }\n\n# Analyze the first image\nif ecg_images:\n _, first_image = ecg_images[0]\n analysis = analyze_ecg_image(first_image)\n \n # Visualize analysis results\n fig = plt.figure(figsize=(18, 12))\n gs = gridspec.GridSpec(3, 3, hspace=0.3, wspace=0.3)\n \n # Original image\n ax1 = plt.subplot(gs[0, :])\n ax1.imshow(first_image)\n ax1.set_title(' Original ECG Image', fontsize=14, fontweight='bold')\n ax1.axis('off')\n \n # Grayscale\n ax2 = plt.subplot(gs[1, 0])\n ax2.imshow(analysis['gray'], cmap='gray')\n ax2.set_title(' Grayscale', fontsize=12, fontweight='bold')\n ax2.axis('off')\n \n # Histogram\n ax3 = plt.subplot(gs[1, 1])\n ax3.plot(analysis['histogram'], color='steelblue', linewidth=2)\n ax3.set_title(' Intensity Histogram', fontsize=12, fontweight='bold')\n ax3.set_xlabel('Pixel Intensity')\n ax3.set_ylabel('Frequency')\n ax3.grid(True, alpha=0.3)\n \n # Canny edges\n ax4 = plt.subplot(gs[1, 2])\n ax4.imshow(analysis['edges_canny'], cmap='gray')\n ax4.set_title(' Canny Edge Detection', fontsize=12, fontweight='bold')\n ax4.axis('off')\n \n # Sobel edges\n ax5 = plt.subplot(gs[2, 0])\n ax5.imshow(analysis['edges_sobel'], cmap='gray')\n ax5.set_title(' Sobel Edge Detection', fontsize=12, fontweight='bold')\n ax5.axis('off')\n \n # Adaptive threshold\n ax6 = plt.subplot(gs[2, 1])\n ax6.imshow(analysis['thresh_adaptive'], cmap='gray')\n ax6.set_title(' Adaptive Thresholding', fontsize=12, fontweight='bold')\n ax6.axis('off')\n \n # Otsu threshold\n ax7 = plt.subplot(gs[2, 2])\n ax7.imshow(analysis['thresh_otsu'], cmap='gray')\n ax7.set_title(' Otsu Thresholding', fontsize=12, fontweight='bold')\n ax7.axis('off')\n \n plt.show()\n \n print(\"\\n Image analysis complete!\")\n print(f\" • Image dimensions: {first_image.shape[0]} x {first_image.shape[1]}\")\n print(f\" • Mean pixel value: {analysis['gray'].mean():.2f}\")\n print(f\" • Std pixel value: {analysis['gray'].std():.2f}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-04T19:33:46.062804Z","iopub.execute_input":"2025-11-04T19:33:46.063253Z","iopub.status.idle":"2025-11-04T19:33:49.575700Z","shell.execute_reply.started":"2025-11-04T19:33:46.063209Z","shell.execute_reply":"2025-11-04T19:33:49.574717Z"}},"outputs":[],"execution_count":null},{"id":"1c366cfd","cell_type":"markdown","source":"---\n## 4. ECG Signal Extraction Pipeline\n\nNow let's build a comprehensive pipeline to extract the ECG signal from images.","metadata":{}},{"id":"979d7584","cell_type":"code","source":"class ECGImageProcessor:\n    \"\"\"\n    A comprehensive class for processing ECG images and extracting signals.\n    \n    This class implements various methods for:\n    - Image preprocessing\n    - Grid detection\n    - Signal extraction\n    - Quality assessment\n    \"\"\"\n    \n    def __init__(self, image):\n        \"\"\"\n        Initialize with an ECG image.\n        \n        Parameters:\n        -----------\n        image : numpy.ndarray\n            RGB image of ECG\n        \"\"\"\n        self.original_image = image.copy()\n        self.height, self.width = image.shape[:2]\n        self.gray = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY)\n        \n    def preprocess(self, denoise=True, enhance_contrast=True):\n        \"\"\"\n        Preprocess the image for better signal extraction.\n        \n        Parameters:\n        -----------\n        denoise : bool\n            Apply denoising filter\n        enhance_contrast : bool\n            Apply contrast enhancement\n        \n        Returns:\n        --------\n        numpy.ndarray : Preprocessed grayscale image\n        \"\"\"\n        processed = self.gray.copy()\n        \n        # Denoise\n        if denoise:\n            processed = cv2.fastNlMeansDenoising(processed, None, 10, 7, 21)\n        \n        # Enhance contrast\n        if enhance_contrast:\n            clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))\n            processed = clahe.apply(processed)\n        \n        self.preprocessed = processed\n        return processed\n    \n    def detect_grid_lines(self, threshold=200):\n        \"\"\"\n        Detect horizontal and vertical grid lines.\n        \n        Parameters:\n        -----------\n        threshold : int\n            Threshold for line detection\n        \n        Returns:\n        --------\n        tuple : (horizontal_lines, vertical_lines)\n        \"\"\"\n        # Threshold image\n        _, binary = cv2.threshold(self.gray, threshold, 255, cv2.THRESH_BINARY)\n        \n        # Detect horizontal lines\n        horizontal_kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (40, 1))\n        horizontal_lines = cv2.morphologyEx(binary, cv2.MORPH_OPEN, horizontal_kernel)\n        \n        # Detect vertical lines\n        vertical_kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (1, 40))\n        vertical_lines = cv2.morphologyEx(binary, cv2.MORPH_OPEN, vertical_kernel)\n        \n        self.horizontal_lines = horizontal_lines\n        self.vertical_lines = vertical_lines\n        \n        return horizontal_lines, vertical_lines\n    \n    def extract_signal_trace(self, roi=None, method='projection'):\n        \"\"\"\n        Extract ECG signal trace from the image.\n        \n        Parameters:\n        -----------\n        roi : tuple or None\n            Region of interest (x, y, w, h)\n        method : str\n            Extraction method: 'projection', 'contour', or 'darkest'\n        \n        Returns:\n        --------\n        numpy.ndarray : Extracted signal values\n        \"\"\"\n        # Use preprocessed image or grayscale\n        img = self.preprocessed if hasattr(self, 'preprocessed') else self.gray\n        \n        # Apply ROI if specified\n        if roi is not None:\n            x, y, w, h = roi\n            img = img[y:y+h, x:x+w]\n        \n        if method == 'projection':\n            # Invert image (signal is dark)\n            inverted = 255 - img\n            # Column-wise projection\n            signal = np.argmax(inverted, axis=0)\n        \n        elif method == 'darkest':\n            # Find darkest pixel in each column\n            signal = np.argmin(img, axis=0)\n        \n        elif method == 'weighted':\n            # Weighted average based on intensity\n            inverted = 255 - img\n            weights = inverted / inverted.sum(axis=0, keepdims=True)\n            positions = np.arange(img.shape[0]).reshape(-1, 1)\n            signal = (positions * weights).sum(axis=0)\n        \n        else:\n            raise ValueError(f\"Unknown method: {method}\")\n        \n        # Invert y-axis (image coordinates are top-down)\n        signal = img.shape[0] - signal\n        \n        return signal\n    \n    def smooth_signal(self, signal, method='savgol', **kwargs):\n        \"\"\"\n        Apply smoothing to extracted signal.\n        \n        Parameters:\n        -----------\n        signal : numpy.ndarray\n            Raw signal values\n        method : str\n            Smoothing method: 'savgol', 'median', 'gaussian'\n        **kwargs : dict\n            Additional parameters for smoothing\n        \n        Returns:\n        --------\n        numpy.ndarray : Smoothed signal\n        \"\"\"\n        if method == 'savgol':\n            window = kwargs.get('window_length', 11)\n            polyorder = kwargs.get('polyorder', 3)\n            smoothed = signal.savgol_filter(signal, window, polyorder)\n        \n        elif method == 'median':\n            kernel_size = kwargs.get('kernel_size', 5)\n            smoothed = ndimage.median_filter(signal, size=kernel_size)\n        \n        elif method == 'gaussian':\n            sigma = kwargs.get('sigma', 2)\n            smoothed = ndimage.gaussian_filter1d(signal, sigma)\n        \n        else:\n            smoothed = signal\n        \n        return smoothed\n\nprint(\"✅ ECGImageProcessor class defined successfully!\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-04T19:33:49.576755Z","iopub.execute_input":"2025-11-04T19:33:49.577015Z","iopub.status.idle":"2025-11-04T19:33:49.594458Z","shell.execute_reply.started":"2025-11-04T19:33:49.576995Z","shell.execute_reply":"2025-11-04T19:33:49.593215Z"}},"outputs":[],"execution_count":null},{"id":"bc8dc5cf","cell_type":"markdown","source":"### Test Image Preprocessing","metadata":{}},{"id":"0a3a7458","cell_type":"code","source":"# Process the first ECG image\nif ecg_images:\n    _, test_image = ecg_images[0]\n    \n    # Initialize processor\n    processor = ECGImageProcessor(test_image)\n    \n    # Preprocess\n    print(\"🔧 Preprocessing image...\")\n    preprocessed = processor.preprocess(denoise=True, enhance_contrast=True)\n    \n    # Visualize preprocessing steps - Focus on what works!\n    fig, axes = plt.subplots(1, 3, figsize=(18, 5))\n    \n    axes[0].imshow(test_image)\n    axes[0].set_title(' Original ECG Image', fontsize=14, fontweight='bold')\n    axes[0].axis('off')\n    \n    axes[1].imshow(processor.gray, cmap='gray')\n    axes[1].set_title(' Grayscale Conversion', fontsize=14, fontweight='bold')\n    axes[1].axis('off')\n    \n    axes[2].imshow(preprocessed, cmap='gray')\n    axes[2].set_title(' Enhanced (Denoised + Contrast)', fontsize=14, fontweight='bold')\n    axes[2].axis('off')\n    \n    plt.tight_layout()\n    plt.show()\n    \n    print(\"\\n✅ Preprocessing complete!\")\n    print(f\"   • Original shape: {test_image.shape}\")\n    print(f\"   • Pixel value range: [{preprocessed.min()}, {preprocessed.max()}]\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-04T19:33:49.595555Z","iopub.execute_input":"2025-11-04T19:33:49.595862Z","iopub.status.idle":"2025-11-04T19:33:54.569999Z","shell.execute_reply.started":"2025-11-04T19:33:49.595841Z","shell.execute_reply":"2025-11-04T19:33:54.568883Z"}},"outputs":[],"execution_count":null},{"id":"e8912cab","cell_type":"markdown","source":"### Extract and Visualize ECG Signal","metadata":{}},{"id":"71d58a4a","cell_type":"code","source":"# Extract signal using different methods\nif ecg_images:\n    print(\" Extracting ECG signal using multiple methods...\\n\")\n    \n    # Define a region of interest - focusing on the ECG signal area\n    # Format: (x, y, width, height)\n    # Adjusted to capture the actual ECG trace in the middle of the image\n    img_height, img_width = test_image.shape[:2]\n    roi = (50, int(img_height * 0.3), img_width - 100, int(img_height * 0.5))\n    \n    # Extract using different methods\n    signal_projection = processor.extract_signal_trace(roi=roi, method='projection')\n    signal_darkest = processor.extract_signal_trace(roi=roi, method='darkest')\n    signal_weighted = processor.extract_signal_trace(roi=roi, method='weighted')\n    \n    # Smooth signals\n    signal_projection_smooth = processor.smooth_signal(signal_projection, method='gaussian', sigma=2)\n    signal_darkest_smooth = processor.smooth_signal(signal_darkest, method='gaussian', sigma=2)\n    signal_weighted_smooth = processor.smooth_signal(signal_weighted, method='gaussian', sigma=2)\n    \n    # Visualize results\n    fig, axes = plt.subplots(4, 1, figsize=(20, 14))\n    \n    # Show ROI on image\n    img_with_roi = test_image.copy()\n    x, y, w, h = roi\n    cv2.rectangle(img_with_roi, (x, y), (x + w, y + h), (255, 0, 0), 3)\n    axes[0].imshow(img_with_roi)\n    axes[0].set_title(' ECG Image with ROI (Region of Interest)', fontsize=14, fontweight='bold')\n    axes[0].axis('off')\n    \n    # Plot extracted signals\n    x_axis = np.arange(len(signal_projection))\n    \n    axes[1].plot(x_axis, signal_projection, alpha=0.6, label='Raw', linewidth=1)\n    axes[1].plot(x_axis, signal_projection_smooth, label='Smoothed', linewidth=2)\n    axes[1].set_title(' Method 1: Projection-based Extraction', fontsize=12, fontweight='bold')\n    axes[1].set_ylabel('Amplitude')\n    axes[1].legend()\n    axes[1].grid(True, alpha=0.3)\n    \n    axes[2].plot(x_axis, signal_darkest, alpha=0.6, label='Raw', linewidth=1)\n    axes[2].plot(x_axis, signal_darkest_smooth, label='Smoothed', linewidth=2)\n    axes[2].set_title(' Method 2: Darkest Pixel Extraction', fontsize=12, fontweight='bold')\n    axes[2].set_ylabel('Amplitude')\n    axes[2].legend()\n    axes[2].grid(True, alpha=0.3)\n    \n    axes[3].plot(x_axis, signal_weighted, alpha=0.6, label='Raw', linewidth=1)\n    axes[3].plot(x_axis, signal_weighted_smooth, label='Smoothed', linewidth=2)\n    axes[3].set_title(' Method 3: Weighted Average Extraction', fontsize=12, fontweight='bold')\n    axes[3].set_xlabel('Sample Index')\n    axes[3].set_ylabel('Amplitude')\n    axes[3].legend()\n    axes[3].grid(True, alpha=0.3)\n    \n    plt.tight_layout()\n    plt.show()\n    \n    print(\"\\n✅ Signal extraction complete!\")\n    print(f\"   • Signal length: {len(signal_projection)} samples\")\n    print(f\"   • Signal range: [{signal_projection.min():.2f}, {signal_projection.max():.2f}]\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-04T19:33:54.571252Z","iopub.execute_input":"2025-11-04T19:33:54.571592Z","iopub.status.idle":"2025-11-04T19:33:56.708708Z","shell.execute_reply.started":"2025-11-04T19:33:54.571568Z","shell.execute_reply":"2025-11-04T19:33:56.707712Z"}},"outputs":[],"execution_count":null},{"id":"7badc4af","cell_type":"markdown","source":"---\n## 5. Advanced Processing & Quality Metrics\n\nLet's implement quality metrics to evaluate our signal extraction.","metadata":{}},{"id":"39a6757f","cell_type":"code","source":"def calculate_signal_quality_metrics(signal):\n    \"\"\"\n    Calculate various quality metrics for an extracted ECG signal.\n    \n    Parameters:\n    -----------\n    signal : numpy.ndarray\n        Extracted ECG signal\n    \n    Returns:\n    --------\n    dict : Quality metrics\n    \"\"\"\n    metrics = {}\n    \n    # Basic statistics\n    metrics['mean'] = np.mean(signal)\n    metrics['std'] = np.std(signal)\n    metrics['min'] = np.min(signal)\n    metrics['max'] = np.max(signal)\n    metrics['range'] = metrics['max'] - metrics['min']\n    \n    # Signal-to-noise ratio (simplified)\n    signal_power = np.mean(signal ** 2)\n    noise_power = np.var(np.diff(signal))\n    metrics['snr'] = 10 * np.log10(signal_power / (noise_power + 1e-10))\n    \n    # Smoothness (variation measure)\n    first_derivative = np.diff(signal)\n    metrics['smoothness'] = np.std(first_derivative)\n    \n    # Zero crossings\n    signal_normalized = signal - np.mean(signal)\n    zero_crossings = np.sum(np.diff(np.sign(signal_normalized)) != 0)\n    metrics['zero_crossings'] = zero_crossings\n    \n    return metrics\n\n# Calculate metrics for our extracted signals\nif ecg_images:\n    print(\" Signal Quality Metrics\")\n    print(\"=\" * 80)\n    \n    for name, sig in [('Projection', signal_projection_smooth),\n                      ('Darkest', signal_darkest_smooth),\n                      ('Weighted', signal_weighted_smooth)]:\n        metrics = calculate_signal_quality_metrics(sig)\n        \n        print(f\"\\n🔍 {name} Method:\")\n        print(f\"   Mean: {metrics['mean']:.2f}\")\n        print(f\"   Std Dev: {metrics['std']:.2f}\")\n        print(f\"   Range: {metrics['range']:.2f}\")\n        print(f\"   SNR: {metrics['snr']:.2f} dB\")\n        print(f\"   Smoothness: {metrics['smoothness']:.2f}\")\n        print(f\"   Zero Crossings: {metrics['zero_crossings']}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-04T19:33:56.709817Z","iopub.execute_input":"2025-11-04T19:33:56.710128Z","iopub.status.idle":"2025-11-04T19:33:56.729154Z","shell.execute_reply.started":"2025-11-04T19:33:56.710104Z","shell.execute_reply":"2025-11-04T19:33:56.727864Z"}},"outputs":[],"execution_count":null},{"id":"d62fccea","cell_type":"markdown","source":"---\n## 6. Complete End-to-End Pipeline\n\nLet's create a complete pipeline that processes all images for a sample.","metadata":{}},{"id":"174c1e61","cell_type":"code","source":"def process_ecg_sample(sample_id, output_signals=True, visualize=True):\n    \"\"\"\n    I couldn't make it :(\n    Complete pipeline to process all ECG images for a sample.\n    \n    Parameters:\n    -----------\n    sample_id : str or int\n        Sample ID to process\n    output_signals : bool\n        Whether to return extracted signals\n    visualize : bool\n        Whether to create visualizations\n    \n    Returns:\n    --------\n    dict : Processed results including signals and metadata\n    \"\"\"\n    print(f\"\\n Processing ECG Sample: {sample_id}\")\n    print(\"=\" * 80)\n    \n    # Load images\n    images = load_ecg_images(sample_id, max_images=6)\n    \n    if not images:\n        print(\"❌ No images found!\")\n        return None\n    \n    print(f\"✅ Loaded {len(images)} images\")\n    \n    results = {\n        'sample_id': sample_id,\n        'n_images': len(images),\n        'signals': [],\n        'metadata': []\n    }\n    \n    # Process each image\n    for idx, (img_name, img) in enumerate(images):\n        print(f\"\\n  Processing {img_name}...\")\n        \n        # Initialize processor\n        processor = ECGImageProcessor(img)\n        \n        # Preprocess\n        processor.preprocess(denoise=True, enhance_contrast=True)\n        \n        # Extract signal - dynamically set ROI based on image size\n        img_height, img_width = img.shape[:2]\n        roi = (50, int(img_height * 0.3), img_width - 100, int(img_height * 0.5))\n        signal_raw = processor.extract_signal_trace(roi=roi, method='weighted')\n        signal_smooth = processor.smooth_signal(signal_raw, method='gaussian', sigma=2)\n        \n        # Calculate quality metrics\n        metrics = calculate_signal_quality_metrics(signal_smooth)\n        \n        # Store results\n        results['signals'].append({\n            'image_name': img_name,\n            'raw_signal': signal_raw,\n            'smooth_signal': signal_smooth,\n            'length': len(signal_smooth)\n        })\n        \n        results['metadata'].append({\n            'image_name': img_name,\n            'image_shape': img.shape,\n            'signal_length': len(signal_smooth),\n            'snr': metrics['snr'],\n            'smoothness': metrics['smoothness']\n        })\n        \n        print(f\"     ✓ Signal length: {len(signal_smooth)} | SNR: {metrics['snr']:.2f} dB\")\n    \n    # Visualize if requested\n    if visualize and len(images) > 0:\n        n_imgs = len(images)\n        fig, axes = plt.subplots(n_imgs, 2, figsize=(20, 4 * n_imgs))\n        \n        if n_imgs == 1:\n            axes = axes.reshape(1, -1)\n        \n        for idx, ((img_name, img), signal_data) in enumerate(zip(images, results['signals'])):\n            # Show image\n            axes[idx, 0].imshow(img)\n            axes[idx, 0].set_title(f'📸 {img_name}', fontsize=12, fontweight='bold')\n            axes[idx, 0].axis('off')\n            \n            # Show signal\n            x = np.arange(len(signal_data['smooth_signal']))\n            axes[idx, 1].plot(x, signal_data['smooth_signal'], linewidth=1.5, color='darkblue')\n            axes[idx, 1].set_title(f' Extracted ECG Signal', fontsize=12, fontweight='bold')\n            axes[idx, 1].set_xlabel('Sample Index')\n            axes[idx, 1].set_ylabel('Amplitude')\n            axes[idx, 1].grid(True, alpha=0.3)\n        \n        plt.tight_layout()\n        plt.show()\n    \n    print(f\"\\n✅ Processing complete for sample {sample_id}!\")\n    print(f\"   Total signals extracted: {len(results['signals'])}\")\n    \n    return results\n\n# Test the complete pipeline\nsample_id = train_df.iloc[0]['id']\nresults = process_ecg_sample(sample_id, visualize=True)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-04T19:33:56.730561Z","iopub.execute_input":"2025-11-04T19:33:56.730894Z","iopub.status.idle":"2025-11-04T19:34:40.123888Z","shell.execute_reply.started":"2025-11-04T19:33:56.730855Z","shell.execute_reply":"2025-11-04T19:34:40.122433Z"}},"outputs":[],"execution_count":null},{"id":"efa756ad","cell_type":"markdown","source":"---\n## 7. Summary & Next Steps\n\n### What We've Accomplished\n\nIn this notebook, we've built a comprehensive ECG image digitization pipeline:\n\n1. **Dataset Exploration** - Analyzed the PhysioNet ECG dataset structure\n2. **Image Analysis** - Performed detailed analysis of ECG images\n3. **Preprocessing** - Implemented denoising and enhancement techniques\n4. **Grid Detection** - Detected and analyzed ECG grid patterns\n5. **Signal Extraction** - Implemented multiple extraction methods\n6. **Quality Metrics** - Developed signal quality assessment tools\n7. **End-to-End Pipeline** - Created a complete processing workflow\n\n### Next Steps & Improvements\n\nTo further improve this pipeline, consider:\n\n1. **Deep Learning Approaches**\n - Train a U-Net for grid point detection\n - Use CNN for automatic ROI detection\n - Implement attention mechanisms for signal extraction\n\n2. **Advanced Preprocessing**\n - Perspective correction for skewed images\n - Automatic calibration detection\n - Multi-lead separation and extraction\n\n3. **Signal Processing**\n - Baseline wander removal\n - High-frequency noise filtering\n - Peak detection (R-peaks, P-waves, T-waves)\n\n4. **Validation & Metrics**\n - Compare with ground truth signals\n - Implement medical-grade quality metrics\n - Cross-validation across different ECG types\n\n### Resources & References\n\n- [PhysioNet Challenge](https://physionet.org/)\n- [ECG Signal Processing Guide](https://www.physionet.org/content/ecg-guide/)\n- [OpenCV Documentation](https://docs.opencv.org/)\n\n### Feedback & Contributions\n\nIf you found this notebook helpful:\n- Star the repository\n- Fork and improve\n- Share your feedback\n- Report issues\n\n---\n\n**Happy Coding! **","metadata":{}},{"id":"4359cbd6","cell_type":"markdown","source":"---\n## Bonus: Interactive Exploration\n\nTry different samples and parameters below!","metadata":{}},{"id":"b05ab675","cell_type":"code","source":"# Interactive cell - change the sample_id to explore different ECG recordings\n# You can select any ID from the train_df dataframe\n\n# Show available samples\nprint(\" Available samples (first 5):\")\nprint(train_df['id'].head(5).tolist())\n\n# Select a sample\ninteractive_sample_id = train_df.iloc[2]['id'] # Change the index to try different samples\n\n# Process it\ninteractive_results = process_ecg_sample(interactive_sample_id, visualize=True)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-04T19:34:40.128114Z","iopub.execute_input":"2025-11-04T19:34:40.128461Z","iopub.status.idle":"2025-11-04T19:35:23.123015Z","shell.execute_reply.started":"2025-11-04T19:34:40.128434Z","shell.execute_reply":"2025-11-04T19:35:23.121950Z"}},"outputs":[],"execution_count":null},{"id":"828b69aa","cell_type":"code","source":"# Compare multiple extraction methods side-by-side\nif interactive_results and len(interactive_results['signals']) > 0:\n    first_img = load_ecg_images(interactive_sample_id, max_images=1)[0][1]\n    processor = ECGImageProcessor(first_img)\n    processor.preprocess()\n    \n    roi = (50, 100, first_img.shape[1] - 100, first_img.shape[0] - 200)\n    \n    methods = ['projection', 'darkest', 'weighted']\n    colors = ['blue', 'red', 'green']\n    \n    plt.figure(figsize=(20, 6))\n    \n    for method, color in zip(methods, colors):\n        signal = processor.extract_signal_trace(roi=roi, method=method)\n        signal_smooth = processor.smooth_signal(signal, method='gaussian', sigma=2)\n        plt.plot(signal_smooth, label=method.capitalize(), color=color, linewidth=2, alpha=0.7)\n    \n    plt.title('🔬 Comparison of Different Extraction Methods', fontsize=16, fontweight='bold')\n    plt.xlabel('Sample Index', fontsize=12)\n    plt.ylabel('Amplitude', fontsize=12)\n    plt.legend(fontsize=12)\n    plt.grid(True, alpha=0.3)\n    plt.tight_layout()\n    plt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-04T19:35:23.123948Z","iopub.execute_input":"2025-11-04T19:35:23.124375Z","iopub.status.idle":"2025-11-04T19:35:26.418679Z","shell.execute_reply.started":"2025-11-04T19:35:23.124333Z","shell.execute_reply":"2025-11-04T19:35:26.417687Z"}},"outputs":[],"execution_count":null},{"id":"5373fc7f-9205-4939-9f7f-f3b7c4fbeb1b","cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}