{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.12.12","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"gpu","dataSources":[{"sourceId":97984,"databundleVersionId":14096757,"sourceType":"competition"},{"sourceId":717360,"sourceType":"modelInstanceVersion","isSourceIdPinned":true,"modelInstanceId":545394,"modelId":558408}],"dockerImageVersionId":31236,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# Clone the ECG-Digitiser repository\n!git clone https://github.com/felixkrones/ECG-Digitiser.git\n%cd ECG-Digitiser\n\n# Install requirements\n!pip install -q -r requirements.txt\n\n# Install additional required packages\n!pip install -q wfdb scipy opencv-python-headless scikit-image\n\n# Install custom nnUNet\n%cd nnUNet\n!pip install -q -e .\n%cd ..\n\nprint(\"✓ Installation complete!\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-12T12:04:16.606548Z","iopub.execute_input":"2026-01-12T12:04:16.606762Z","iopub.status.idle":"2026-01-12T12:05:06.278505Z","shell.execute_reply.started":"2026-01-12T12:04:16.606741Z","shell.execute_reply":"2026-01-12T12:05:06.277733Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\n\n# Set nnUNet environment variables\nos.environ['nnUNet_raw'] = '/kaggle/working/nnUNet_raw'\nos.environ['nnUNet_preprocessed'] = '/kaggle/working/nnUNet_preprocessed'\nos.environ['nnUNet_results'] = '/kaggle/working/nnUNet_results'\n\n# Create required directories\n!mkdir -p /kaggle/working/nnUNet_raw\n!mkdir -p /kaggle/working/nnUNet_preprocessed\n!mkdir -p /kaggle/working/nnUNet_results\n\n# Create temporary directories for the digitizer\n!mkdir -p /kaggle/working/ECG-Digitiser/data/temp_nnUNet_output\n!mkdir -p /kaggle/working/ECG-Digitiser/data/temp_nnUNet_input\n\nprint(\"✓ Environment variables and directories set!\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-12T12:05:06.280496Z","iopub.execute_input":"2026-01-12T12:05:06.280768Z","iopub.status.idle":"2026-01-12T12:05:06.838603Z","shell.execute_reply.started":"2026-01-12T12:05:06.280740Z","shell.execute_reply":"2026-01-12T12:05:06.837912Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport shutil\n\n# Your model file from Kaggle input\nmodel_input = '/kaggle/input/ecg-digitizer-pretrained/pytorch/default/1/checkpoint_best.pth'\n\n# The code expects: models/M3/nnUNet_results/Dataset500_Signals/nnUNetTrainer__nnUNetPlans__2d/fold_all/\n# NOT the standard nnUNet structure!\nmodel_base = '/kaggle/working/ECG-Digitiser/models/M3/nnUNet_results/Dataset500_Signals/nnUNetTrainer__nnUNetPlans__2d'\n\n# Create fold_all directory (the code looks for fold_all, not individual folds)\nfold_path = f'{model_base}/fold_all'\nos.makedirs(fold_path, exist_ok=True)\n\n# Copy the checkpoint\nshutil.copy(model_input, f'{fold_path}/checkpoint_final.pth')\n\nprint(\"✓ Model organized in expected structure!\")\nprint(f\"✓ Model location: {fold_path}/checkpoint_final.pth\")\nprint(f\"✓ Model size: {os.path.getsize(f'{fold_path}/checkpoint_final.pth') / (1024**3):.2f} GB\")\n\n# Verify structure\nprint(\"\\nModel directory structure:\")\n!ls -lR /kaggle/working/ECG-Digitiser/models/\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-12T12:05:06.839718Z","iopub.execute_input":"2026-01-12T12:05:06.839939Z","iopub.status.idle":"2026-01-12T12:05:13.284310Z","shell.execute_reply.started":"2026-01-12T12:05:06.839915Z","shell.execute_reply":"2026-01-12T12:05:13.283668Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Fix the NaN rotation angle bug\ndigitize_path = '/kaggle/working/ECG-Digitiser/src/run/digitize.py'\n\nwith open(digitize_path, 'r') as f:\n    lines = f.readlines()\n\n# Fix line 352 (0-indexed as 351)\nfor i, line in enumerate(lines):\n    if i == 351:\n        original = line.strip()\n        lines[i] = '        image_rotated = rotate(image, float(rot_angle) if not np.isnan(rot_angle) else 0.0)\\n'\n        print(f\"Original line 352: {original}\")\n        print(f\"Fixed line 352: {lines[i].strip()}\")\n        break\n\n# Write back\nwith open(digitize_path, 'w') as f:\n    f.writelines(lines)\n\nprint(\"\\n✓ Fixed rotation angle NaN handling!\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-12T12:05:13.285414Z","iopub.execute_input":"2026-01-12T12:05:13.285718Z","iopub.status.idle":"2026-01-12T12:05:13.292569Z","shell.execute_reply.started":"2026-01-12T12:05:13.285690Z","shell.execute_reply":"2026-01-12T12:05:13.291927Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"%cd /kaggle/working/ECG-Digitiser\n\n# Test on one training sample\ntrain_sample = '/kaggle/input/physionet-ecg-image-digitization/train/1006427285'\noutput_dir = '/kaggle/working/validation'\n\n!mkdir -p {output_dir}\n\n# Run digitization with verbose output\nprint(\"Running digitization on training sample\")\n!python -m src.run.digitize -d {train_sample} -o {output_dir} --verbose\n\nprint(\"\\n✓ Digitization complete!\")\nprint(\"\\nOutput files:\")\n!ls -lh {output_dir}/\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-12T12:05:13.293433Z","iopub.execute_input":"2026-01-12T12:05:13.293667Z","iopub.status.idle":"2026-01-12T12:13:34.255699Z","shell.execute_reply.started":"2026-01-12T12:05:13.293637Z","shell.execute_reply":"2026-01-12T12:13:34.254882Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"%cd /kaggle/working/ECG-Digitiser\n\n# Test on just ONE ECG image instead of the whole folder\nsingle_image = '/kaggle/input/physionet-ecg-image-digitization/train/1006427285/1006427285-0001.png'\noutput_dir = '/kaggle/working/validation_single'\n\n!mkdir -p {output_dir}\n\n# Copy just this one image to a temp folder for processing\ntemp_input = '/kaggle/working/temp_single_image'\n!mkdir -p {temp_input}\n!cp {single_image} {temp_input}/\n\nprint(f\"Testing on single image: {single_image}\")\nprint(\"Running digitization\")\n!python -m src.run.digitize -d {temp_input} -o {output_dir} --verbose\n\nprint(\"\\n✓ Digitization complete!\")\nprint(\"\\nOutput files:\")\n!ls -lh {output_dir}/\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-12T12:18:36.797088Z","iopub.execute_input":"2026-01-12T12:18:36.797417Z","iopub.status.idle":"2026-01-12T12:19:19.143024Z","shell.execute_reply.started":"2026-01-12T12:18:36.797395Z","shell.execute_reply":"2026-01-12T12:19:19.142091Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import pandas as pd\nimport numpy as np\nimport wfdb\n\n# List output files\noutput_files = !ls {output_dir}/*.hea\nprint(f\"Found {len(output_files)} output record(s)\")\n\nif len(output_files) > 0:\n    # Get the record name (without extension)\n    record_file = output_files[0].replace('.hea', '')\n    record_name = os.path.basename(record_file)\n    \n    print(f\"\\nLoading record: {record_name}\")\n    \n    # Read WFDB record\n    record = wfdb.rdrecord(f'{output_dir}/{record_name}')\n    \n    print(f\"✓ Successfully loaded!\")\n    print(f\"  Signal shape: {record.p_signal.shape}\")\n    print(f\"  Lead names: {record.sig_name}\")\n    print(f\"  Sampling frequency: {record.fs} Hz\")\n    print(f\"  Duration: {len(record.p_signal) / record.fs:.2f} seconds\")\n    \n    # Convert to DataFrame\n    pred = pd.DataFrame(record.p_signal, columns=record.sig_name)\n    \n    print(f\"\\nPrediction DataFrame:\")\n    print(f\"  Shape: {pred.shape}\")\n    print(f\"  Columns: {pred.columns.tolist()}\")\n    print(f\"\\nFirst few rows:\\n{pred.head()}\")\n    print(f\"\\nBasic statistics:\\n{pred.describe()}\")\nelse:\n    print(\"✗ No output files found!\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-12T12:19:36.565328Z","iopub.execute_input":"2026-01-12T12:19:36.566162Z","iopub.status.idle":"2026-01-12T12:19:36.607647Z","shell.execute_reply.started":"2026-01-12T12:19:36.566128Z","shell.execute_reply":"2026-01-12T12:19:36.606929Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import matplotlib.pyplot as plt\nfrom IPython.display import Image, display\n\n# Display the input ECG image\nprint(\"=\"*80)\nprint(\"INPUT ECG IMAGE\")\nprint(\"=\"*80)\ndisplay(Image(filename=single_image, width=900))\n\n# Plot all 12 leads from the prediction\nprint(\"\\n\" + \"=\"*80)\nprint(\"DIGITIZED SIGNALS (All 12 Leads)\")\nprint(\"=\"*80)\n\nfig, axes = plt.subplots(6, 2, figsize=(18, 20))\naxes = axes.flatten()\n\ntime = np.arange(len(pred)) / record.fs\n\nfor idx, lead in enumerate(pred.columns):\n    ax = axes[idx]\n    \n    ax.plot(time, pred[lead].values, linewidth=1.5, color='blue')\n    ax.set_title(f'Lead {lead}', fontsize=13, fontweight='bold')\n    ax.set_xlabel('Time (s)', fontsize=11)\n    ax.set_ylabel('Amplitude (mV)', fontsize=11)\n    ax.grid(True, alpha=0.3, linestyle='--')\n    \n    # Add some statistics\n    mean_val = pred[lead].mean()\n    std_val = pred[lead].std()\n    ax.text(0.02, 0.98, f'Mean: {mean_val:.3f}\\nStd: {std_val:.3f}', \n            transform=ax.transAxes, verticalalignment='top',\n            bbox=dict(boxstyle='round', facecolor='wheat', alpha=0.5),\n            fontsize=9)\n\nplt.tight_layout()\nplt.savefig('/kaggle/working/single_image_digitization.png', dpi=150, bbox_inches='tight')\nplt.show()\n\nprint(\"\\n✓ Visualization saved!\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-12T12:19:49.362565Z","iopub.execute_input":"2026-01-12T12:19:49.363143Z","iopub.status.idle":"2026-01-12T12:19:52.611776Z","shell.execute_reply.started":"2026-01-12T12:19:49.363113Z","shell.execute_reply":"2026-01-12T12:19:52.611146Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-12T12:21:13.497022Z","iopub.execute_input":"2026-01-12T12:21:13.497899Z","iopub.status.idle":"2026-01-12T12:21:13.507529Z","shell.execute_reply.started":"2026-01-12T12:21:13.497857Z","shell.execute_reply":"2026-01-12T12:21:13.506633Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def calculate_snr(original, reconstructed):\n    \"\"\"Calculate Signal-to-Noise Ratio in dB\"\"\"\n    min_len = min(len(original), len(reconstructed))\n    original = original[:min_len]\n    reconstructed = reconstructed[:min_len]\n    \n    signal_power = np.mean(original ** 2)\n    noise = original - reconstructed\n    noise_power = np.mean(noise ** 2)\n    \n    if noise_power == 0 or noise_power < 1e-10:\n        return 100.0  # Very high SNR\n    \n    snr = 10 * np.log10(signal_power / noise_power)\n    return snr\n\ndef calculate_mse(original, reconstructed):\n    \"\"\"Calculate Mean Squared Error\"\"\"\n    min_len = min(len(original), len(reconstructed))\n    return np.mean((original[:min_len] - reconstructed[:min_len]) ** 2)\n\ndef calculate_rmse(original, reconstructed):\n    \"\"\"Calculate Root Mean Squared Error\"\"\"\n    return np.sqrt(calculate_mse(original, reconstructed))\n\ndef calculate_prd(original, reconstructed):\n    \"\"\"Calculate Percentage Root-mean-square Difference\"\"\"\n    min_len = min(len(original), len(reconstructed))\n    original = original[:min_len]\n    reconstructed = reconstructed[:min_len]\n    \n    numerator = np.sum((original - reconstructed) ** 2)\n    denominator = np.sum(original ** 2)\n    \n    if denominator == 0:\n        return 100.0\n    \n    prd = np.sqrt(numerator / denominator) * 100\n    return prd\n\ndef calculate_correlation(original, reconstructed):\n    \"\"\"Calculate Pearson correlation coefficient\"\"\"\n    min_len = min(len(original), len(reconstructed))\n    return np.corrcoef(original[:min_len], reconstructed[:min_len])[0, 1]\n\ndef calculate_all_metrics(gt_signal, pred_signal, lead_name):\n    \"\"\"Calculate all metrics for a single lead\"\"\"\n    metrics = {\n        'Lead': lead_name,\n        'SNR (dB)': calculate_snr(gt_signal, pred_signal),\n        'MSE': calculate_mse(gt_signal, pred_signal),\n        'RMSE': calculate_rmse(gt_signal, pred_signal),\n        'PRD (%)': calculate_prd(gt_signal, pred_signal),\n        'Correlation': calculate_correlation(gt_signal, pred_signal)\n    }\n    return metrics\n\nprint(\"✓ Metrics functions defined!\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-12T12:20:50.289003Z","iopub.execute_input":"2026-01-12T12:20:50.289342Z","iopub.status.idle":"2026-01-12T12:20:50.299254Z","shell.execute_reply.started":"2026-01-12T12:20:50.289317Z","shell.execute_reply":"2026-01-12T12:20:50.298552Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Calculate metrics for all 12 leads\nleads = ['I', 'II', 'III', 'aVR', 'aVL', 'aVF', 'V1', 'V2', 'V3', 'V4', 'V5', 'V6']\nall_metrics = []\n\nfor lead in leads:\n    if lead in gt.columns and lead in pred.columns:\n        print(f\"Calculating metrics for lead {lead}...\")\n        metrics = calculate_all_metrics(\n            gt[lead].values, \n            pred[lead].values, \n            lead\n        )\n        all_metrics.append(metrics)\n    else:\n        print(f\"⚠ Lead {lead} not found in both GT and prediction\")\n        \n# Create DataFrame with results\nresults_df = pd.DataFrame(all_metrics)\n\nprint(\"\\n\" + \"=\"*80)\nprint(\"EVALUATION METRICS FOR ALL LEADS\")\nprint(\"=\"*80)\nprint(results_df.to_string(index=False))\nprint(\"\\n\" + \"=\"*80)\nprint(\"AVERAGE METRICS\")\nprint(\"=\"*80)\nprint(f\"Average SNR: {results_df['SNR (dB)'].mean():.2f} dB\")\nprint(f\"Average Correlation: {results_df['Correlation'].mean():.4f}\")\nprint(f\"Average PRD: {results_df['PRD (%)'].mean():.2f}%\")\nprint(f\"Average RMSE: {results_df['RMSE'].mean():.6f}\")\nprint(\"=\"*80)\n\n# Save results\nresults_df.to_csv('/kaggle/working/validation_metrics.csv', index=False)\nprint(\"\\n✓ Metrics saved to /kaggle/working/validation_metrics.csv\")\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def interpret_snr(snr):\n    \"\"\"Interpret SNR quality for ECG signals\"\"\"\n    if snr >= 80:\n        return \"Excellent - Clinical quality\"\n    elif snr >= 60:\n        return \"Good - Acceptable for analysis\"\n    elif snr >= 40:\n        return \"Fair - May need review\"\n    else:\n        return \"Poor - Not suitable\"\n\nprint(\"\\n\" + \"=\"*80)\nprint(\"SNR QUALITY INTERPRETATION\")\nprint(\"=\"*80)\nfor _, row in results_df.iterrows():\n    quality = interpret_snr(row['SNR (dB)'])\n    print(f\"Lead {row['Lead']:>3}: {row['SNR (dB)']:6.2f} dB - {quality}\")\nprint(\"=\"*80)\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import matplotlib.pyplot as plt\n\n# Visualize predictions vs ground truth for all 12 leads\nfig, axes = plt.subplots(6, 2, figsize=(18, 22))\naxes = axes.flatten()\n\nfs = patient_info['fs']  # Sampling frequency\n\nfor idx, lead in enumerate(leads):\n    if lead in gt.columns and lead in pred.columns:\n        ax = axes[idx]\n        \n        # Plot first 2 seconds\n        time = np.arange(len(gt[lead])) / fs\n        plot_samples = min(int(2 * fs), len(gt[lead]))\n        \n        ax.plot(time[:plot_samples], gt[lead].values[:plot_samples], \n                label='Ground Truth', linewidth=2, alpha=0.9, color='blue')\n        ax.plot(time[:plot_samples], pred[lead].values[:plot_samples], \n                label='Predicted', linewidth=1.5, alpha=0.8, linestyle='--', color='red')\n        \n        # Add metrics to plot\n        lead_metrics = results_df[results_df['Lead'] == lead]\n        if not lead_metrics.empty:\n            snr = lead_metrics['SNR (dB)'].values[0]\n            corr = lead_metrics['Correlation'].values[0]\n            \n            ax.set_title(f'Lead {lead} - SNR: {snr:.1f} dB, Corr: {corr:.3f}', \n                         fontsize=13, fontweight='bold')\n        else:\n            ax.set_title(f'Lead {lead}', fontsize=13, fontweight='bold')\n        \n        ax.set_xlabel('Time (s)', fontsize=11)\n        ax.set_ylabel('Amplitude (mV)', fontsize=11)\n        ax.legend(loc='upper right', fontsize=10)\n        ax.grid(True, alpha=0.3, linestyle='--')\n\nplt.tight_layout()\nplt.savefig('/kaggle/working/ecg_comparison.png', dpi=150, bbox_inches='tight')\nplt.show()\n\nprint(\"\\n✓ Visualization saved to /kaggle/working/ecg_comparison.png\")\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from IPython.display import Image, display\n\n# Display one of the input ECG images\nimage_files = !ls /kaggle/input/physionet-ecg-image-digitization/train/1006427285/*.png\nif image_files:\n    image_path = f'/kaggle/input/physionet-ecg-image-digitization/train/1006427285/{image_files[0]}'\n    print(f\"Sample ECG Image: {image_files[0]}\")\n    display(Image(filename=image_path, width=900))\n","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}