{"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":"nvidiaTeslaT4","dataSources":[{"sourceId":97984,"databundleVersionId":14096757,"sourceType":"competition"}],"dockerImageVersionId":31154,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import pandas as pd\nimport numpy as np\nimport matplotlib.pyplot as plt\nimport seaborn as sns\nfrom pathlib import Path\nfrom PIL import Image\nimport cv2\nimport warnings\n\nwarnings.filterwarnings('ignore')\n\nimport os\nimport torch\nimport torch.nn as nn\nimport albumentations as A\nimport torch.optim as optim\nfrom torchvision import datasets, transforms, models\nfrom scipy import signal as scipy_signal\nfrom albumentations.pytorch import ToTensorV2\nfrom torch.utils.data import Dataset, DataLoader\nfrom sklearn.metrics import classification_report\nfrom tqdm import tqdm\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\ndevice","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-10-22T14:06:39.471519Z","iopub.execute_input":"2025-10-22T14:06:39.471795Z","iopub.status.idle":"2025-10-22T14:06:50.885313Z","shell.execute_reply.started":"2025-10-22T14:06:39.471774Z","shell.execute_reply":"2025-10-22T14:06:50.884553Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Dataset loaded","metadata":{}},{"cell_type":"code","source":"# Set paths\nDATA_PATH = Path('/kaggle/input/physionet-ecg-image-digitization')\nTRAIN_PATH = DATA_PATH / 'train'\n\n# Load metadata\ntrain_df = pd.read_csv(DATA_PATH / 'train.csv')\nprint(f\"Loaded {len(train_df)} training samples\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-22T14:06:50.886516Z","iopub.execute_input":"2025-10-22T14:06:50.886945Z","iopub.status.idle":"2025-10-22T14:06:50.902409Z","shell.execute_reply.started":"2025-10-22T14:06:50.886923Z","shell.execute_reply":"2025-10-22T14:06:50.901688Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Exploratory Data Analysis\n## Analyze Image Dimensions and Properties","metadata":{}},{"cell_type":"code","source":"# Sample a few images and check their properties\nsample_ids = train_df['id'].head(10).tolist()\nimage_properties = []\n\nfor sample_id in sample_ids:\n    sample_dir = TRAIN_PATH / str(sample_id)\n    # Check the original image (0001)\n    img_path = sample_dir / f\"{sample_id}-0001.png\"\n\n    if img_path.exists():\n        img = Image.open(img_path)\n        img_array = np.array(img)\n\n        image_properties.append({\n            'id': sample_id,\n            'width': img.size[0],\n            'height': img.size[1],\n            'mode': img.mode,\n            'channels': img_array.shape[2] if len(img_array.shape) == 3 else 1,\n            'dtype': img_array.dtype,\n            'min_val': img_array.min(),\n            'max_val': img_array.max(),\n            'mean_val': img_array.mean()\n        })\n\nprops_df = pd.DataFrame(image_properties)\nprint(\"\\nImage Properties Summary:\")\nprint(props_df)\n\nprint(\"\\nImage Dimensions Distribution:\")\nprint(props_df[['width', 'height']].describe())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-22T14:06:50.903380Z","iopub.execute_input":"2025-10-22T14:06:50.903767Z","iopub.status.idle":"2025-10-22T14:06:52.094896Z","shell.execute_reply.started":"2025-10-22T14:06:50.903741Z","shell.execute_reply":"2025-10-22T14:06:52.094008Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Visualize Image Structure with Annotations","metadata":{}},{"cell_type":"code","source":"# Load a sample image and analyze its structure\nsample_id = str(train_df['id'].iloc[0])\nsample_dir = TRAIN_PATH / sample_id\nimg_path = sample_dir / f\"{sample_id}-0001.png\"\n\n# Load with PIL and OpenCV\nimg_pil = Image.open(img_path)\nimg_cv = cv2.imread(str(img_path))\nimg_rgb = cv2.cvtColor(img_cv, cv2.COLOR_BGR2RGB)\nimg_gray = cv2.cvtColor(img_cv, cv2.COLOR_BGR2GRAY)\n\nprint(f\"Sample ID: {sample_id}\")\nprint(f\"Image shape (RGB): {img_rgb.shape}\")\nprint(f\"Image shape (Gray): {img_gray.shape}\")\n\n# Display original and grayscale\nfig, axes = plt.subplots(1, 2, figsize=(18, 8))\n\naxes[0].imshow(img_rgb)\naxes[0].set_title('Original ECG Image (RGB)')\naxes[0].axis('off')\n\naxes[1].imshow(img_gray, cmap='gray')\naxes[1].set_title('Grayscale ECG Image')\naxes[1].axis('off')\n\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-22T14:06:52.096507Z","iopub.execute_input":"2025-10-22T14:06:52.096735Z","iopub.status.idle":"2025-10-22T14:06:53.570256Z","shell.execute_reply.started":"2025-10-22T14:06:52.096716Z","shell.execute_reply":"2025-10-22T14:06:53.569404Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Analyze Horizontal and Vertical Projections","metadata":{}},{"cell_type":"code","source":"# Horizontal and vertical projections can help identify grid lines and lead regions\nhorizontal_projection = np.sum(img_gray, axis=1)\nvertical_projection = np.sum(img_gray, axis=0)\n\nfig, axes = plt.subplots(2, 2, figsize=(18, 10))\n\n# Show image with projections\naxes[0, 0].imshow(img_gray, cmap='gray')\naxes[0, 0].set_title('ECG Image')\naxes[0, 0].axis('off')\n\n# Horizontal projection\naxes[0, 1].plot(horizontal_projection, range(len(horizontal_projection)))\naxes[0, 1].set_ylim(len(horizontal_projection), 0)\naxes[0, 1].set_title('Horizontal Projection')\naxes[0, 1].set_xlabel('Sum of pixel intensities')\naxes[0, 1].set_ylabel('Row index')\naxes[0, 1].grid(True, alpha=0.3)\n\n# Vertical projection\naxes[1, 0].plot(vertical_projection)\naxes[1, 0].set_title('Vertical Projection')\naxes[1, 0].set_xlabel('Column index')\naxes[1, 0].set_ylabel('Sum of pixel intensities')\naxes[1, 0].grid(True, alpha=0.3)\n\n# Combined view\naxes[1, 1].imshow(img_gray, cmap='gray')\naxes[1, 1].set_title('ECG Image')\naxes[1, 1].set_xlabel('Column index')\naxes[1, 1].set_ylabel('Row index')\n\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-22T14:06:53.571316Z","iopub.execute_input":"2025-10-22T14:06:53.571668Z","iopub.status.idle":"2025-10-22T14:06:54.870033Z","shell.execute_reply.started":"2025-10-22T14:06:53.571647Z","shell.execute_reply":"2025-10-22T14:06:54.869128Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Detect Grid Lines","metadata":{}},{"cell_type":"code","source":"# Try to detect horizontal and vertical grid lines using edge detection\nedges = cv2.Canny(img_gray, 50, 150)\n\n# Detect lines using Hough Transform\nlines = cv2.HoughLinesP(edges, 1, np.pi/180, threshold=100, minLineLength=100, maxLineGap=10)\n\n# Draw detected lines\nimg_with_lines = img_rgb.copy()\nif lines is not None:\n    print(f\"Detected {len(lines)} line segments\")\n\n    # Separate horizontal and vertical lines\n    horizontal_lines = []\n    vertical_lines = []\n\n    for line in lines:\n        x1, y1, x2, y2 = line[0]\n        angle = np.abs(np.arctan2(y2 - y1, x2 - x1) * 180 / np.pi)\n\n        if angle < 10 or angle > 170:  # Horizontal\n            horizontal_lines.append(line)\n            cv2.line(img_with_lines, (x1, y1), (x2, y2), (255, 0, 0), 1)\n        elif 80 < angle < 100:  # Vertical\n            vertical_lines.append(line)\n            cv2.line(img_with_lines, (x1, y1), (x2, y2), (0, 255, 0), 1)\n\n    print(f\"Horizontal lines: {len(horizontal_lines)}\")\n    print(f\"Vertical lines: {len(vertical_lines)}\")\n\nfig, axes = plt.subplots(1, 3, figsize=(20, 7))\n\naxes[0].imshow(img_gray, cmap='gray')\naxes[0].set_title('Original Image')\naxes[0].axis('off')\n\naxes[1].imshow(edges, cmap='gray')\naxes[1].set_title('Edge Detection (Canny)')\naxes[1].axis('off')\n\naxes[2].imshow(img_with_lines)\naxes[2].set_title(f'Detected Lines (H: blue, V: green)')\naxes[2].axis('off')\n\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-22T14:06:54.870914Z","iopub.execute_input":"2025-10-22T14:06:54.871391Z","iopub.status.idle":"2025-10-22T14:06:56.619767Z","shell.execute_reply.started":"2025-10-22T14:06:54.871370Z","shell.execute_reply":"2025-10-22T14:06:56.618763Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Analyze Color Channels","metadata":{}},{"cell_type":"code","source":"# Analyze RGB channels separately - ECG signals might be in specific channels\nr_channel = img_rgb[:, :, 0]\ng_channel = img_rgb[:, :, 1]\nb_channel = img_rgb[:, :, 2]\n\nfig, axes = plt.subplots(2, 3, figsize=(18, 12))\n\n# Show each channel\naxes[0, 0].imshow(r_channel, cmap='Reds')\naxes[0, 0].set_title('Red Channel')\naxes[0, 0].axis('off')\n\naxes[0, 1].imshow(g_channel, cmap='Greens')\naxes[0, 1].set_title('Green Channel')\naxes[0, 1].axis('off')\n\naxes[0, 2].imshow(b_channel, cmap='Blues')\naxes[0, 2].set_title('Blue Channel')\naxes[0, 2].axis('off')\n\n# Histograms\naxes[1, 0].hist(r_channel.flatten(), bins=50, color='red', alpha=0.7)\naxes[1, 0].set_title('Red Channel Histogram')\naxes[1, 0].set_xlabel('Pixel Value')\naxes[1, 0].set_ylabel('Frequency')\n\naxes[1, 1].hist(g_channel.flatten(), bins=50, color='green', alpha=0.7)\naxes[1, 1].set_title('Green Channel Histogram')\naxes[1, 1].set_xlabel('Pixel Value')\naxes[1, 1].set_ylabel('Frequency')\n\naxes[1, 2].hist(b_channel.flatten(), bins=50, color='blue', alpha=0.7)\naxes[1, 2].set_title('Blue Channel Histogram')\naxes[1, 2].set_xlabel('Pixel Value')\naxes[1, 2].set_ylabel('Frequency')\n\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-22T14:06:56.620688Z","iopub.execute_input":"2025-10-22T14:06:56.621022Z","iopub.status.idle":"2025-10-22T14:06:58.943566Z","shell.execute_reply.started":"2025-10-22T14:06:56.620995Z","shell.execute_reply":"2025-10-22T14:06:58.942750Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Compare Multiple Image Types","metadata":{}},{"cell_type":"code","source":"# Compare different image types (original, scanned, photographed, damaged)\nimage_types_to_check = ['0005', '0006', '0004', '0001', '0010']\nimage_type_names = {\n    '0005': 'Original',\n    '0002': 'Color Scan',\n    '0004': 'B&W Scan',\n    '0001': 'Mobile Photo',\n    '0010': 'Damaged'\n}\n\nfig, axes = plt.subplots(len(image_types_to_check), 2, figsize=(18, 4*len(image_types_to_check)))\n\nfor idx, img_type in enumerate(image_types_to_check):\n    img_path = sample_dir / f\"{sample_id}-{img_type}.png\"\n\n    if img_path.exists():\n        img = cv2.imread(str(img_path))\n        img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)\n        img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)\n\n        # Show RGB\n        axes[idx, 0].imshow(img_rgb)\n        axes[idx, 0].set_title(f'{image_type_names.get(img_type, img_type)} - RGB')\n        axes[idx, 0].axis('off')\n\n        # Show grayscale histogram\n        axes[idx, 1].hist(img_gray.flatten(), bins=50, alpha=0.7)\n        axes[idx, 1].set_title(f'{image_type_names.get(img_type, img_type)} - Intensity Histogram')\n        axes[idx, 1].set_xlabel('Pixel Value')\n        axes[idx, 1].set_ylabel('Frequency')\n        axes[idx, 1].grid(True, alpha=0.3)\n    else:\n        axes[idx, 0].text(0.5, 0.5, 'Image not found', ha='center', va='center')\n        axes[idx, 0].axis('off')\n        axes[idx, 1].text(0.5, 0.5, 'Image not found', ha='center', va='center')\n        axes[idx, 1].axis('off')\n\nplt.tight_layout()\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-22T14:06:58.944404Z","iopub.execute_input":"2025-10-22T14:06:58.944648Z","iopub.status.idle":"2025-10-22T14:07:07.909741Z","shell.execute_reply.started":"2025-10-22T14:06:58.944627Z","shell.execute_reply":"2025-10-22T14:07:07.908679Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Cropped Region Analysis","metadata":{}},{"cell_type":"code","source":"# Take a closer look at a small region to see grid and signal details\n# Crop a region from the middle of the image\ncrop_height = 400\ncrop_width = 800\nstart_y = (img_gray.shape[0] - crop_height) // 2\nstart_x = (img_gray.shape[1] - crop_width) // 2\n\ncropped = img_rgb[start_y:start_y+crop_height, start_x:start_x+crop_width]\ncropped_gray = img_gray[start_y:start_y+crop_height, start_x:start_x+crop_width]\n\nfig, axes = plt.subplots(2, 2, figsize=(16, 12))\n\n# Show crop location\naxes[0, 0].imshow(img_rgb)\naxes[0, 0].add_patch(plt.Rectangle((start_x, start_y), crop_width, crop_height,\n                                   fill=False, edgecolor='red', linewidth=2))\naxes[0, 0].set_title('Full Image with Crop Region')\naxes[0, 0].axis('off')\n\n# Show cropped region\naxes[0, 1].imshow(cropped)\naxes[0, 1].set_title('Cropped Region (Color)')\naxes[0, 1].axis('off')\n\n# Show cropped grayscale\naxes[1, 0].imshow(cropped_gray, cmap='gray')\naxes[1, 0].set_title('Cropped Region (Grayscale)')\naxes[1, 0].axis('off')\n\n# Show inverted (dark signals on light background)\naxes[1, 1].imshow(255 - cropped_gray, cmap='gray')\naxes[1, 1].set_title('Cropped Region (Inverted)')\naxes[1, 1].axis('off')\n\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-22T14:07:07.910835Z","iopub.execute_input":"2025-10-22T14:07:07.911448Z","iopub.status.idle":"2025-10-22T14:07:10.444000Z","shell.execute_reply.started":"2025-10-22T14:07:07.911420Z","shell.execute_reply":"2025-10-22T14:07:10.442964Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# ECG image preprocessor","metadata":{}},{"cell_type":"code","source":"class Config:\n    lr = 3e-4\n    num_workers = 4\n    img_size = (1024, 1024)\n    target_length = 5000\n\nconfig = Config()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-22T14:07:10.447678Z","iopub.execute_input":"2025-10-22T14:07:10.448061Z","iopub.status.idle":"2025-10-22T14:07:10.452683Z","shell.execute_reply.started":"2025-10-22T14:07:10.448035Z","shell.execute_reply":"2025-10-22T14:07:10.451907Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class ECGImageProcessor:\n    \n    def __init__(self):\n        self.lead_positions = {\n            'I': (0.1, 0.15), 'II': (0.1, 0.3), 'III': (0.1, 0.45),\n            'aVR': (0.1, 0.6), 'aVL': (0.1, 0.75), 'aVF': (0.1, 0.9),\n            'V1': (0.55, 0.15), 'V2': (0.55, 0.3), 'V3': (0.55, 0.45),\n            'V4': (0.55, 0.6), 'V5': (0.55, 0.75), 'V6': (0.55, 0.9)\n        }\n    \n    def preprocess_image(self, image):\n        \"\"\"Basic ECG image preprocessing.\"\"\"\n        # Convert to grayscale\n        if len(image.shape) == 3:\n            gray = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY)\n        else:\n            gray = image\n        \n        # Enhance contrast\n        clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8,8))\n        enhanced = clahe.apply(gray)\n        \n        # Remove grid lines\n        kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (2,2))\n        opened = cv2.morphologyEx(enhanced, cv2.MORPH_OPEN, kernel)\n        \n        # Binarize\n        _, binary = cv2.threshold(opened, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)\n        \n        return binary\n    \n    def extract_lead_signal(self, image, lead_name, fs=500):\n        \"\"\"Extracting a specific lead signal.\"\"\"\n        try:\n            # Get lead position\n            h, w = image.shape[:2]\n            x_ratio, y_ratio = self.lead_positions[lead_name]\n            lead_x = int(w * x_ratio)\n            lead_y = int(h * y_ratio)\n            \n            # Define ROI around lead\n            roi_width = int(w * 0.4)\n            roi_height = int(h * 0.08)\n            roi_x = max(0, lead_x - roi_width//2)\n            roi_y = max(0, lead_y - roi_height//2)\n            \n            roi = image[roi_y:roi_y+roi_height, roi_x:roi_x+roi_width]\n            \n            if roi.size == 0:\n                return np.zeros(config.target_length)\n            \n            # Find signal line (dark pixels)\n            signal_y = []\n            for col in range(roi.shape[1]):\n                column = roi[:, col]\n                dark_pixels = np.where(column < 128)[0]\n                if len(dark_pixels) > 0:\n                    signal_y.append(np.mean(dark_pixels))\n                else:\n                    signal_y.append(roi.shape[0] / 2)\n            \n            if not signal_y:\n                return np.zeros(config.target_length)\n            \n            # Convert to signal\n            ecg_signal = np.array(signal_y)\n            \n            # Invert and normalize\n            ecg_signal = roi_height - ecg_signal  # Invert y-axis\n            ecg_signal = (ecg_signal - ecg_signal.mean()) / (ecg_signal.std() + 1e-8)\n            \n            # Resample to target length\n            if len(ecg_signal) > 0:\n                ecg_signal = scipy_signal.resample(ecg_signal, config.target_length)\n            \n            return ecg_signal.astype(np.float32)\n            \n        except Exception as e:\n            print(f\"Error extracting {lead_name}: {e}\")\n            return np.zeros(config.target_length)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-22T14:07:10.453523Z","iopub.execute_input":"2025-10-22T14:07:10.453746Z","iopub.status.idle":"2025-10-22T14:07:10.476380Z","shell.execute_reply.started":"2025-10-22T14:07:10.453730Z","shell.execute_reply":"2025-10-22T14:07:10.475690Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## ECG Image Dataset","metadata":{}},{"cell_type":"code","source":"class ECGDataset(Dataset):\n    \n    def __init__(self, df, image_dir, transform=None, is_train=True):\n        self.df = df\n        self.image_dir = image_dir\n        self.transform = transform\n        self.is_train = is_train\n        self.processor = ECGImageProcessor()\n        self.leads = ['I', 'II', 'III', 'aVR', 'aVL', 'aVF', 'V1', 'V2', 'V3', 'V4', 'V5', 'V6']\n        \n    def __len__(self):\n        return len(self.df)\n    \n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n        base_id = row['id']\n        \n        # Load image\n        if self.is_train:\n            img_path = os.path.join(self.image_dir, str(base_id), f\"{base_id}-0001.png\")\n        else:\n            img_path = os.path.join(self.image_dir, f\"{base_id}.png\")\n            \n        image = cv2.imread(img_path)\n        if image is None:\n            # Try alternative images\n            if self.is_train:\n                for seg in ['0003', '0004', '0005']:\n                    img_path = os.path.join(self.image_dir, str(base_id), f\"{base_id}-{seg}.png\")\n                    image = cv2.imread(img_path)\n                    if image is not None:\n                        break\n            \n        if image is None:\n            # Create dummy image as last resort\n            image = np.ones((1024, 1024, 3), dtype=np.uint8) * 255 # img_size\n            print(f\"Could not load image for {base_id}\")\n        \n        image_rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)\n        \n        if self.is_train:\n            # Load ground truth from CSV\n            csv_path = os.path.join(self.image_dir, str(base_id), f\"{base_id}.csv\")\n            try:\n                signals_df = pd.read_csv(csv_path)\n                # Use lead II as target for training\n                target_signal = signals_df['II'].values.astype(np.float32)\n                \n                # Resize to target length\n                if len(target_signal) > config.target_length:\n                    target_signal = target_signal[:config.target_length]\n                else:\n                    target_signal = np.pad(target_signal, (0, config.target_length - len(target_signal)), \n                                         mode='constant')\n                \n                # Normalize\n                if target_signal.std() > 0:\n                    target_signal = (target_signal - target_signal.mean()) / target_signal.std()\n                    \n            except Exception as e:\n                print(f\"Error loading CSV for {base_id}: {e}\")\n                # Create synthetic ECG as fallback\n                t = np.linspace(0, 10, config.target_length)\n                target_signal = (np.sin(2 * np.pi * 1 * t) + \n                               0.5 * np.sin(2 * np.pi * 2 * t) +\n                               0.2 * np.sin(2 * np.pi * 0.5 * t))\n                target_signal = target_signal.astype(np.float32)\n            \n            # Process image to extract features\n            processed_img = self.processor.preprocess_image(image_rgb)\n            \n            # Extract lead II signal from image (this will be our input feature)\n            extracted_signal = self.processor.extract_lead_signal(processed_img, 'II')\n            \n            # Prepare image for CNN\n            if self.transform:\n                image_tensor = self.transform(image=image_rgb)['image']\n            else:\n                # Default transform\n                image_tensor = torch.from_numpy(\n                    cv2.resize(image_rgb, config.img_size).transpose(2, 0, 1)\n                ).float() / 255.0\n            \n            return image_tensor, torch.FloatTensor(extracted_signal), torch.FloatTensor(target_signal), base_id\n            \n        else:\n            # For test - just return image\n            if self.transform:\n                image_tensor = self.transform(image=image_rgb)['image']\n            else:\n                image_tensor = torch.from_numpy(\n                    cv2.resize(image_rgb, config.img_size).transpose(2, 0, 1)\n                ).float() / 255.0\n            \n            return image_tensor, base_id","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-22T14:07:10.477261Z","iopub.execute_input":"2025-10-22T14:07:10.477501Z","iopub.status.idle":"2025-10-22T14:07:10.493610Z","shell.execute_reply.started":"2025-10-22T14:07:10.477484Z","shell.execute_reply":"2025-10-22T14:07:10.492843Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Custom CNN model ( Image to Signal Regression)","metadata":{}},{"cell_type":"code","source":"# convolution block with BatchNormalization\ndef ConvBlock(in_channels, out_channels, pool=False):\n    layers = [nn.Conv2d(in_channels, out_channels, kernel_size=3, padding=1),\n             nn.BatchNorm2d(out_channels),\n             nn.ReLU(inplace=True)]\n    if pool:\n        layers.append(nn.MaxPool2d(4))\n    return nn.Sequential(*layers)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-22T14:07:10.494360Z","iopub.execute_input":"2025-10-22T14:07:10.494589Z","iopub.status.idle":"2025-10-22T14:07:10.512190Z","shell.execute_reply.started":"2025-10-22T14:07:10.494573Z","shell.execute_reply":"2025-10-22T14:07:10.511424Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# resnet architecture \nclass ECGNet(nn.Module):\n    def __init__(self):\n        super().__init__()\n        \n        self.conv1 = ConvBlock(3, 64)\n        self.conv2 = ConvBlock(64, 128, pool=True) \n        self.res1 = nn.Sequential(ConvBlock(128, 128), ConvBlock(128, 128))\n        \n        self.conv3 = ConvBlock(128, 256, pool=True) \n        self.conv4 = ConvBlock(256, 512, pool=True)\n        #self.conv5 = ConvBlock(256, 256, pool=True)\n        #self.conv6 = ConvBlock(256, 512, pool=True)\n        #self.conv7 = ConvBlock(512, 512, pool=True)\n        \n        self.res2 = nn.Sequential(ConvBlock(512, 512), ConvBlock(512, 512))\n\n        # self.classifier = nn.Sequential(nn.MaxPool2d(4),\n        #                                nn.Flatten(),\n        #                                nn.Linear(512, num_diseases))\n        \n        self.classifier = nn.Sequential(\n                nn.AdaptiveAvgPool2d((1, 1)),  # Safe replacement\n                nn.Flatten(),\n                nn.Linear(512, config.target_length)\n        )\n        \n    def forward(self, x): # x is the loaded batch\n        out = self.conv1(x)\n        out = self.conv2(out)\n        out = self.res1(out) + out\n        out = self.conv3(out)\n        out = self.conv4(out)\n        #out = self.conv5(out)\n        #out = self.conv6(out)\n        #out = self.conv7(out)\n        out = self.res2(out) + out\n        out = self.classifier(out)\n        \n        return out        ","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-22T14:15:15.274501Z","iopub.execute_input":"2025-10-22T14:15:15.274738Z","iopub.status.idle":"2025-10-22T14:15:15.280946Z","shell.execute_reply.started":"2025-10-22T14:15:15.274723Z","shell.execute_reply":"2025-10-22T14:15:15.280286Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## ECG loss ","metadata":{}},{"cell_type":"code","source":"class ECGLoss(nn.Module):\n    \n    def __init__(self):\n        super().__init__()\n        self.mse = nn.MSELoss()\n        self.mae = nn.L1Loss()\n        \n    def forward(self, pred, target):\n        mse_loss = self.mse(pred, target)\n        mae_loss = self.mae(pred, target)\n        \n        # Frequency domain\n        pred_fft = torch.fft.fft(pred, dim=1)\n        target_fft = torch.fft.fft(target, dim=1)\n        freq_loss = torch.mean(torch.abs(pred_fft - target_fft))\n        \n        # Loss of correlation\n        correlation_loss = 1 - torch.cosine_similarity(pred, target, dim=1).mean()\n        \n        # Loss for characteristic ECG points\n        peak_loss = self._ecg_characteristic_loss(pred, target)\n        \n        return (0.4 * mse_loss + 0.2 * mae_loss + \n                0.2 * freq_loss + 0.1 * correlation_loss + 0.1 * peak_loss)\n    \n    def _ecg_characteristic_loss(self, pred, target):\n        # Focusing on important areas of the ECG (QRS complexes)\n        return self.mae(pred, target)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-22T14:09:38.478274Z","iopub.execute_input":"2025-10-22T14:09:38.478549Z","iopub.status.idle":"2025-10-22T14:09:38.484616Z","shell.execute_reply.started":"2025-10-22T14:09:38.478528Z","shell.execute_reply":"2025-10-22T14:09:38.483828Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_df = pd.read_csv('/kaggle/input/physionet-ecg-image-digitization/train.csv')\ntest_df = pd.read_csv('/kaggle/input/physionet-ecg-image-digitization/test.csv')\n\ntrain_df = train_df.head(1000)  # higher -> better results\n    \nH, W = config.img_size  # unpack\ntrain_transform = A.Compose([\n    A.Resize(height=H, width=W),\n    A.HorizontalFlip(p=0.3),\n    A.ShiftScaleRotate(shift_limit=0.05, scale_limit=0.1, rotate_limit=5, p=0.3),\n    A.GridDistortion(p=0.1),  # imitation of paper distortion\n    A.GaussNoise(p=0.2),\n    A.RandomBrightnessContrast(p=0.3),\n    A.MotionBlur(p=0.1),      # simulate blurring when photographing\n    A.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),\n    ToTensorV2()\n])\n    \n# Dataset\ntrain_dataset = ECGDataset(\n    train_df, \n    '/kaggle/input/physionet-ecg-image-digitization/train',\n    transform=train_transform,\n    is_train=True\n)\n    \ntrain_loader = DataLoader(\n    train_dataset, \n    batch_size=4, \n    shuffle=True, \n    num_workers=4\n)\n    \n# Model\nresnet_model = ECGNet().to(device)\n    \n# Loss and optimizer\ncriterion = ECGLoss()\noptimizer = optim.AdamW(resnet_model.parameters(), lr=config.lr, weight_decay=1e-4)\nscheduler = optim.lr_scheduler.ReduceLROnPlateau(optimizer, patience=6, factor=0.5)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-22T14:19:46.090528Z","iopub.execute_input":"2025-10-22T14:19:46.091314Z","iopub.status.idle":"2025-10-22T14:19:46.198493Z","shell.execute_reply.started":"2025-10-22T14:19:46.091286Z","shell.execute_reply":"2025-10-22T14:19:46.197912Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Model training","metadata":{}},{"cell_type":"code","source":"best_loss = float('inf')\nepochs =20\nfor epoch in range(epochs):\n    resnet_model.train()\n    running_loss = 0.0\n        \n    for batch_idx, (images, extracted, targets, base_ids) in tqdm(enumerate(train_loader), desc=f\"Epoch {epoch+1}/{epochs}\"):\n        images = images.to(device)\n        targets = targets.to(device)\n            \n        optimizer.zero_grad()\n        outputs = resnet_model(images)\n        loss = criterion(outputs, targets)\n        loss.backward()\n            \n        torch.nn.utils.clip_grad_norm_(resnet_model.parameters(), 1.0)\n        optimizer.step()\n            \n        running_loss += loss.item()\n        \n    epoch_loss = running_loss / len(train_loader)\n    scheduler.step(epoch_loss)\n        \n    print(f'Epoch {epoch+1}/{epochs}, Loss: {epoch_loss:.4f}')\n        \n    if epoch_loss < best_loss:\n        best_loss = epoch_loss\n        torch.save(resnet_model.state_dict(), 'best_ecg_model.pth')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-22T14:27:44.811744Z","iopub.execute_input":"2025-10-22T14:27:44.812581Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Predict, create submission and check results","metadata":{}},{"cell_type":"code","source":"resnet_model.load_state_dict(torch.load('best_ecg_model.pth', map_location=device)) # Load best weights\ntest_df = pd.read_csv('/kaggle/input/physionet-ecg-image-digitization/test.csv')\n    \n# Test transform\ntest_transform = A.Compose([\n    A.Resize(*config.img_size),\n    A.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),\n    ToTensorV2()\n])\n    \ntest_dataset = ECGDataset(\n    test_df, \n    '/kaggle/input/physionet-ecg-image-digitization/test',\n    transform=test_transform,\n    is_train=False\n)\n\n\ntest_loader = DataLoader(test_dataset, batch_size=2, shuffle=False)\n    \nsubmission_data = []\nprocessor = ECGImageProcessor()\n    \nresnet_model.eval()\n    \nfor batch_idx, (images, base_ids) in enumerate(test_loader):\n    if batch_idx >= len(test_df):  # Safety check\n        break\n            \n    test_row = test_df.iloc[batch_idx]\n    base_id = test_row['id']\n    lead = test_row['lead']\n    num_rows = test_row['number_of_rows']\n        \n    images = images.to(device)\n        \n    with torch.no_grad():\n        prediction = resnet_model(images).cpu().numpy().flatten()\n        \n    # Adjust length to required number of rows\n    if len(prediction) > num_rows:\n        prediction = prediction[:num_rows]\n    elif len(prediction) < num_rows:\n        prediction = np.pad(prediction, (0, num_rows - len(prediction)), mode='edge')\n        \n    # Add some realistic variation based on lead type\n    if lead in ['V1', 'V2', 'V3', 'V4', 'V5', 'V6']:\n        # Chest leads have different characteristics\n        prediction = prediction * 0.8 + np.random.normal(0, 0.05, len(prediction))\n        \n    for row_id in range(num_rows):\n        composite_id = f\"{base_id}_{row_id}_{lead}\"\n        submission_data.append({\n            'id': composite_id,\n            'value': float(prediction[row_id])\n        })\n    \n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Create submission\nsubmission_df = pd.DataFrame(submission_data)\nsubmission_df.to_csv('submission.csv', index=False)\nsubmission_df.head(30)","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}