Source code for vitalDSP.signal_quality_assessment.multi_modal_artifact_detection
"""
Signal Quality Assessment Module for Physiological Signal Processing
This module provides comprehensive capabilities for physiological
signal processing including ECG, PPG, EEG, and other vital signs.
Author: vitalDSP Team
Date: 2025-01-27
Version: 1.0.0
Key Features:
- Multiple processing methods and functions
- NumPy integration for numerical computations
- Pattern and anomaly detection
Examples:
--------
Basic usage:
>>> import numpy as np
>>> from vitalDSP.signal_quality_assessment.multi_modal_artifact_detection import MultiModalArtifactDetection
>>> signal = np.random.randn(1000)
>>> processor = MultiModalArtifactDetection(signal)
>>> result = processor.process()
>>> print(f'Processing result: {result}')
"""
import numpy as np
[docs]
def correlation_based_artifact_detection(signals, threshold=0.5):
"""
Detect artifacts by analyzing the correlation between multiple signals.
Parameters
----------
signals : list of numpy.ndarray
List of signals to analyze for artifacts.
threshold : float, optional (default=0.5)
The correlation threshold below which points are considered artifacts.
Returns
-------
artifact_indices : numpy.ndarray
Indices of the detected artifacts across all signals.
Examples
--------
>>> signal1 = np.sin(2 * np.pi * 0.2 * np.arange(0, 10, 0.01))
>>> signal2 = np.sin(2 * np.pi * 0.2 * np.arange(0, 10, 0.01) + 0.5)
>>> artifacts = correlation_based_artifact_detection([signal1, signal2], threshold=0.7)
>>> print(artifacts)
"""
if len(signals) == 0:
raise ValueError("Signal list cannot be empty")
combined_signal = np.mean(np.array(signals), axis=0)
correlations = np.array(
[np.corrcoef(combined_signal, signal)[0, 1] for signal in signals]
)
# Adjust to select indices where the correlation is below the threshold
artifact_indices = np.where(correlations < threshold)[0]
return artifact_indices
[docs]
def energy_ratio_artifact_detection(signals, window_size=100, threshold=0.5):
"""
Detect artifacts by analyzing the energy ratio between multiple signals.
This version doesn't involve mutual information, but checks for energy ratios.
Parameters
----------
signals : list of numpy.ndarray
List of signals to analyze for artifacts.
window_size : int, optional (default=100)
The size of the window for local analysis.
threshold : float, optional (default=0.5)
The energy ratio threshold below which points are considered artifacts.
Returns
-------
artifact_indices : numpy.ndarray
Indices of the detected artifacts across all signals.
Examples
--------
>>> signal1 = np.sin(2 * np.pi * 0.2 * np.arange(0, 10, 0.01))
>>> signal2 = np.sin(2 * np.pi * 0.2 * np.arange(0, 10, 0.01) + 0.5)
>>> artifacts = energy_ratio_artifact_detection([signal1, signal2], window_size=50, threshold=0.6)
>>> print(artifacts)
"""
if len(signals) == 0:
raise ValueError("Signal list cannot be empty")
artifact_indices = []
combined_signal = np.mean(np.array(signals), axis=0)
for i in range(0, len(combined_signal) - window_size + 1, window_size):
window_energies = [
np.sum(signal[i : i + window_size] ** 2) for signal in signals
]
if np.max(window_energies) == 0:
continue # Avoid division by zero in energy ratio calculation
energy_ratio = np.min(window_energies) / np.max(window_energies)
if energy_ratio < threshold:
artifact_indices.extend(range(i, i + window_size))
return np.array(artifact_indices)
[docs]
def mutual_information_artifact_detection(signals, num_bins=10, threshold=0.1):
"""
Detect artifacts using mutual information between multiple signals.
Parameters
----------
signals : list of numpy.ndarray
List of signals to analyze for artifacts.
num_bins : int, optional (default=10)
Number of bins to use for histogram estimation in mutual information.
threshold : float, optional (default=0.1)
The mutual information threshold below which points are considered artifacts.
Returns
-------
artifact_indices : numpy.ndarray
Indices of the detected artifacts across all signals.
Examples
--------
>>> signal1 = np.sin(2 * np.pi * 0.2 * np.arange(0, 10, 0.01))
>>> signal2 = np.sin(2 * np.pi * 0.2 * np.arange(0, 10, 0.01) + 0.5)
>>> artifacts = mutual_information_artifact_detection([signal1, signal2], threshold=0.05)
>>> print(artifacts)
"""
if threshold < 0:
raise ValueError("Threshold must be a non-negative value.")
if len(signals) == 0:
raise ValueError("Signal list cannot be empty")
def mutual_information(x, y, num_bins):
# Create a 2D histogram for the joint distribution
joint_histogram = np.histogram2d(x, y, bins=num_bins)[0]
joint_prob = joint_histogram / np.sum(joint_histogram)
# Marginal distributions for x and y
x_marginal = np.sum(
joint_prob, axis=1, keepdims=True
) # Reshape to (num_bins, 1)
y_marginal = np.sum(
joint_prob, axis=0, keepdims=True
) # Reshape to (1, num_bins)
# Get non-zero joint probabilities and their corresponding marginals
non_zero_joint = joint_prob[joint_prob > 0]
# Create a meshgrid of x and y marginals matching the shape of the joint_prob
x_marginal_mesh, y_marginal_mesh = np.meshgrid(
x_marginal.flatten(), y_marginal.flatten(), indexing="ij"
)
# Select the marginals that correspond to the non-zero joint probabilities
valid_x_marginals = x_marginal_mesh[joint_prob > 0]
valid_y_marginals = y_marginal_mesh[joint_prob > 0]
# Compute mutual information (using only non-zero entries)
mi = np.sum(
non_zero_joint
* np.log(
non_zero_joint / (valid_x_marginals * valid_y_marginals + 1e-9)
) # To avoid division by zero
)
return mi
artifact_indices = []
combined_signal = np.mean(np.array(signals), axis=0)
for i in range(len(combined_signal)):
mutual_infos = [
mutual_information(combined_signal, signal, num_bins) for signal in signals
]
if np.min(mutual_infos) < threshold:
artifact_indices.append(i)
return np.array(artifact_indices)