{"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"},{"sourceId":13746387,"sourceType":"datasetVersion","datasetId":8747012},{"sourceId":13816899,"sourceType":"datasetVersion","datasetId":8620533},{"sourceId":271051632,"sourceType":"kernelVersion"},{"sourceId":677607,"sourceType":"modelInstanceVersion","modelInstanceId":513841,"modelId":528480}],"dockerImageVersionId":31153,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"<div style=\"\n    background: linear-gradient(135deg, #0c0c2e 0%, #1a1a4a 50%, #2d1a4a 100%);\n    border: 2px solid #6366f1;\n    border-radius: 15px;\n    padding: 20px;\n    margin: 15px 0;\n    box-shadow: 0 0 25px rgba(99, 102, 241, 0.3),\n                inset 0 0 15px rgba(255, 255, 255, 0.1);\n    color: #e2e8f0;\n    font-family: 'Segoe UI', system-ui, sans-serif;\n    position: relative;\n    overflow: hidden;\n\">\n\n<div style=\"\n    position: absolute;\n    bottom: -30px;\n    left: -30px;\n    width: 80px;\n    height: 80px;\n    background: radial-gradient(circle, rgba(168, 85, 247, 0.3) 0%, transparent 70%);\n    border-radius: 50%;\n\"></div>\n\n<h1 style=\"\n    color: #818cf8;\n    margin-top: 0;\n    text-align: center;\n    font-weight: 600;\n    text-shadow: 0 0 10px rgba(129, 140, 248, 0.5);\n    position: relative;\n    z-index: 1;\n\">\n    Import libraries\n</h1>","metadata":{}},{"cell_type":"code","source":"import os\nimport cv2\nimport torch\nimport numpy as np\nimport pandas as pd\nimport torch.nn as nn\nimport albumentations as A\nimport torch.optim as optim\n\nfrom scipy import signal as scipy_signal\nfrom albumentations.pytorch import ToTensorV2\nfrom torch.utils.data import Dataset, DataLoader","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-22T14:15:06.710994Z","iopub.execute_input":"2025-12-22T14:15:06.711264Z","iopub.status.idle":"2025-12-22T14:15:11.270010Z","shell.execute_reply.started":"2025-12-22T14:15:06.711237Z","shell.execute_reply":"2025-12-22T14:15:11.269237Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"<div style=\"\n    background: linear-gradient(135deg, #0c0c2e 0%, #1a1a4a 50%, #2d1a4a 100%);\n    border: 2px solid #6366f1;\n    border-radius: 15px;\n    padding: 20px;\n    margin: 15px 0;\n    box-shadow: 0 0 25px rgba(99, 102, 241, 0.3),\n                inset 0 0 15px rgba(255, 255, 255, 0.1);\n    color: #e2e8f0;\n    font-family: 'Segoe UI', system-ui, sans-serif;\n    position: relative;\n    overflow: hidden;\n\">\n\n<div style=\"\n    position: absolute;\n    bottom: -30px;\n    left: -30px;\n    width: 80px;\n    height: 80px;\n    background: radial-gradient(circle, rgba(168, 85, 247, 0.3) 0%, transparent 70%);\n    border-radius: 50%;\n\"></div>\n\n<h1 style=\"\n    color: #818cf8;\n    margin-top: 0;\n    text-align: center;\n    font-weight: 600;\n    text-shadow: 0 0 10px rgba(129, 140, 248, 0.5);\n    position: relative;\n    z-index: 1;\n\">\n    Config(u can change parameters)\n</h1>","metadata":{}},{"cell_type":"code","source":"class Config:\n    batch_size = 10\n    epochs = 1\n    lr = 1e-4\n    num_workers = 2\n    img_size = (1240, 1024)\n    target_length = 5000\n\nconfig = Config()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-22T14:15:11.271116Z","iopub.execute_input":"2025-12-22T14:15:11.271512Z","iopub.status.idle":"2025-12-22T14:15:11.275613Z","shell.execute_reply.started":"2025-12-22T14:15:11.271489Z","shell.execute_reply":"2025-12-22T14:15:11.274805Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"<div style=\"\n    background: linear-gradient(135deg, #0c0c2e 0%, #1a1a4a 50%, #2d1a4a 100%);\n    border: 2px solid #6366f1;\n    border-radius: 15px;\n    padding: 20px;\n    margin: 15px 0;\n    box-shadow: 0 0 25px rgba(99, 102, 241, 0.3),\n                inset 0 0 15px rgba(255, 255, 255, 0.1);\n    color: #e2e8f0;\n    font-family: 'Segoe UI', system-ui, sans-serif;\n    position: relative;\n    overflow: hidden;\n\">\n\n<div style=\"\n    position: absolute;\n    bottom: -30px;\n    left: -30px;\n    width: 80px;\n    height: 80px;\n    background: radial-gradient(circle, rgba(168, 85, 247, 0.3) 0%, transparent 70%);\n    border-radius: 50%;\n\"></div>\n\n<h1 style=\"\n    color: #818cf8;\n    margin-top: 0;\n    text-align: center;\n    font-weight: 600;\n    text-shadow: 0 0 10px rgba(129, 140, 248, 0.5);\n    position: relative;\n    z-index: 1;\n\">\n    ECG image processor: preprocessing + extraction of 12-lead signals\n</h1>","metadata":{}},{"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":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-12-22T14:15:11.276450Z","iopub.execute_input":"2025-12-22T14:15:11.276669Z","iopub.status.idle":"2025-12-22T14:15:11.419103Z","shell.execute_reply.started":"2025-12-22T14:15:11.276654Z","shell.execute_reply":"2025-12-22T14:15:11.418507Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"<div style=\"\n    background: linear-gradient(135deg, #0c0c2e 0%, #1a1a4a 50%, #2d1a4a 100%);\n    border: 2px solid #6366f1;\n    border-radius: 15px;\n    padding: 20px;\n    margin: 15px 0;\n    box-shadow: 0 0 25px rgba(99, 102, 241, 0.3),\n                inset 0 0 15px rgba(255, 255, 255, 0.1);\n    color: #e2e8f0;\n    font-family: 'Segoe UI', system-ui, sans-serif;\n    position: relative;\n    overflow: hidden;\n\">\n\n<div style=\"\n    position: absolute;\n    bottom: -30px;\n    left: -30px;\n    width: 80px;\n    height: 80px;\n    background: radial-gradient(circle, rgba(168, 85, 247, 0.3) 0%, transparent 70%);\n    border-radius: 50%;\n\"></div>\n\n<h1 style=\"\n    color: #818cf8;\n    margin-top: 0;\n    text-align: center;\n    font-weight: 600;\n    text-shadow: 0 0 10px rgba(129, 140, 248, 0.5);\n    position: relative;\n    z-index: 1;\n\">\n    Electrocardiography Dataset\n</h1>","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((1240, 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-12-22T14:15:11.420846Z","iopub.execute_input":"2025-12-22T14:15:11.421104Z","iopub.status.idle":"2025-12-22T14:15:11.433843Z","shell.execute_reply.started":"2025-12-22T14:15:11.421087Z","shell.execute_reply":"2025-12-22T14:15:11.433247Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"<div style=\"\n    background: linear-gradient(135deg, #0c0c2e 0%, #1a1a4a 50%, #2d1a4a 100%);\n    border: 2px solid #6366f1;\n    border-radius: 15px;\n    padding: 20px;\n    margin: 15px 0;\n    box-shadow: 0 0 25px rgba(99, 102, 241, 0.3),\n                inset 0 0 15px rgba(255, 255, 255, 0.1);\n    color: #e2e8f0;\n    font-family: 'Segoe UI', system-ui, sans-serif;\n    position: relative;\n    overflow: hidden;\n\">\n\n<div style=\"\n    position: absolute;\n    bottom: -30px;\n    left: -30px;\n    width: 80px;\n    height: 80px;\n    background: radial-gradient(circle, rgba(168, 85, 247, 0.3) 0%, transparent 70%);\n    border-radius: 50%;\n\"></div>\n\n<h1 style=\"\n    color: #818cf8;\n    margin-top: 0;\n    text-align: center;\n    font-weight: 600;\n    text-shadow: 0 0 10px rgba(129, 140, 248, 0.5);\n    position: relative;\n    z-index: 1;\n\">\n    Model architecture: Image to Signal Regression\n</h1>","metadata":{}},{"cell_type":"code","source":"class ECGNet(nn.Module):\n    \n    def __init__(self):\n        super().__init__()\n        \n        # CNN for image features\n        self.cnn = nn.Sequential(\n            # Block 1\n            nn.Conv2d(3, 64, 3, padding=1),\n            nn.BatchNorm2d(64),\n            nn.ReLU(),\n            nn.Conv2d(64, 64, 3, padding=1),\n            nn.BatchNorm2d(64),\n            nn.ReLU(),\n            nn.MaxPool2d(2),\n            \n            # Block 2  \n            nn.Conv2d(64, 128, 3, padding=1),\n            nn.BatchNorm2d(128),\n            nn.ReLU(),\n            nn.Conv2d(128, 128, 3, padding=1),\n            nn.BatchNorm2d(128),\n            nn.ReLU(),\n            nn.MaxPool2d(2),\n            \n            # Block 3\n            nn.Conv2d(128, 256, 3, padding=1),\n            nn.BatchNorm2d(256),\n            nn.ReLU(),\n            nn.Conv2d(256, 256, 3, padding=1),\n            nn.BatchNorm2d(256),\n            nn.ReLU(),\n            nn.AdaptiveAvgPool2d((4, 4))\n        )\n        \n        # Regression_head\n        self.regressor = nn.Sequential(\n            nn.Linear(256 * 4 * 4, 1024),\n            nn.ReLU(),\n            nn.Dropout(0.5),\n            nn.Linear(1024, 512),\n            nn.ReLU(), \n            nn.Dropout(0.3),\n            nn.Linear(512, config.target_length)\n        )\n        \n    def forward(self, x):\n        # CNN features\n        features = self.cnn(x)\n        features = features.view(features.size(0), -1)\n        \n        # Regression\n        output = self.regressor(features)\n        return output","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-22T14:15:11.434533Z","iopub.execute_input":"2025-12-22T14:15:11.434745Z","iopub.status.idle":"2025-12-22T14:15:11.446152Z","shell.execute_reply.started":"2025-12-22T14:15:11.434721Z","shell.execute_reply":"2025-12-22T14:15:11.445455Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"<div style=\"\n    background: linear-gradient(135deg, #0c0c2e 0%, #1a1a4a 50%, #2d1a4a 100%);\n    border: 2px solid #6366f1;\n    border-radius: 15px;\n    padding: 20px;\n    margin: 15px 0;\n    box-shadow: 0 0 25px rgba(99, 102, 241, 0.3),\n                inset 0 0 15px rgba(255, 255, 255, 0.1);\n    color: #e2e8f0;\n    font-family: 'Segoe UI', system-ui, sans-serif;\n    position: relative;\n    overflow: hidden;\n\">\n\n<div style=\"\n    position: absolute;\n    bottom: -30px;\n    left: -30px;\n    width: 80px;\n    height: 80px;\n    background: radial-gradient(circle, rgba(168, 85, 247, 0.3) 0%, transparent 70%);\n    border-radius: 50%;\n\"></div>\n\n<h1 style=\"\n    color: #818cf8;\n    margin-top: 0;\n    text-align: center;\n    font-weight: 600;\n    text-shadow: 0 0 10px rgba(129, 140, 248, 0.5);\n    position: relative;\n    z-index: 1;\n\">\n    ECG Loss Function\n</h1>","metadata":{}},{"cell_type":"code","source":"class ECGLoss(nn.Module):\n    \n    def __init__(self, eps=1e-8):\n        super().__init__()\n        self.eps = eps\n\n    def forward(self, pred, target):\n        snr = self.snr_loss(pred, target)\n        smooth = self.smoothness_loss(pred)\n        return snr + 0.1 * smooth  # smoothness as a regularizer\n\n    def snr_loss(self, pred, target):\n        noise = target - pred\n        \n        signal_power = torch.sum(target ** 2, dim=1)\n        noise_power = torch.sum(noise ** 2, dim=1)\n        \n        snr = signal_power / (noise_power + self.eps)\n        \n        return -torch.mean(torch.log(snr + self.eps))\n\n    def smoothness_loss(self, signal):\n        if signal.size(1) < 3:\n            return torch.tensor(0.0, device=signal.device)\n        \n        diff1 = signal[:, 1:] - signal[:, :-1]\n        diff2 = diff1[:, 1:] - diff1[:, :-1]\n        \n        return torch.mean(diff2 ** 2)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-22T14:15:11.446928Z","iopub.execute_input":"2025-12-22T14:15:11.447163Z","iopub.status.idle":"2025-12-22T14:15:11.458876Z","shell.execute_reply.started":"2025-12-22T14:15:11.447138Z","shell.execute_reply":"2025-12-22T14:15:11.458159Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"<div style=\"\n    background: linear-gradient(135deg, #0c0c2e 0%, #1a1a4a 50%, #2d1a4a 100%);\n    border: 2px solid #6366f1;\n    border-radius: 15px;\n    padding: 20px;\n    margin: 15px 0;\n    box-shadow: 0 0 25px rgba(99, 102, 241, 0.3),\n                inset 0 0 15px rgba(255, 255, 255, 0.1);\n    color: #e2e8f0;\n    font-family: 'Segoe UI', system-ui, sans-serif;\n    position: relative;\n    overflow: hidden;\n\">\n\n<div style=\"\n    position: absolute;\n    bottom: -30px;\n    left: -30px;\n    width: 80px;\n    height: 80px;\n    background: radial-gradient(circle, rgba(168, 85, 247, 0.3) 0%, transparent 70%);\n    border-radius: 50%;\n\"></div>\n\n<h1 style=\"\n    color: #818cf8;\n    margin-top: 0;\n    text-align: center;\n    font-weight: 600;\n    text-shadow: 0 0 10px rgba(129, 140, 248, 0.5);\n    position: relative;\n    z-index: 1;\n\">\n    Model training\n</h1>","metadata":{}},{"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(500)  # higher -> better results\n    \n# Transforms\ntrain_transform = A.Compose([\n    A.Resize(*config.img_size),\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=config.batch_size, \n    shuffle=True, \n    num_workers=config.num_workers\n)\n    \n# Model\nmodel = ECGNet()\n\n# Two GPU's\ndevice = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\nif torch.cuda.device_count() > 1:\n    print(f\"Using {torch.cuda.device_count()} GPU's\")\n    model = nn.DataParallel(model)\n\nmodel = model.to(device)\n    \n# Loss and optimizer\ncriterion = ECGLoss()\noptimizer = optim.AdamW(model.parameters(), lr=config.lr, weight_decay=1e-4)\nscheduler = optim.lr_scheduler.ReduceLROnPlateau(optimizer, patience=3, factor=0.5)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-22T14:15:11.459578Z","iopub.execute_input":"2025-12-22T14:15:11.459779Z","iopub.status.idle":"2025-12-22T14:17:13.355057Z","shell.execute_reply.started":"2025-12-22T14:15:11.459765Z","shell.execute_reply":"2025-12-22T14:17:13.353983Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"best_loss = float('inf')\nfor epoch in range(config.epochs):\n    model.train()\n    running_loss = 0.0\n        \n    for batch_idx, (images, extracted, targets, base_ids) in enumerate(train_loader):\n        images = images.to(device)\n        targets = targets.to(device)\n            \n        optimizer.zero_grad()\n        outputs = model(images)\n        loss = criterion(outputs, targets)\n        loss.backward()\n            \n        torch.nn.utils.clip_grad_norm_(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}/{config.epochs}, Loss: {epoch_loss:.4f}')\n        \n    if epoch_loss < best_loss:\n        best_loss = epoch_loss\n        torch.save(model.state_dict(), 'best_ecg_model.pth')","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"<div style=\"\n    background: linear-gradient(135deg, #0c0c2e 0%, #1a1a4a 50%, #2d1a4a 100%);\n    border: 2px solid #6366f1;\n    border-radius: 15px;\n    padding: 20px;\n    margin: 15px 0;\n    box-shadow: 0 0 25px rgba(99, 102, 241, 0.3),\n                inset 0 0 15px rgba(255, 255, 255, 0.1);\n    color: #e2e8f0;\n    font-family: 'Segoe UI', system-ui, sans-serif;\n    position: relative;\n    overflow: hidden;\n\">\n\n<div style=\"\n    position: absolute;\n    bottom: -30px;\n    left: -30px;\n    width: 80px;\n    height: 80px;\n    background: radial-gradient(circle, rgba(168, 85, 247, 0.3) 0%, transparent 70%);\n    border-radius: 50%;\n\"></div>\n\n<h1 style=\"\n    color: #818cf8;\n    margin-top: 0;\n    text-align: center;\n    font-weight: 600;\n    text-shadow: 0 0 10px rgba(129, 140, 248, 0.5);\n    position: relative;\n    z-index: 1;\n\">\n    Predict, create submission and check results\n</h1>","metadata":{}},{"cell_type":"code","source":"from scipy.signal import butter, filtfilt\n\ndef smooth_ecg(x, fs=500, lowcut=0.5, highcut=40):\n    nyq = 0.5 * fs\n    b, a = butter(2, [lowcut / nyq, highcut / nyq], btype='band')\n    return filtfilt(b, a, x)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"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    \ntest_loader = DataLoader(test_dataset, batch_size=1, shuffle=False)\n    \nsubmission_data = []\nprocessor = ECGImageProcessor()\n    \nmodel.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 = 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    prediction = smooth_ecg(prediction, fs=test_row['fs']) # New feature\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\nsubmission = pd.DataFrame(submission_data)\nsubmission.to_csv('submission1.csv', index=False)\nsubmission.head(30)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-22T14:17:13.356901Z","iopub.execute_input":"2025-12-22T14:17:13.357579Z","iopub.status.idle":"2025-12-22T14:17:15.466561Z","shell.execute_reply.started":"2025-12-22T14:17:13.357552Z","shell.execute_reply":"2025-12-22T14:17:15.465759Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## second model","metadata":{}},{"cell_type":"code","source":"!pip uninstall -y tensorflow\n!uv pip install --no-deps --system --no-index --find-links='/kaggle/input/hengck23-submit-physionet/hengck23-submit-physionet/setup' 'connected-components-3d'","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-22T14:17:15.467476Z","iopub.execute_input":"2025-12-22T14:17:15.467842Z","iopub.status.idle":"2025-12-22T14:17:37.527079Z","shell.execute_reply.started":"2025-12-22T14:17:15.467817Z","shell.execute_reply":"2025-12-22T14:17:37.526414Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import kagglehub\ndeterministic = kagglehub.package_import('wasupandceacar/deterministic').deterministic\ndeterministic.init_all(35, disable_list=['cuda_block'])\n\nimport sys\nsys.path.append('/kaggle/input/hengck23-submit-physionet/hengck23-submit-physionet')\n\nimport os\nimport gc\nimport cv2\nimport torch\nimport traceback\nimport numpy as np\nimport pandas as pd\n\nfrom tqdm import tqdm\nfrom pathlib import Path\nfrom tqdm.auto import tqdm\nfrom shutil import copyfile\nfrom scipy.signal import resample\n\nfrom stage0_model import Net as Stage0Net\nfrom stage0_common import *\n\nif_submit = os.getenv('KAGGLE_IS_COMPETITION_RERUN')\n\nif if_submit:\n    test_meta = Path(\"/kaggle/input/physionet-ecg-image-digitization/test.csv\")\n    test_dir = Path(\"/kaggle/input/physionet-ecg-image-digitization/test\")\nelse:\n    test_meta = Path(\"/kaggle/input/physio-test-fake-dataset/test_fake/test.csv\")\n    test_dir = Path(\"/kaggle/input/physio-test-fake-dataset/test_fake\")\n\nvalid_df = pd.read_csv(test_meta)\nvalid_df['id'] = valid_df['id'].astype(str) \nvalid_id = valid_df['id'].unique().tolist()\n\n\nglobal_dict = {\n    \"stage0_dir\": \"/kaggle/working/stage0\",\n    \"stage1_dir\": \"/kaggle/working/stage1\",\n    \"stage2_dir\": \"/kaggle/working/stage2\",\n}\n\ndef change_color(image_rgb):\n    gray = cv2.cvtColor(image_rgb, cv2.COLOR_RGB2GRAY)\n    denoised = cv2.fastNlMeansDenoising(gray, h=10)\n    clahe = cv2.createCLAHE(clipLimit=4.0, tileGridSize=(8,8))\n    contrast_enhanced = clahe.apply(denoised)\n    return cv2.cvtColor(contrast_enhanced, cv2.COLOR_GRAY2RGB)\n\nstage0_dir = Path(global_dict[\"stage0_dir\"])\nstage0_dir.mkdir(exist_ok=True)\n\nstage0_net = Stage0Net(pretrained=False)\nstage0_net = load_net(stage0_net, '/kaggle/input/hengck23-submit-physionet/hengck23-submit-physionet/weight/stage0-last.checkpoint.pth')\nstage0_net.to(\"cuda:0\")\nstage0_net.eval()\n\nfor n, sample_id in enumerate(tqdm(valid_id)):\n    path = test_dir / f'{sample_id}.png'\n    output_path = stage0_dir / f'{sample_id}.png'\n    \n    image_original = cv2.imread(str(path), cv2.IMREAD_COLOR)\n    image_original = cv2.cvtColor(image_original, cv2.COLOR_BGR2RGB)\n    image_for_model = change_color(image_original)\n    \n    batch = image_to_batch(image_for_model)\n\n    try:\n        with torch.no_grad(), torch.amp.autocast('cuda', dtype=torch.float32):\n            output = stage0_net(batch)\n        \n        rotated, keypoint = output_to_predict(image_original, batch, output)\n        normalised, _, _ = normalise_by_homography(rotated, keypoint)\n        \n        cv2.imwrite(str(output_path), cv2.cvtColor(normalised, cv2.COLOR_RGB2BGR))\n    except Exception as e:\n        traceback.print_exc()\n        copyfile(path, output_path)\n\n\nfrom stage1_model import Net as Stage1Net\nfrom stage1_common import *\n\nstage0_dir = Path(global_dict[\"stage0_dir\"])\nstage1_dir = Path(global_dict[\"stage1_dir\"])\nstage1_dir.mkdir(exist_ok=True)\n\nstage1_net = Stage1Net(pretrained=False)\nstage1_net = load_net(stage1_net, '/kaggle/input/hengck23-submit-physionet/hengck23-submit-physionet/weight/stage1-last.checkpoint.pth')\nstage1_net.to(\"cuda:0\")\n\nfor n, sample_id in enumerate(tqdm(valid_id)):\n    path = stage0_dir / f'{sample_id}.png'\n    output_path = stage1_dir / f'{sample_id}.png'\n    image = cv2.imread(path, cv2.IMREAD_COLOR_RGB)\n    batch = {'image': torch.from_numpy(np.ascontiguousarray(image.transpose(2, 0, 1))).unsqueeze(0)}\n\n    try:\n        with torch.no_grad(), torch.amp.autocast('cuda', dtype=torch.float32):\n            output = stage1_net(batch)\n        gridpoint_xy, _ = output_to_predict(image, batch, output)\n        rectified = rectify_image(image, gridpoint_xy)\n        cv2.imwrite(output_path, cv2.cvtColor(rectified, cv2.COLOR_RGB2BGR))\n    except:\n        traceback.print_exc()\n        copyfile(path, output_path)\n\nimport torchvision.transforms as T\nfrom stage2_model import *\nfrom stage2_common import *\nfrom scipy.signal import savgol_filter, medfilt\n\n\nclass Net3(nn.Module):\n    \n    def __init__(self, pretrained=True):\n        super(Net3, self).__init__()\n        encoder_dim = [64, 128, 256, 512]\n        decoder_dim = [128, 64, 32, 16]\n\n        self.encoder = timm.create_model(\n            model_name='resnet34.a3_in1k', pretrained=pretrained, in_chans=3, num_classes=0, global_pool=''\n        )\n\n        self.decoder = MyCoordUnetDecoder(\n            in_channel=encoder_dim[-1],\n            skip_channel=encoder_dim[:-1][::-1] + [0],\n            out_channel=decoder_dim,\n            scale=[2, 2, 2, 2]\n        )\n        self.pixel = nn.Conv2d(decoder_dim[-1], 4, 1)\n\n    def forward(self, image):\n        encode = encode_with_resnet(self.encoder, image)\n        last, _ = self.decoder(feature=encode[-1], skip=encode[:-1][::-1] + [None])\n        pixel = self.pixel(last)\n        return pixel\n\nstage1_dir = Path(global_dict[\"stage1_dir\"])\nstage2_dir = Path(global_dict[\"stage2_dir\"])\nstage2_dir.mkdir(exist_ok=True)\n\nstage2_net = Net3(pretrained=False).to(\"cuda:0\")\nmodel_path = \"/kaggle/input/physio-seg-public/pytorch/net3_009_4200/1/iter_0004200.pt\"\nstage2_net.load_state_dict(torch.load(model_path))\nstage2_net.eval()\n\nx0, x1 = 0, 2176\ny0, y1 = 0, 1696\nzero_mv = [703.5, 987.5, 1271.5, 1531.5]\nmv_to_pixel = 78.5\nt0, t1 = 235, 4161\n\nresize = T.Resize((1696, 4352), interpolation=T.InterpolationMode.BILINEAR)\n\nfor n, sample_id in enumerate(tqdm(valid_id)):\n    path = stage1_dir / f'{sample_id}.png'\n    output_path = stage2_dir / f'{sample_id}.npy'\n    image = cv2.imread(path, cv2.IMREAD_COLOR_RGB)\n    \n    length = valid_df[(valid_df['id']==sample_id) & (valid_df['lead']=='II')].iloc[0].number_of_rows\n    \n    image = image[y0:y1, x0:x1] / 255\n    batch = resize(torch.from_numpy(np.ascontiguousarray(image.transpose(2, 0, 1))).unsqueeze(0)).float().to(\"cuda:0\")\n    \n    try:\n        with torch.no_grad(), torch.amp.autocast('cuda', dtype=torch.float32):\n            output = stage2_net(batch)\n        \n        pixel = torch.sigmoid(output).float().data.cpu().numpy()[0]\n        series_in_pixel = pixel_to_series(pixel[..., t0:t1], zero_mv, length)\n        series = (np.array(zero_mv).reshape(4, 1) - series_in_pixel) / mv_to_pixel\n        \n       \n        for i in range(series.shape[0]):\n            series[i] = savgol_filter(series[i], window_length=7, polyorder=2)\n\n        np.save(output_path, series)\n    except:\n        traceback.print_exc()\n        series = np.zeros((4, length)) \n        np.save(output_path, series)\n\ndef series_dict(series):\n    d = {}\n    for l in range(3):\n        lead_names = [\n            ['I',   'aVR', 'V1', 'V4'],\n            ['II',  'aVL', 'V2', 'V5'],\n            ['III', 'aVF', 'V3', 'V6'],\n        ][l]\n        split = np.array_split(series[l], 4)\n        for (k, s) in zip(lead_names, split):\n            d[k] = s\n    \n    d['II'] = series[3]\n    \n    return d\n\n\nstage2_dir = Path(global_dict[\"stage2_dir\"])\n\nres = []\ngb = valid_df.groupby('id')\n\nfor i, (sample_id, df) in enumerate(tqdm(gb)):\n    series = np.load(stage2_dir / f'{sample_id}.npy')\n    d_series = series_dict(series)\n\n    for _, d in df.iterrows():\n        s = d_series.get(d.lead, np.zeros(d.number_of_rows))\n        \n        if len(s) != d.number_of_rows:\n            x_old = np.linspace(0, 1, len(s))\n            x_new = np.linspace(0, 1, d.number_of_rows)\n            s = np.interp(x_new, x_old, s)\n        \n        row_id = [f'{sample_id}_{x}_{d.lead}' for x in range(d.number_of_rows)]\n        res.append(pd.DataFrame({'id': row_id, 'value': s}))\n\n    if i % 100 == 0:\n        gc.collect()\n\nsubmission = pd.concat(res, axis=0, ignore_index=True)\nsubmission.to_csv('submission2.csv', index=False)\nsubmission.head(30)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-22T14:17:37.529401Z","iopub.execute_input":"2025-12-22T14:17:37.529624Z","iopub.status.idle":"2025-12-22T14:18:42.935850Z","shell.execute_reply.started":"2025-12-22T14:17:37.529606Z","shell.execute_reply":"2025-12-22T14:18:42.935042Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"sub1 = pd.read_csv('submission1.csv')\nsub2 = pd.read_csv('submission2.csv')\n\neps = 1e-69\n\nweight_sub1 = eps\nweight_sub2 = 1 - eps\n\nsub2['value'] = weight_sub1 * sub1['value'] + weight_sub2 * sub2['value']\nsub2.to_csv('submission.csv', index=False)\nsub2.head(30)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-22T14:19:10.115543Z","iopub.execute_input":"2025-12-22T14:19:10.115825Z","iopub.status.idle":"2025-12-22T14:19:10.588009Z","shell.execute_reply.started":"2025-12-22T14:19:10.115807Z","shell.execute_reply":"2025-12-22T14:19:10.587211Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}