ECGLight / backend /digitization_runner.py
nsrek's picture
Upload folder using huggingface_hub
e4d73f9 verified
Raw
History Blame Contribute Delete
5.85 kB
# backend/digitization_runner.py
"""
Backend adapter for ECG Digitization.
Wraps the ECGImage pipeline from digitization.py and manages model caching.
"""
import os
import sys
import pandas as pd
import numpy as np
import streamlit as st
import torch
# Add parent directory to path to ensure digitization and other local modules are importable
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
try:
from digitization import ECGImage
from ultralytics import YOLO
except ImportError as e:
# We will log the error but keep running so the dashboard can load and display elegant warnings
pass
class ModelPathNotSetError(Exception):
"""Raised when one or more YOLO model paths are not set in the configuration."""
pass
class ModelFileNotFoundError(Exception):
"""Raised when a specified YOLO model file does not exist on disk."""
pass
def check_models_configured(config):
"""
Verify that all YOLO model paths are set in the configuration.
"""
missing_configs = []
if not config.YOLO_BOX_MODEL_PATH:
missing_configs.append("YOLO Box Model (YOLO_BOX_MODEL_PATH)")
if not config.YOLO_LEAD_NAME_MODEL_PATH:
missing_configs.append("YOLO Lead Name Model (YOLO_LEAD_NAME_MODEL_PATH)")
if not config.YOLO_PULSE_MODEL_PATH:
missing_configs.append("YOLO Pulse Model (YOLO_PULSE_MODEL_PATH)")
if not config.YOLO_SEGMENTATION_MODEL_PATH:
missing_configs.append("YOLO Segmentation Model (YOLO_SEGMENTATION_MODEL_PATH)")
if missing_configs:
raise ModelPathNotSetError(
f"The following YOLO model paths are not set in config.py:\n" +
"\n".join([f"- {m}" for m in missing_configs]) +
"\n\nPlease configure these paths in config.py before running digitization."
)
def check_model_files_exist(config):
"""
Verify that the configured model files actually exist on disk.
"""
missing_files = []
for name, path in [
("Box Model", config.YOLO_BOX_MODEL_PATH),
("Lead Name Model", config.YOLO_LEAD_NAME_MODEL_PATH),
("Pulse Model", config.YOLO_PULSE_MODEL_PATH),
("Segmentation Model", config.YOLO_SEGMENTATION_MODEL_PATH)
]:
if path and not os.path.exists(path):
missing_files.append(f"{name} at path: '{path}'")
if missing_files:
raise ModelFileNotFoundError(
f"The following model files were not found on disk:\n" +
"\n".join([f"- {f}" for f in missing_files]) +
"\n\nPlease verify that the files are downloaded and located at these paths."
)
@st.cache_resource
def load_yolo_models(box_path, lead_name_path, pulse_path):
"""
Load YOLO models once and cache them in Streamlit memory.
Returns a dict containing the loaded model objects.
"""
device = "cuda" if torch.cuda.is_available() else "cpu"
# Load each model
box_model = YOLO(box_path)
lead_name_model = YOLO(lead_name_path)
pulse_model = YOLO(pulse_path)
# Move to GPU if available
box_model.to(device)
lead_name_model.to(device)
pulse_model.to(device)
return {
"box_model": box_model,
"lead_name_model": lead_name_model,
"pulse_model": pulse_model,
"device": device
}
def run_digitization_pipeline(image_path, config):
"""
Runs the full digitization pipeline on a single ECG image.
Parameters
----------
image_path : str
Path to the uploaded image file.
config : module
The config module containing paths and settings.
Returns
-------
df : pandas.DataFrame
Digitized ECG signals with lead names as columns and timesteps as rows.
ecg_instance : ECGImage
The raw ECGImage instance for accessing signal_grid, masks, or metadata.
"""
# 1. Validation
check_models_configured(config)
check_model_files_exist(config)
# 2. Load models (cached via st.cache_resource)
models = load_yolo_models(
config.YOLO_BOX_MODEL_PATH,
config.YOLO_LEAD_NAME_MODEL_PATH,
config.YOLO_PULSE_MODEL_PATH
)
# 3. Instantiate ECGImage
ecg = ECGImage(
box_model=models["box_model"],
segmentation_model=config.YOLO_SEGMENTATION_MODEL_PATH,
lead_name_model=models["lead_name_model"],
pulse_model=models["pulse_model"],
image_path=image_path
)
# 4. Run the pipeline (without Linux-specific SIGALRM timeouts)
ecg.run_full_pipeline()
# 5. Extract DataFrame from signal_grid
df = extract_dataframe_from_ecg(ecg)
# 6. Clear GPU cache if needed
if torch.cuda.is_available():
try:
torch.cuda.synchronize()
torch.cuda.empty_cache()
except Exception:
pass
return df, ecg
def extract_dataframe_from_ecg(ecg_instance):
"""
Extracts signal data from an ECGImage instance and converts it to a pandas DataFrame.
"""
signals, lead_names, max_length = [], [], 0
for row in ecg_instance.signal_grid:
for cell in row:
if 'signal' in cell and 'lead' in cell:
sig = np.asarray(cell['signal'], dtype=float)
signals.append(sig)
lead_names.append(cell['lead'])
max_length = max(max_length, len(sig))
if not signals:
raise ValueError("No valid lead signals were extracted from the ECG image.")
# Pad shorter signals with NaN to make them uniform length
padded = [np.pad(s, (0, max_length - len(s)), mode='constant', constant_values=np.nan) for s in signals]
# Build DataFrame
df = pd.DataFrame(np.vstack(padded).T, columns=lead_names)
return df