{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Signal Processing\n",
    "\n",
    "The notebook goes into more detail on the processing of the 12 ECG data per patient. First, the signal itself is considered and then the ECG signal processing methods integrated in the 'wfdb' module are explored. Various approaches are then used to normalize the data and reduce noise in order to make the signals as comparable as possible."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [],
   "source": [
    "from matplotlib import pyplot as plt\n",
    "import wfdb.processing\n",
    "import scipy.signal\n",
    "import sys\n",
    "\n",
    "sys.path.append('../scripts')\n",
    "import data_helper"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Load Data"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Reading GSVT\n",
      "Reading AFIB\n",
      "Reading SR\n",
      "Reading SB\n",
      "Number of patients per category:\n",
      "GSVT: 1027\n",
      "AFIB: 9787\n",
      "SR: 10426\n",
      "SB: 15826\n"
     ]
    }
   ],
   "source": [
    "data = data_helper.load_data(only_demographic=False)\n",
    "\n",
    "print(\"Number of patients per category:\")\n",
    "for cat_name in data.keys():\n",
    "    print(f\"{cat_name}: {len(data[cat_name])}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "For a better understanding of the structure and dimensions of the individual data sets, there are printed."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Keys of first record of SB: dict_keys(['record_name', 'n_sig', 'fs', 'counter_freq', 'base_counter', 'sig_len', 'base_time', 'base_date', 'comments', 'sig_name', 'p_signal', 'd_signal', 'e_p_signal', 'e_d_signal', 'file_name', 'fmt', 'samps_per_frame', 'skew', 'byte_offset', 'adc_gain', 'baseline', 'units', 'adc_res', 'adc_zero', 'init_value', 'checksum', 'block_size'])\n",
      "{'record_name': 'JS00002', 'n_sig': 12, 'fs': 500, 'counter_freq': None, 'base_counter': None, 'sig_len': 5000, 'base_time': None, 'base_date': None, 'comments': ['Age: 59', 'Sex: Female', 'Dx: 426177001,164934002', 'Rx: Unknown', 'Hx: Unknown', 'Sx: Unknown'], 'sig_name': ['I', 'II', 'III', 'aVR', 'aVL', 'aVF', 'V1', 'V2', 'V3', 'V4', 'V5', 'V6'], 'p_signal': array([[-0.01 ,  0.01 ,  0.02 , ...,  0.054,  0.049,  0.   ],\n",
      "       [-0.024, -0.02 ,  0.005, ...,  0.034,  0.034, -0.015],\n",
      "       [-0.02 , -0.02 ,  0.   , ...,  0.034,  0.034, -0.01 ],\n",
      "       ...,\n",
      "       [ 0.015,  0.01 , -0.005, ..., -0.015, -0.02 ,  0.005],\n",
      "       [ 0.01 ,  0.01 ,  0.   , ..., -0.02 , -0.024,  0.   ],\n",
      "       [ 0.01 ,  0.01 ,  0.   , ..., -0.024, -0.029, -0.005]]), 'd_signal': None, 'e_p_signal': None, 'e_d_signal': None, 'file_name': ['JS00002.mat', 'JS00002.mat', 'JS00002.mat', 'JS00002.mat', 'JS00002.mat', 'JS00002.mat', 'JS00002.mat', 'JS00002.mat', 'JS00002.mat', 'JS00002.mat', 'JS00002.mat', 'JS00002.mat'], 'fmt': ['16', '16', '16', '16', '16', '16', '16', '16', '16', '16', '16', '16'], 'samps_per_frame': [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], 'skew': [None, None, None, None, None, None, None, None, None, None, None, None], 'byte_offset': [24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24], 'adc_gain': [1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0], 'baseline': [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 'units': ['mV', 'mV', 'mV', 'mV', 'mV', 'mV', 'mV', 'mV', 'mV', 'mV', 'mV', 'mV'], 'adc_res': [16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16], 'adc_zero': [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 'init_value': [-10, 10, 20, 0, -15, 15, -10, -54, 63, 54, 49, 0], 'checksum': [12346, 26962, 14528, -13537, -6991, 14569, -383, 16468, -29512, -7548, -22267, 31542], 'block_size': [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]}\n",
      "(5000, 12)\n"
     ]
    }
   ],
   "source": [
    "# print keys of first record\n",
    "print(f\"Keys of first record of SB: {data['SB'][0].__dict__.keys()}\")\n",
    "print(data['SB'][0].__dict__)\n",
    "print(data['SB'][0].__dict__['p_signal'].shape)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Number of records with p_signal: 10426\n",
      "Number of records with e_p_signal: 0\n",
      "Number of records with d_signal: 0\n",
      "Number of records with e_d_signal: 0\n"
     ]
    }
   ],
   "source": [
    "# count how many different signals are available\n",
    "counter_p = 0\n",
    "counter_e_p = 0\n",
    "counter_d = 0\n",
    "counter_e_d = 0\n",
    "for record in data['SR']:\n",
    "    if record.p_signal is not None:\n",
    "        counter_p += 1\n",
    "    if record.e_p_signal is not None:\n",
    "        counter_e_p += 1\n",
    "    if record.d_signal is not None:\n",
    "        counter_d += 1\n",
    "\n",
    "print(f\"Number of records with p_signal: {counter_p}\")\n",
    "print(f\"Number of records with e_p_signal: {counter_e_p}\")\n",
    "print(f\"Number of records with d_signal: {counter_d}\")\n",
    "print(f\"Number of records with e_d_signal: {counter_e_d}\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 131692 characters]",
      "text/plain": [
       "<Figure size 640x480 with 12 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "#Plot a complete Record\n",
    "wfdb.plot_wfdb(data['SB'][24],title=\"Example for all Channels\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 79652 characters]",
      "text/plain": [
       "<Figure size 1500x400 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "#Plot specific Channel of Record\n",
    "fig = wfdb.plot_items(data['SB'][24].p_signal[:,0],title=\"Example for Channel 7\",figsize=(15,4),return_fig=True)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [],
   "source": [
    "#Find peaks in Channel 6 of Record 25\n",
    "hard = wfdb.processing.find_local_peaks(data['SB'][24].p_signal[:,6],radius=3)\n",
    "record = data['SB'][24].p_signal[:,6]\n",
    "wholeRecord = data['SB'][24]\n",
    "peak_points_y = [record[i] for i in hard] "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 74764 characters]",
      "text/plain": [
       "<Figure size 1000x500 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "plt.figure(figsize=(10, 5))\n",
    "plt.plot(record)\n",
    "plt.plot(hard, peak_points_y, 'ro', label='Peaks')\n",
    "plt.xlabel('Time')\n",
    "plt.ylabel('Amplitude')\n",
    "plt.title('Signal with Peaks')\n",
    "plt.legend()\n",
    "plt.grid(True)\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Learning initial signal parameters...\n",
      "Failed to find 8 beats during learning.\n",
      "Initializing using default parameters\n",
      "Running QRS detection...\n",
      "QRS detection complete.\n",
      "[ 484 1038 1590 2131 2675 3219 3758 4300 4838]\n"
     ]
    }
   ],
   "source": [
    "# Normalize data by setting qrs peaks to index position 0 (“X-axis normalization”)\n",
    "indicies = wfdb.processing.xqrs_detect(wholeRecord.p_signal[:,6],wholeRecord.fs,0,5000)\n",
    "print(indicies)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "For normalization:    [0.059 0.068 0.083 ... 0.068 0.063 0.049]\n",
      "After normalization:  [0.27931363 0.28789323 0.30219256 ... 0.28789323 0.28312679 0.26978074]\n"
     ]
    }
   ],
   "source": [
    "# Normalization through upper and lower bounds (“Y-axis normalization”)\n",
    "print(\"For normalization:   \", wholeRecord.p_signal[:,0])\n",
    "wholeRecord.p_signal[:,0] = wfdb.processing.normalize_bound(wholeRecord.p_signal[:,0],0,1)\n",
    "print(\"After normalization: \",wholeRecord.p_signal[:,0])\n",
    "\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 130884 characters]",
      "text/plain": [
       "<Figure size 640x480 with 12 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "wfdb.plot_wfdb(wholeRecord,title=\"Example for all Channels\")\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 78632 characters]",
      "text/plain": [
       "<Figure size 1500x400 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "wfdb.plot_items(data['SB'][24].p_signal[:,0],title=\"Example for Channel 1\",figsize=(15,4))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 99172 characters]",
      "text/plain": [
       "<Figure size 1000x500 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "test_data = data['SB'][50].p_signal[:,0]\n",
    "\n",
    "\n",
    "from scipy.signal import butter, lfilter\n",
    "\n",
    "def butter_lowpass(cutoff, fs, order=5):\n",
    "    nyq = 0.5 * fs\n",
    "    normal_cutoff = cutoff / nyq\n",
    "    b, a = butter(order, normal_cutoff, btype='low', analog=False)\n",
    "    return b, a\n",
    "\n",
    "def butter_lowpass_filter(data, cutoff, fs, order=5):\n",
    "    b, a = butter_lowpass(cutoff, fs, order=order)\n",
    "    y = lfilter(b, a, data)\n",
    "    return y\n",
    "\n",
    "# Filter requirements.\n",
    "\n",
    "order = 1\n",
    "fs = 500.0\n",
    "cutoff = 25#25\n",
    "\n",
    "# Get the filter coefficients so we can check its frequency response.\n",
    "b, a = butter_lowpass(cutoff, fs, order)\n",
    "\n",
    "# Apply filter to the signal\n",
    "filtered_signal = butter_lowpass_filter(test_data, cutoff, fs, order)\n",
    "\n",
    "# Plot the signal\n",
    "plt.figure(figsize=(10, 5))\n",
    "plt.plot(test_data, 'b-', label='data')\n",
    "plt.plot(filtered_signal, 'g-', linewidth=2, label='filtered data')\n",
    "plt.xlabel('Time')\n",
    "plt.ylabel('Amplitude')\n",
    "plt.title('Signal with and without Filter')\n",
    "plt.legend()\n",
    "plt.grid(True)\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 95888 characters]",
      "text/plain": [
       "<Figure size 1000x500 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "#Find peaks in Channel 6 of Record 25\n",
    "hard = wfdb.processing.find_local_peaks(filtered_signal,radius=3)\n",
    "record = filtered_signal\n",
    "peak_points_y = [record[i] for i in hard] \n",
    "\n",
    "plt.figure(figsize=(10, 5))\n",
    "plt.plot(record)\n",
    "plt.plot(hard, peak_points_y, 'ro', label='Peaks')\n",
    "plt.xlabel('Time')\n",
    "plt.ylabel('Amplitude')\n",
    "plt.title('Filtered Signal with Peaks')\n",
    "plt.legend()\n",
    "plt.grid(True)\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Learning initial signal parameters...\n",
      "Failed to find 8 beats during learning.\n",
      "Initializing using default parameters\n",
      "Running QRS detection...\n",
      "QRS detection complete.\n",
      "[ 530 1076 1631 2174 2725 3281 3828 4378 4938]\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 62580 characters]",
      "text/plain": [
       "<Figure size 1000x500 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "# Normalize data by setting qrs peaks to index position 0 (“X-axis normalization”)\n",
    "indicies = wfdb.processing.xqrs_detect(filtered_signal,500,0,5000)\n",
    "print(indicies)\n",
    "\n",
    "# detected = [ 530 1076 1631 2174 2725 3281 3828 4378 4938]\n",
    "cut_signal = filtered_signal[indicies[0]:indicies[2]]\n",
    "\n",
    "# plot the cut signal\n",
    "plt.figure(figsize=(10, 5))\n",
    "plt.plot(cut_signal)\n",
    "plt.xlabel('Time')\n",
    "plt.ylabel('Amplitude')\n",
    "plt.title('Cut Signal')\n",
    "plt.grid(True)\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "1076\n",
      "(array([15, 46], dtype=int64),)\n",
      "[[1061 1092]]\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 87604 characters]",
      "text/plain": [
       "<Figure size 1000x500 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "import numpy as np\n",
    "# get the local minimas around the peaks\n",
    "peak = indicies[1]\n",
    "print(peak)\n",
    "start_idx = 30\n",
    "end_idx = 30\n",
    "signal_section = filtered_signal[peak-start_idx:peak+end_idx]\n",
    "minimas = scipy.signal.argrelextrema(signal_section, comparator=np.less, order=5)\n",
    "print(minimas)\n",
    "# map the minimas to the original signal\n",
    "minimas = minimas + peak - start_idx\n",
    "print(minimas)\n",
    "minimas_y = [filtered_signal[i] for i in minimas]\n",
    "\n",
    "# plot the cut signal with the minimas\n",
    "plt.figure(figsize=(10, 5))\n",
    "plt.plot(filtered_signal)\n",
    "plt.plot(minimas, minimas_y, 'ro', label='Minimas')\n",
    "plt.xlabel('Time')\n",
    "plt.ylabel('Amplitude')\n",
    "plt.title('Cut Signal with Minimas')\n",
    "plt.legend()\n",
    "plt.grid(True)\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 20,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 89076 characters]",
      "text/plain": [
       "<Figure size 1000x500 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[-0.027759944001706754, -0.042024054853653535]\n"
     ]
    }
   ],
   "source": [
    "plt.figure(figsize=(10, 5))\n",
    "plt.plot(filtered_signal)\n",
    "plt.xlabel('Time')\n",
    "plt.ylabel('Amplitude')\n",
    "plt.title('Cut Signal with Minimas')\n",
    "plt.grid(True)\n",
    "\n",
    "for peak in indicies:\n",
    "    start_idx = 20\n",
    "    end_idx = 25\n",
    "    signal_section = filtered_signal[peak-start_idx:peak+end_idx]\n",
    "    minimas = scipy.signal.argrelextrema(signal_section, comparator=np.less, order=5)\n",
    "    \n",
    "    # map the minimas to the original signal\n",
    "    minimas = minimas[0] + peak - start_idx\n",
    "    minimas_y = [filtered_signal[i] for i in minimas]\n",
    "    # if minimas more than 2 take those with biggest difference in y\n",
    "    if len(minimas) > 2:\n",
    "        minimas_y = [filtered_signal[i] for i in minimas]\n",
    "        minimas_y = sorted(minimas_y)\n",
    "        minimas_y = minimas_y[:2]\n",
    "        minimas = [minimas[i] for i in range(0,2)]\n",
    "\n",
    "    # plot the minimas\n",
    "    plt.plot(minimas, minimas_y, 'ro')\n",
    "\n",
    "plt.legend(['Signal', 'Minimas'])\n",
    "plt.show()\n",
    "\n",
    "#print y values of minimas\n",
    "print(minimas_y)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 21,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 67812 characters]",
      "text/plain": [
       "<Figure size 1000x500 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "# find peaks in the cut signal\n",
    "hard = wfdb.processing.find_local_peaks(cut_signal,radius=3)\n",
    "#peak_points_y = [cut_signal[i] for i in hard]\n",
    "\n",
    "tolerance = 1\n",
    "# get standard deviation of the hard peaks\n",
    "std = cut_signal[hard].std() * tolerance\n",
    "# filter peaks with std\n",
    "hard = [i for i in hard if cut_signal[i] > std]\n",
    "# get the y values of the peaks\n",
    "peak_points_y = [cut_signal[i] for i in hard]\n",
    "\n",
    "plt.figure(figsize=(10, 5))\n",
    "plt.plot(cut_signal)\n",
    "plt.plot(hard, peak_points_y, 'ro', label='Peaks')\n",
    "plt.xlabel('Time')\n",
    "plt.ylabel('Amplitude')\n",
    "plt.title('Cut Signal with Peaks')\n",
    "plt.legend()\n",
    "plt.grid(True)\n",
    "plt.show()"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.10.4"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
