| |
| """ |
| 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 |
|
|
| |
| 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: |
| |
| 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" |
| |
| |
| box_model = YOLO(box_path) |
| lead_name_model = YOLO(lead_name_path) |
| pulse_model = YOLO(pulse_path) |
| |
| |
| 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. |
| """ |
| |
| check_models_configured(config) |
| check_model_files_exist(config) |
| |
| |
| models = load_yolo_models( |
| config.YOLO_BOX_MODEL_PATH, |
| config.YOLO_LEAD_NAME_MODEL_PATH, |
| config.YOLO_PULSE_MODEL_PATH |
| ) |
| |
| |
| 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 |
| ) |
| |
| |
| ecg.run_full_pipeline() |
| |
| |
| df = extract_dataframe_from_ecg(ecg) |
| |
| |
| 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.") |
| |
| |
| padded = [np.pad(s, (0, max_length - len(s)), mode='constant', constant_values=np.nan) for s in signals] |
| |
| |
| df = pd.DataFrame(np.vstack(padded).T, columns=lead_names) |
| return df |
|
|