deepecgkit 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- deepecgkit/__init__.py +42 -0
- deepecgkit/__main__.py +6 -0
- deepecgkit/cli/__init__.py +61 -0
- deepecgkit/cli/commands/__init__.py +16 -0
- deepecgkit/cli/commands/evaluate.py +83 -0
- deepecgkit/cli/commands/info.py +73 -0
- deepecgkit/cli/commands/predict.py +94 -0
- deepecgkit/cli/commands/train.py +331 -0
- deepecgkit/cli/config.py +23 -0
- deepecgkit/cli/evaluation.py +251 -0
- deepecgkit/cli/logger.py +26 -0
- deepecgkit/cli/main.py +429 -0
- deepecgkit/cli/registry.py +32 -0
- deepecgkit/datasets/__init__.py +33 -0
- deepecgkit/datasets/af_classification.py +301 -0
- deepecgkit/datasets/base.py +146 -0
- deepecgkit/datasets/ltafdb.py +363 -0
- deepecgkit/datasets/mitbih_afdb.py +287 -0
- deepecgkit/datasets/modules.py +222 -0
- deepecgkit/datasets/preprocessing.py +208 -0
- deepecgkit/datasets/ptbxl.py +567 -0
- deepecgkit/datasets/splitting.py +225 -0
- deepecgkit/datasets/unified_af.py +316 -0
- deepecgkit/evaluation/__init__.py +40 -0
- deepecgkit/evaluation/evaluator.py +398 -0
- deepecgkit/evaluation/metrics.py +164 -0
- deepecgkit/evaluation/visualization.py +285 -0
- deepecgkit/models/__init__.py +51 -0
- deepecgkit/models/af_classifier.py +177 -0
- deepecgkit/models/convnext_v2_1d.py +183 -0
- deepecgkit/models/crnn.py +117 -0
- deepecgkit/models/deep_res_cnn.py +215 -0
- deepecgkit/models/ecg_dualnet.py +230 -0
- deepecgkit/models/fcn_wang.py +88 -0
- deepecgkit/models/gru_model.py +99 -0
- deepecgkit/models/inception_time.py +197 -0
- deepecgkit/models/kanres_wide_x.py +150 -0
- deepecgkit/models/kanres_x.py +126 -0
- deepecgkit/models/lstm_model.py +96 -0
- deepecgkit/models/mamba1d.py +232 -0
- deepecgkit/models/medformer.py +229 -0
- deepecgkit/models/resnet1d.py +189 -0
- deepecgkit/models/resnet1d_wang.py +182 -0
- deepecgkit/models/se_resnet1d.py +232 -0
- deepecgkit/models/simple_cnn.py +82 -0
- deepecgkit/models/tcn.py +178 -0
- deepecgkit/models/transformer_ecg.py +127 -0
- deepecgkit/models/xresnet1d.py +265 -0
- deepecgkit/models/xresnet1d_benchmark.py +227 -0
- deepecgkit/registry.py +97 -0
- deepecgkit/training/__init__.py +9 -0
- deepecgkit/training/train.py +171 -0
- deepecgkit/utils/__init__.py +72 -0
- deepecgkit/utils/download.py +123 -0
- deepecgkit/utils/weights.py +107 -0
- deepecgkit-0.1.0.dist-info/METADATA +99 -0
- deepecgkit-0.1.0.dist-info/RECORD +61 -0
- deepecgkit-0.1.0.dist-info/WHEEL +5 -0
- deepecgkit-0.1.0.dist-info/entry_points.txt +2 -0
- deepecgkit-0.1.0.dist-info/licenses/LICENSE +201 -0
- deepecgkit-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
"""
|
|
2
|
+
PyTorch Lightning data module for ECG datasets.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Dict, Optional, Type, Union
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
import pytorch_lightning as pl
|
|
9
|
+
import torch
|
|
10
|
+
from torch.utils.data import DataLoader, Dataset
|
|
11
|
+
|
|
12
|
+
from deepecgkit.datasets.base import BaseECGDataset
|
|
13
|
+
from deepecgkit.datasets.splitting import DataSplitter
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ECGDataModule(pl.LightningDataModule):
|
|
17
|
+
"""PyTorch Lightning data module for ECG datasets."""
|
|
18
|
+
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
dataset: Optional[Union[BaseECGDataset, Dataset]] = None,
|
|
22
|
+
dataset_class: Optional[Type[BaseECGDataset]] = None,
|
|
23
|
+
data_dir: Optional[str] = None,
|
|
24
|
+
sampling_rate: int = 500,
|
|
25
|
+
leads: Optional[list] = None,
|
|
26
|
+
transform: Optional[callable] = None,
|
|
27
|
+
target_transform: Optional[callable] = None,
|
|
28
|
+
download: bool = False,
|
|
29
|
+
batch_size: int = 32,
|
|
30
|
+
val_split: float = 0.2,
|
|
31
|
+
test_split: float = 0.1,
|
|
32
|
+
num_workers: int = 4,
|
|
33
|
+
seed: int = 42,
|
|
34
|
+
stratify: bool = True,
|
|
35
|
+
pin_memory: bool = False,
|
|
36
|
+
persistent_workers: bool = True,
|
|
37
|
+
prefetch_factor: int = 2,
|
|
38
|
+
verbose: bool = True,
|
|
39
|
+
dataset_kwargs: Optional[Dict] = None,
|
|
40
|
+
):
|
|
41
|
+
"""Initialize the ECG data module.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
dataset: PyTorch dataset instance (optional)
|
|
45
|
+
dataset_class: Class of dataset to create (optional)
|
|
46
|
+
data_dir: Directory containing ECG data files (optional, uses dataset's default if None)
|
|
47
|
+
sampling_rate: Sampling rate of the ECG signals (Hz)
|
|
48
|
+
leads: List of leads to use (e.g., ['I', 'II', 'III'] for standard leads)
|
|
49
|
+
transform: Optional transform to be applied to the ECG signals
|
|
50
|
+
target_transform: Optional transform to be applied to the labels
|
|
51
|
+
download: Whether to download the dataset if it doesn't exist
|
|
52
|
+
batch_size: Batch size for data loaders
|
|
53
|
+
val_split: Fraction of data to use for validation
|
|
54
|
+
test_split: Fraction of data to use for testing
|
|
55
|
+
num_workers: Number of worker processes for data loading
|
|
56
|
+
seed: Random seed for reproducibility
|
|
57
|
+
stratify: Whether to use stratified splitting based on labels
|
|
58
|
+
dataset_kwargs: Additional keyword arguments to pass to the dataset class
|
|
59
|
+
"""
|
|
60
|
+
super().__init__()
|
|
61
|
+
self.dataset = dataset
|
|
62
|
+
self.dataset_class = dataset_class
|
|
63
|
+
self.data_dir = data_dir
|
|
64
|
+
self.sampling_rate = sampling_rate
|
|
65
|
+
self.leads = leads
|
|
66
|
+
self.transform = transform
|
|
67
|
+
self.target_transform = target_transform
|
|
68
|
+
self.download = download
|
|
69
|
+
self.batch_size = batch_size
|
|
70
|
+
self.val_split = val_split
|
|
71
|
+
self.test_split = test_split
|
|
72
|
+
self.num_workers = num_workers
|
|
73
|
+
self.seed = seed
|
|
74
|
+
self.stratify = stratify
|
|
75
|
+
self.pin_memory = pin_memory
|
|
76
|
+
self.persistent_workers = persistent_workers
|
|
77
|
+
self.prefetch_factor = prefetch_factor
|
|
78
|
+
self.verbose = verbose
|
|
79
|
+
self.dataset_kwargs = dataset_kwargs or {}
|
|
80
|
+
|
|
81
|
+
self.train_dataset = None
|
|
82
|
+
self.val_dataset = None
|
|
83
|
+
self.test_dataset = None
|
|
84
|
+
|
|
85
|
+
def setup(self, stage: Optional[str] = None):
|
|
86
|
+
"""Set up the datasets.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
stage: Current stage ('fit', 'validate', 'test', or 'predict')
|
|
90
|
+
"""
|
|
91
|
+
if self.dataset is None:
|
|
92
|
+
if self.dataset_class is None:
|
|
93
|
+
raise ValueError("dataset_class must be provided if dataset is None")
|
|
94
|
+
|
|
95
|
+
if self.data_dir is None:
|
|
96
|
+
self.data_dir = self.dataset_class.get_default_data_dir()
|
|
97
|
+
|
|
98
|
+
init_kwargs = {
|
|
99
|
+
"data_dir": self.data_dir,
|
|
100
|
+
"sampling_rate": self.sampling_rate,
|
|
101
|
+
"transform": self.transform,
|
|
102
|
+
"target_transform": self.target_transform,
|
|
103
|
+
"download": self.download,
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if self.leads is not None:
|
|
107
|
+
init_kwargs["leads"] = self.leads
|
|
108
|
+
|
|
109
|
+
init_kwargs.update(self.dataset_kwargs)
|
|
110
|
+
|
|
111
|
+
self.dataset = self.dataset_class(**init_kwargs)
|
|
112
|
+
|
|
113
|
+
stratify_labels = None
|
|
114
|
+
if (
|
|
115
|
+
self.stratify and len(self.dataset) >= 8
|
|
116
|
+
): # Need at least 2 samples per class for 4 classes
|
|
117
|
+
try:
|
|
118
|
+
all_labels = []
|
|
119
|
+
for i in range(len(self.dataset)):
|
|
120
|
+
_, label = self.dataset[i]
|
|
121
|
+
all_labels.append(label)
|
|
122
|
+
stacked = torch.stack(all_labels).numpy()
|
|
123
|
+
|
|
124
|
+
# For multi-label (2D), convert rows to string keys for stratification
|
|
125
|
+
if stacked.ndim > 1:
|
|
126
|
+
stratify_labels = np.array(
|
|
127
|
+
["_".join(str(int(v)) for v in row) for row in stacked]
|
|
128
|
+
)
|
|
129
|
+
else:
|
|
130
|
+
stratify_labels = stacked
|
|
131
|
+
|
|
132
|
+
# Check if we have enough samples per class for stratification
|
|
133
|
+
_, counts = np.unique(stratify_labels, return_counts=True)
|
|
134
|
+
if np.min(counts) < 2:
|
|
135
|
+
stratify_labels = None # Disable stratification
|
|
136
|
+
except Exception:
|
|
137
|
+
stratify_labels = None # Fallback to no stratification
|
|
138
|
+
|
|
139
|
+
# Extract groups for patient-level splitting (prevents data leakage)
|
|
140
|
+
groups = None
|
|
141
|
+
if hasattr(self.dataset, "record_names") and self.dataset.record_names:
|
|
142
|
+
groups = np.array(self.dataset.record_names)
|
|
143
|
+
if self.verbose:
|
|
144
|
+
n_groups = len(np.unique(groups))
|
|
145
|
+
print(f"Using patient-level splitting ({n_groups} groups)")
|
|
146
|
+
|
|
147
|
+
splitter = DataSplitter(
|
|
148
|
+
dataset=self.dataset,
|
|
149
|
+
val_split=self.val_split,
|
|
150
|
+
test_split=self.test_split,
|
|
151
|
+
seed=self.seed,
|
|
152
|
+
stratify=stratify_labels,
|
|
153
|
+
groups=groups,
|
|
154
|
+
)
|
|
155
|
+
self.train_dataset, self.val_dataset, self.test_dataset = splitter.split()
|
|
156
|
+
|
|
157
|
+
if self.verbose:
|
|
158
|
+
print(f"Dataset size: {len(self.train_dataset)}")
|
|
159
|
+
print(f"Validation set size: {len(self.val_dataset)}")
|
|
160
|
+
print(f"Test set size: {len(self.test_dataset)}")
|
|
161
|
+
|
|
162
|
+
def train_dataloader(self) -> DataLoader:
|
|
163
|
+
"""Get the training data loader."""
|
|
164
|
+
if self.train_dataset is None:
|
|
165
|
+
raise RuntimeError("Call setup() before using the data module")
|
|
166
|
+
return DataLoader(
|
|
167
|
+
self.train_dataset,
|
|
168
|
+
batch_size=self.batch_size,
|
|
169
|
+
shuffle=True,
|
|
170
|
+
num_workers=self.num_workers,
|
|
171
|
+
pin_memory=self.pin_memory,
|
|
172
|
+
persistent_workers=self.persistent_workers and self.num_workers > 0,
|
|
173
|
+
prefetch_factor=self.prefetch_factor if self.num_workers > 0 else None,
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
def val_dataloader(self) -> DataLoader:
|
|
177
|
+
"""Get the validation data loader."""
|
|
178
|
+
if self.val_dataset is None:
|
|
179
|
+
raise RuntimeError("Call setup() before using the data module")
|
|
180
|
+
return DataLoader(
|
|
181
|
+
self.val_dataset,
|
|
182
|
+
batch_size=self.batch_size,
|
|
183
|
+
shuffle=False,
|
|
184
|
+
num_workers=self.num_workers,
|
|
185
|
+
pin_memory=self.pin_memory,
|
|
186
|
+
persistent_workers=self.persistent_workers and self.num_workers > 0,
|
|
187
|
+
prefetch_factor=self.prefetch_factor if self.num_workers > 0 else None,
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
def test_dataloader(self) -> DataLoader:
|
|
191
|
+
"""Get the test data loader."""
|
|
192
|
+
if self.test_dataset is None:
|
|
193
|
+
raise RuntimeError("Call setup() before using the data module")
|
|
194
|
+
return DataLoader(
|
|
195
|
+
self.test_dataset,
|
|
196
|
+
batch_size=self.batch_size,
|
|
197
|
+
shuffle=False,
|
|
198
|
+
num_workers=self.num_workers,
|
|
199
|
+
pin_memory=self.pin_memory,
|
|
200
|
+
persistent_workers=self.persistent_workers and self.num_workers > 0,
|
|
201
|
+
prefetch_factor=self.prefetch_factor if self.num_workers > 0 else None,
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
def get_metadata(self) -> Dict:
|
|
205
|
+
"""Get metadata about the dataset.
|
|
206
|
+
|
|
207
|
+
Returns:
|
|
208
|
+
Dictionary containing dataset metadata
|
|
209
|
+
"""
|
|
210
|
+
if self.dataset is None:
|
|
211
|
+
raise RuntimeError("Call setup() before getting metadata")
|
|
212
|
+
if isinstance(self.dataset, BaseECGDataset):
|
|
213
|
+
return self.dataset.get_metadata()
|
|
214
|
+
return {
|
|
215
|
+
"dataset_size": len(self.dataset),
|
|
216
|
+
"batch_size": self.batch_size,
|
|
217
|
+
"num_workers": self.num_workers,
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
def print_metadata(self):
|
|
221
|
+
"""Print metadata about the dataset."""
|
|
222
|
+
print(self.get_metadata())
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
from typing import ClassVar, Dict, Optional, Tuple, Union
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
import torch
|
|
5
|
+
from scipy import signal
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ECGStandardizer:
|
|
9
|
+
def __init__(
|
|
10
|
+
self,
|
|
11
|
+
target_sampling_rate: int = 300,
|
|
12
|
+
target_length: Optional[int] = None,
|
|
13
|
+
target_duration_seconds: Optional[float] = None,
|
|
14
|
+
normalization: str = "zscore",
|
|
15
|
+
clip_method: str = "center",
|
|
16
|
+
):
|
|
17
|
+
self.target_sampling_rate = target_sampling_rate
|
|
18
|
+
self.normalization = normalization
|
|
19
|
+
self.clip_method = clip_method
|
|
20
|
+
|
|
21
|
+
if target_length is not None and target_duration_seconds is not None:
|
|
22
|
+
raise ValueError("Cannot specify both target_length and target_duration_seconds")
|
|
23
|
+
|
|
24
|
+
if target_duration_seconds is not None:
|
|
25
|
+
self.target_length = int(target_duration_seconds * target_sampling_rate)
|
|
26
|
+
else:
|
|
27
|
+
self.target_length = target_length
|
|
28
|
+
|
|
29
|
+
def resample(self, ecg_signal: np.ndarray, original_sampling_rate: int) -> np.ndarray:
|
|
30
|
+
if original_sampling_rate == self.target_sampling_rate:
|
|
31
|
+
return ecg_signal
|
|
32
|
+
|
|
33
|
+
num_samples = ecg_signal.shape[-1]
|
|
34
|
+
target_samples = int(num_samples * self.target_sampling_rate / original_sampling_rate)
|
|
35
|
+
|
|
36
|
+
if ecg_signal.ndim == 1:
|
|
37
|
+
return signal.resample(ecg_signal, target_samples)
|
|
38
|
+
else:
|
|
39
|
+
resampled = np.zeros((ecg_signal.shape[0], target_samples))
|
|
40
|
+
for i in range(ecg_signal.shape[0]):
|
|
41
|
+
resampled[i] = signal.resample(ecg_signal[i], target_samples)
|
|
42
|
+
return resampled
|
|
43
|
+
|
|
44
|
+
def normalize(self, ecg_signal: np.ndarray) -> np.ndarray:
|
|
45
|
+
if self.normalization == "zscore":
|
|
46
|
+
mean = np.mean(ecg_signal, axis=-1, keepdims=True)
|
|
47
|
+
std = np.std(ecg_signal, axis=-1, keepdims=True)
|
|
48
|
+
return (ecg_signal - mean) / (std + 1e-8)
|
|
49
|
+
|
|
50
|
+
elif self.normalization == "minmax":
|
|
51
|
+
min_val = np.min(ecg_signal, axis=-1, keepdims=True)
|
|
52
|
+
max_val = np.max(ecg_signal, axis=-1, keepdims=True)
|
|
53
|
+
range_val = max_val - min_val
|
|
54
|
+
return np.where(
|
|
55
|
+
range_val > 1e-8, (ecg_signal - min_val) / range_val, ecg_signal - min_val
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
elif self.normalization == "none":
|
|
59
|
+
return ecg_signal
|
|
60
|
+
|
|
61
|
+
else:
|
|
62
|
+
raise ValueError(f"Unknown normalization method: {self.normalization}")
|
|
63
|
+
|
|
64
|
+
def clip_or_pad(self, ecg_signal: np.ndarray) -> np.ndarray:
|
|
65
|
+
if self.target_length is None or ecg_signal.shape[-1] == self.target_length:
|
|
66
|
+
return ecg_signal
|
|
67
|
+
|
|
68
|
+
current_length = ecg_signal.shape[-1]
|
|
69
|
+
|
|
70
|
+
if ecg_signal.ndim == 1:
|
|
71
|
+
ecg_signal = ecg_signal[np.newaxis, :]
|
|
72
|
+
|
|
73
|
+
if self.clip_method not in ("center", "start", "end"):
|
|
74
|
+
raise ValueError(f"Unknown clip_method: {self.clip_method}")
|
|
75
|
+
|
|
76
|
+
if current_length < self.target_length:
|
|
77
|
+
return self._pad_signal(ecg_signal, current_length)
|
|
78
|
+
return self._clip_signal(ecg_signal, current_length)
|
|
79
|
+
|
|
80
|
+
def _pad_signal(self, ecg_signal: np.ndarray, current_length: int) -> np.ndarray:
|
|
81
|
+
pad_width = self.target_length - current_length
|
|
82
|
+
if self.clip_method == "center":
|
|
83
|
+
pad_left = pad_width // 2
|
|
84
|
+
pad_right = pad_width - pad_left
|
|
85
|
+
return np.pad(ecg_signal, ((0, 0), (pad_left, pad_right)), mode="constant")
|
|
86
|
+
if self.clip_method == "start":
|
|
87
|
+
return np.pad(ecg_signal, ((0, 0), (0, pad_width)), mode="constant")
|
|
88
|
+
return np.pad(ecg_signal, ((0, 0), (pad_width, 0)), mode="constant")
|
|
89
|
+
|
|
90
|
+
def _clip_signal(self, ecg_signal: np.ndarray, current_length: int) -> np.ndarray:
|
|
91
|
+
excess = current_length - self.target_length
|
|
92
|
+
if self.clip_method == "center":
|
|
93
|
+
start = excess // 2
|
|
94
|
+
return ecg_signal[:, start : start + self.target_length]
|
|
95
|
+
if self.clip_method == "start":
|
|
96
|
+
return ecg_signal[:, : self.target_length]
|
|
97
|
+
return ecg_signal[:, -self.target_length :]
|
|
98
|
+
|
|
99
|
+
def __call__(self, ecg_signal: np.ndarray, original_sampling_rate: int) -> np.ndarray:
|
|
100
|
+
ecg_signal = self.resample(ecg_signal, original_sampling_rate)
|
|
101
|
+
ecg_signal = self.clip_or_pad(ecg_signal)
|
|
102
|
+
ecg_signal = self.normalize(ecg_signal)
|
|
103
|
+
return ecg_signal
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class ECGSegmenter:
|
|
107
|
+
def __init__(
|
|
108
|
+
self,
|
|
109
|
+
segment_duration_seconds: float,
|
|
110
|
+
sampling_rate: int,
|
|
111
|
+
overlap: float = 0.0,
|
|
112
|
+
):
|
|
113
|
+
self.segment_duration_seconds = segment_duration_seconds
|
|
114
|
+
self.sampling_rate = sampling_rate
|
|
115
|
+
self.overlap = overlap
|
|
116
|
+
self.segment_length = int(segment_duration_seconds * sampling_rate)
|
|
117
|
+
self.stride = int(self.segment_length * (1 - overlap))
|
|
118
|
+
|
|
119
|
+
def segment(self, ecg_signal: np.ndarray) -> Tuple[np.ndarray, np.ndarray]:
|
|
120
|
+
if ecg_signal.ndim == 1:
|
|
121
|
+
ecg_signal = ecg_signal[np.newaxis, :]
|
|
122
|
+
|
|
123
|
+
_, signal_length = ecg_signal.shape
|
|
124
|
+
|
|
125
|
+
if signal_length < self.segment_length:
|
|
126
|
+
return np.array([]), np.array([])
|
|
127
|
+
|
|
128
|
+
segments = []
|
|
129
|
+
start_indices = []
|
|
130
|
+
|
|
131
|
+
for start in range(0, signal_length - self.segment_length + 1, self.stride):
|
|
132
|
+
segment = ecg_signal[:, start : start + self.segment_length]
|
|
133
|
+
segments.append(segment)
|
|
134
|
+
start_indices.append(start)
|
|
135
|
+
|
|
136
|
+
return np.array(segments), np.array(start_indices)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class RhythmAnnotationExtractor:
|
|
140
|
+
RHYTHM_MAP: ClassVar[Dict[str, int]] = {
|
|
141
|
+
"(AFIB": 1,
|
|
142
|
+
"(AFL": 2,
|
|
143
|
+
"(J": 3,
|
|
144
|
+
"(N": 0,
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
def __init__(
|
|
148
|
+
self,
|
|
149
|
+
sampling_rate: int,
|
|
150
|
+
binary_classification: bool = False,
|
|
151
|
+
):
|
|
152
|
+
self.sampling_rate = sampling_rate
|
|
153
|
+
self.binary_classification = binary_classification
|
|
154
|
+
|
|
155
|
+
def extract_labels(
|
|
156
|
+
self,
|
|
157
|
+
annotation,
|
|
158
|
+
signal_length: int,
|
|
159
|
+
original_sampling_rate: Optional[int] = None,
|
|
160
|
+
) -> np.ndarray:
|
|
161
|
+
labels = np.zeros(signal_length, dtype=np.int64)
|
|
162
|
+
|
|
163
|
+
if not hasattr(annotation, "aux_note") or not hasattr(annotation, "sample"):
|
|
164
|
+
return labels
|
|
165
|
+
|
|
166
|
+
scale = 1.0
|
|
167
|
+
if original_sampling_rate is not None and original_sampling_rate != self.sampling_rate:
|
|
168
|
+
scale = self.sampling_rate / original_sampling_rate
|
|
169
|
+
|
|
170
|
+
for i, (sample_idx, aux_note) in enumerate(zip(annotation.sample, annotation.aux_note)):
|
|
171
|
+
scaled_idx = int(sample_idx * scale)
|
|
172
|
+
if scaled_idx >= signal_length:
|
|
173
|
+
break
|
|
174
|
+
|
|
175
|
+
rhythm_code = self.RHYTHM_MAP.get(aux_note.strip(), 0)
|
|
176
|
+
|
|
177
|
+
if self.binary_classification:
|
|
178
|
+
rhythm_code = 1 if rhythm_code == 1 else 0
|
|
179
|
+
|
|
180
|
+
raw_next = annotation.sample[i + 1] if i + 1 < len(annotation.sample) else signal_length
|
|
181
|
+
next_sample = min(int(raw_next * scale), signal_length)
|
|
182
|
+
|
|
183
|
+
labels[scaled_idx:next_sample] = rhythm_code
|
|
184
|
+
|
|
185
|
+
return labels
|
|
186
|
+
|
|
187
|
+
def segment_with_labels(
|
|
188
|
+
self, labels: np.ndarray, segment_start_indices: np.ndarray, segment_length: int
|
|
189
|
+
) -> np.ndarray:
|
|
190
|
+
segment_labels = []
|
|
191
|
+
|
|
192
|
+
for start_idx in segment_start_indices:
|
|
193
|
+
segment_label_region = labels[start_idx : start_idx + segment_length]
|
|
194
|
+
|
|
195
|
+
unique, counts = np.unique(segment_label_region, return_counts=True)
|
|
196
|
+
majority_label = unique[np.argmax(counts)]
|
|
197
|
+
|
|
198
|
+
segment_labels.append(majority_label)
|
|
199
|
+
|
|
200
|
+
return np.array(segment_labels)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def convert_to_tensor(
|
|
204
|
+
data: Union[np.ndarray, torch.Tensor], dtype: torch.dtype = torch.float32
|
|
205
|
+
) -> torch.Tensor:
|
|
206
|
+
if isinstance(data, torch.Tensor):
|
|
207
|
+
return data.to(dtype)
|
|
208
|
+
return torch.from_numpy(data).to(dtype)
|