{"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","name":"python3"},"language_info":{"name":"python","version":"3.11.0"},"kaggle":{"accelerator":"none","dataSources":[{"sourceId":97984,"databundleVersionId":14096757,"isSourceIdPinned":false,"sourceType":"competition"}],"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# ECG Image Digitization - Inference Only\n\nThis notebook loads a pre-trained model and generates predictions for the PhysioNet Challenge.\n\n**Competition**: [PhysioNet ECG Image Digitization](https://www.kaggle.com/competitions/physionet-ecg-image-digitization)\n\n## Pipeline Overview\n1. ✅ Environment Setup\n2. ✅ Load Pre-trained Model\n3. ✅ Run Inference\n4. ✅ Generate Submission","metadata":{}},{"cell_type":"markdown","source":"## 1. Environment Setup","metadata":{}},{"cell_type":"code","source":"import os\nimport sys\nfrom pathlib import Path\n\n# Detect Kaggle environment\nIS_KAGGLE = 'KAGGLE_KERNEL_RUN_TYPE' in os.environ\nprint(f\"Running on Kaggle: {IS_KAGGLE}\")\n\n# Install dependencies if needed\nif IS_KAGGLE:\n    !pip install -q \"numpy<2\" \"pandas>=2.2.2\" segmentation-models-pytorch wfdb\n    \n    # Paths for Kaggle\n    DATA_DIR = Path('/kaggle/input/physionet-ecg-image-digitization')\n    OUTPUT_DIR = Path('/kaggle/working')\n    # Model is in the git repo attached as dataset\n    MODEL_DIR = Path('/kaggle/input/physionet-ecg-models')  # Your repo attached as dataset\nelse:\n    # Local paths\n    DATA_DIR = Path('../data')\n    OUTPUT_DIR = Path('../outputs')\n    MODEL_DIR = Path('../models/exports')\n\nprint(f\"Data directory: {DATA_DIR}\")\nprint(f\"Model directory: {MODEL_DIR}\")\nprint(f\"Output directory: {OUTPUT_DIR}\")","metadata":{},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch\nimport numpy as np\nimport pandas as pd\nfrom PIL import Image\nfrom tqdm import tqdm\nimport json\n\n# Set device\ndevice = 'cuda' if torch.cuda.is_available() else 'cpu'\nprint(f\"\\n🖥️ Device: {device}\")\n\nif torch.cuda.is_available():\n    print(f\"  GPU: {torch.cuda.get_device_name(0)}\")\n    print(f\"  Memory: {torch.cuda.get_device_properties(0).total_memory / 1e9:.1f} GB\")","metadata":{},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 2. Load Pre-trained Model","metadata":{}},{"cell_type":"code","source":"# Add src to path\nif not IS_KAGGLE:\n    src_path = Path('../src')\n    if src_path.exists() and str(src_path) not in sys.path:\n        sys.path.insert(0, str(src_path.resolve()))\n\n# Import model components\nfrom ecg_digitization.models import ECGDigitizer\nfrom ecg_digitization.data import get_val_transforms\n\n# Model configuration (should match training)\nMODEL_CONFIG = {\n    'encoder_name': 'resnet50',\n    'encoder_weights': None,  # Will load from checkpoint\n    'num_leads': 12,\n    'signal_length': 5000,\n}\n\n# Image configuration\nIMAGE_SIZE = (512, 640)\n\nprint(\"📋 Model Configuration:\")\nfor k, v in MODEL_CONFIG.items():\n    print(f\"  {k}: {v}\")\nprint(f\"  image_size: {IMAGE_SIZE}\")","metadata":{},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Load model metadata if available\nmetadata_path = MODEL_DIR / 'model_metadata.json'\nif metadata_path.exists():\n    with open(metadata_path) as f:\n        model_metadata = json.load(f)\n    print(\"📊 Model Metadata:\")\n    for k, v in model_metadata.items():\n        print(f\"  {k}: {v}\")\nelse:\n    print(\"⚠️ No model metadata found\")\n    model_metadata = {}","metadata":{},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Create model\nprint(\"\\n🏗️ Creating model...\")\nmodel = ECGDigitizer(\n    encoder_name=MODEL_CONFIG['encoder_name'],\n    encoder_weights=MODEL_CONFIG['encoder_weights'],\n    num_leads=MODEL_CONFIG['num_leads'],\n    signal_length=MODEL_CONFIG['signal_length'],\n)\n\n# Load checkpoint\ncheckpoint_path = MODEL_DIR / 'ecg_digitizer.pt'\nif not checkpoint_path.exists():\n    # Try alternative paths\n    alt_paths = [\n        MODEL_DIR / 'best_model.pt',\n        MODEL_DIR / 'checkpoints' / 'best_model.pt',\n    ]\n    for alt in alt_paths:\n        if alt.exists():\n            checkpoint_path = alt\n            break\n\nprint(f\"📁 Loading checkpoint: {checkpoint_path}\")\n\nif checkpoint_path.exists():\n    checkpoint = torch.load(checkpoint_path, map_location=device)\n    \n    # Handle different checkpoint formats\n    if isinstance(checkpoint, dict):\n        if 'model_state_dict' in checkpoint:\n            model.load_state_dict(checkpoint['model_state_dict'])\n        elif 'state_dict' in checkpoint:\n            model.load_state_dict(checkpoint['state_dict'])\n        else:\n            model.load_state_dict(checkpoint)\n    else:\n        model.load_state_dict(checkpoint)\n    \n    print(\"✅ Model loaded successfully\")\nelse:\n    raise FileNotFoundError(f\"Checkpoint not found: {checkpoint_path}\")\n\n# Move to device and set eval mode\nmodel = model.to(device)\nmodel.eval()\n\n# Count parameters\nnum_params = sum(p.numel() for p in model.parameters())\nprint(f\"  Total parameters: {num_params:,}\")","metadata":{},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 3. Prepare Test Data","metadata":{}},{"cell_type":"code","source":"# Load test metadata\ntest_csv = DATA_DIR / 'test.csv'\ntest_df = pd.read_csv(test_csv)\nprint(f\"📊 Test samples: {len(test_df)}\")\nprint(f\"Columns: {list(test_df.columns)}\")\ntest_df.head()","metadata":{},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Create transform\ntransform = get_val_transforms(IMAGE_SIZE)\n\n# Find test images\ntest_dir = DATA_DIR / 'test'\ntest_images = list(test_dir.glob('*.png'))\nprint(f\"\\n📁 Found {len(test_images)} test images\")\n\nif len(test_images) > 0:\n    print(f\"  First image: {test_images[0].name}\")","metadata":{},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 4. Run Inference","metadata":{}},{"cell_type":"code","source":"def preprocess_image(image_path, transform):\n    \"\"\"Load and preprocess an image.\"\"\"\n    image = Image.open(image_path).convert('RGB')\n    image_tensor = transform(image=np.array(image))['image']\n    return image_tensor\n\ndef run_inference(model, image_paths, transform, device, batch_size=8):\n    \"\"\"Run inference on a list of images.\"\"\"\n    model.eval()\n    predictions = {}\n    \n    with torch.no_grad():\n        for i in tqdm(range(0, len(image_paths), batch_size), desc=\"Inference\"):\n            batch_paths = image_paths[i:i + batch_size]\n            batch_tensors = []\n            \n            for path in batch_paths:\n                try:\n                    tensor = preprocess_image(path, transform)\n                    batch_tensors.append(tensor)\n                except Exception as e:\n                    print(f\"Warning: Failed to process {path}: {e}\")\n                    continue\n            \n            if len(batch_tensors) == 0:\n                continue\n            \n            # Stack batch\n            batch = torch.stack(batch_tensors).to(device)\n            \n            # Forward pass\n            outputs = model(batch, target_length=5000)\n            signals = outputs['signals'].cpu().numpy()\n            \n            # Store predictions\n            for j, path in enumerate(batch_paths[:len(signals)]):\n                sample_id = path.stem  # Get filename without extension\n                predictions[sample_id] = signals[j]\n    \n    return predictions\n\nprint(\"\\n🔮 Running inference...\")","metadata":{},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Run inference\npredictions = run_inference(\n    model=model,\n    image_paths=test_images,\n    transform=transform,\n    device=device,\n    batch_size=8,\n)\n\nprint(f\"\\n✅ Generated predictions for {len(predictions)} samples\")","metadata":{},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 5. Generate Submission","metadata":{}},{"cell_type":"code","source":"# Lead names for ECG\nLEAD_NAMES = ['I', 'II', 'III', 'aVR', 'aVL', 'aVF', 'V1', 'V2', 'V3', 'V4', 'V5', 'V6']\n\ndef create_submission(predictions, lead_names=LEAD_NAMES):\n    \"\"\"Create submission DataFrame in required format.\"\"\"\n    rows = []\n    \n    for sample_id, signal in predictions.items():\n        # Signal shape: [num_leads, signal_length]\n        for lead_idx, lead_name in enumerate(lead_names):\n            if lead_idx < signal.shape[0]:\n                # Convert signal to list of values\n                signal_values = signal[lead_idx].tolist()\n                \n                rows.append({\n                    'id': f\"{sample_id}_{lead_name}\",\n                    'signal': signal_values,\n                })\n    \n    return pd.DataFrame(rows)\n\nprint(\"\\n📝 Creating submission file...\")","metadata":{},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Create submission DataFrame\nsubmission_df = create_submission(predictions)\n\nprint(f\"Submission shape: {submission_df.shape}\")\nprint(f\"Columns: {list(submission_df.columns)}\")\nsubmission_df.head()","metadata":{},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Save submission\nsubmission_path = OUTPUT_DIR / 'submission.parquet'\nsubmission_df.to_parquet(submission_path, index=False)\n\nprint(f\"\\n✅ Submission saved: {submission_path}\")\nprint(f\"  File size: {submission_path.stat().st_size / 1024 / 1024:.2f} MB\")","metadata":{},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 6. Visualize Sample Predictions","metadata":{}},{"cell_type":"code","source":"import matplotlib.pyplot as plt\n\n# Visualize a few predictions\nsample_ids = list(predictions.keys())[:3]\n\nfig, axes = plt.subplots(len(sample_ids), 1, figsize=(14, 4 * len(sample_ids)))\nif len(sample_ids) == 1:\n    axes = [axes]\n\nfor idx, sample_id in enumerate(sample_ids):\n    signal = predictions[sample_id]\n    time = np.arange(signal.shape[1]) / 500  # 500 Hz sampling rate\n    \n    # Plot all 12 leads with offset\n    for lead_idx in range(min(12, signal.shape[0])):\n        axes[idx].plot(\n            time, \n            signal[lead_idx, :] + lead_idx * 2, \n            linewidth=0.8, \n            alpha=0.8,\n            label=LEAD_NAMES[lead_idx] if lead_idx < len(LEAD_NAMES) else f'Lead {lead_idx}'\n        )\n    \n    axes[idx].set_xlabel('Time (s)', fontsize=11)\n    axes[idx].set_ylabel('Lead (offset)', fontsize=11)\n    axes[idx].set_title(f'Sample: {sample_id}', fontsize=12, fontweight='bold')\n    axes[idx].grid(True, alpha=0.3)\n\nplt.tight_layout()\nplt.show()\n\nprint(f\"\\n✅ Displayed {len(sample_ids)} sample predictions\")","metadata":{},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 🎉 Submission Complete!\n\nYour submission file has been generated:\n- **File**: `submission.parquet`\n- **Format**: Parquet with columns `id` and `signal`\n\n### Next Steps\n1. Submit to the [PhysioNet ECG Image Digitization competition](https://www.kaggle.com/competitions/physionet-ecg-image-digitization)\n2. Check your leaderboard score\n\n### Model Info\n- **Architecture**: ECGDigitizer (ResNet50 encoder + Signal Regression Head)\n- **Input**: ECG paper images (RGB)\n- **Output**: 12-lead ECG signals (5000 samples each)","metadata":{}}]}