Skip to content

Commit 04cec23

Browse files
committed
feat: add scale param, recording metadata, and save_recording() helper
1 parent 05d565c commit 04cec23

5 files changed

Lines changed: 112 additions & 8 deletions

File tree

README.md

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ try:
5757
with EEGConnector() as device:
5858
info = device.get_device_data()
5959
print(f"Połączono z: {info.name}")
60+
print(f"Kanały: {info.channel_names}")
6061
6162
# Akwizycja 5 sekund danych
6263
eeg_data = device.get_output(duration=5.0)
@@ -70,6 +71,59 @@ finally:
7071
close()
7172
```
7273

74+
### BrainAccess — skalowanie jednostek
75+
76+
SDK BrainAccess zwraca surowe wartości ADC. Parametr `scale` pozwala przeliczać je na µV bezpośrednio przy streamowaniu:
77+
78+
```python
79+
from bridge.eeg.brainaccess import BrainaccessDevice
80+
81+
device = BrainaccessDevice(scale=1 / 1000) # ADC → µV
82+
device.connect()
83+
for chunk in device.stream():
84+
# chunk jest już w µV
85+
...
86+
device.disconnect()
87+
```
88+
89+
### Nagrywanie i odtwarzanie
90+
91+
**Nagrywanie live** z urządzenia:
92+
93+
```python
94+
from bridge.eeg.recorder import EEGRecorder
95+
from bridge.eeg.brainaccess import BrainaccessDevice
96+
97+
device = BrainaccessDevice(scale=1 / 1000)
98+
with EEGRecorder(device, filename="session.npz", sfreq=250, ch_names=["C3", "C4", "Cz"]) as rec:
99+
for chunk in rec.stream():
100+
pass # dane są buforowane i zapisywane automatycznie przy wyjściu
101+
```
102+
103+
**Zapis gotowych danych** (gdy pętla streamowania jest zarządzana ręcznie):
104+
105+
```python
106+
from bridge.eeg.recorder import save_recording
107+
import numpy as np
108+
109+
data: np.ndarray # (n_channels, n_samples)
110+
save_recording(data, path="session.npz", sfreq=250, ch_names=["C3", "C4", "Cz"])
111+
```
112+
113+
**Odtwarzanie** nagrania zamiast prawdziwego urządzenia:
114+
115+
```python
116+
from bridge.eeg.file.device import FileDevice
117+
118+
device = FileDevice("session.npz")
119+
device.connect()
120+
for chunk in device.stream():
121+
... # identyczne API jak z live urządzeniem
122+
device.disconnect()
123+
```
124+
125+
Pliki `.npz` zapisane przez `EEGRecorder` i `save_recording` są w pełni wymienne z `FileDevice`.
126+
73127
## Rozwój Projektu
74128
75129
### Konfiguracja Środowiska

bridge/eeg/brainaccess/device.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,15 @@
2626

2727

2828
class BrainaccessDevice(EEGDevice):
29-
def __init__(self, logger: Logger | None = None) -> None:
29+
def __init__(self, scale: float = 1.0, logger: Logger | None = None) -> None:
3030
self._eeg: EEG = acquisition.EEG()
3131
self._manager: EEGManager | None = None
3232
self._cap: dict[int, str] | None = None
3333
self._mac_address: str | None = None
3434
self._device_name: str | None = None
3535
self._stream_queue: Queue[EEGArray] = Queue()
3636
self._is_streaming: bool = False
37+
self._scale: float = scale
3738

3839
super().__init__(logger or getLogger(__name__))
3940

@@ -173,7 +174,7 @@ def stream(self) -> Generator[EEGArray, None, None]:
173174
while self._is_streaming:
174175
try:
175176
chunk = self._stream_queue.get(timeout=1.0)
176-
yield chunk
177+
yield chunk * self._scale if self._scale != 1.0 else chunk
177178
except Empty:
178179
continue
179180
finally:
@@ -224,4 +225,5 @@ def get_device_data(self) -> DeviceData:
224225
manufacturer=BRAINACCESS_MANUFACTURER,
225226
electrodes_num=len(self._cap) if self._cap else None,
226227
sample_rate=self._manager.get_sample_frequency() if self._manager else None,
228+
channel_names=tuple(self._cap.values()) if self._cap else None,
227229
)

bridge/eeg/core/device_data.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ class DeviceData:
1010
manufacturer: str | None = None
1111
electrodes_num: int | None = None
1212
sample_rate: int | None = None
13+
channel_names: tuple[str, ...] | None = None
1314

1415

1516
@dataclass(frozen=True, slots=True, kw_only=True)

bridge/eeg/file/device.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,10 @@ def __init__(
1616
) -> None:
1717
super().__init__(logger or getLogger(__name__))
1818
self._path: Final[Path] = Path(file_path)
19-
self._sfreq: Final[float] = sfreq
19+
self._sfreq: float = sfreq
2020
self._chunk_size: Final[int] = chunk_size
2121
self._data: np.ndarray[Any, Any] | None = None
22+
self._ch_names: tuple[str, ...] | None = None
2223
self._is_connected: bool = False
2324

2425
def connect(self) -> None:
@@ -27,6 +28,10 @@ def connect(self) -> None:
2728

2829
with np.load(self._path) as loader:
2930
self._data = loader["data"]
31+
if "sfreq" in loader:
32+
self._sfreq = float(loader["sfreq"])
33+
if "ch_names" in loader:
34+
self._ch_names = tuple(str(n) for n in loader["ch_names"])
3035

3136
self._is_connected = True
3237
if self._data is None or self._data.size == 0:
@@ -58,4 +63,11 @@ def stream(self) -> Generator[EEGArray, None, None]:
5863
yield self._data[:, start : start + self._chunk_size].astype(np.float64)
5964

6065
def get_device_data(self) -> DeviceData:
61-
return DeviceData(name=self._path.name, manufacturer="BinarySim", sample_rate=int(self._sfreq))
66+
n_ch = self._data.shape[0] if self._data is not None else None
67+
return DeviceData(
68+
name=self._path.name,
69+
manufacturer="BinarySim",
70+
sample_rate=int(self._sfreq),
71+
electrodes_num=n_ch,
72+
channel_names=self._ch_names,
73+
)

bridge/eeg/recorder.py

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,20 +9,50 @@
99
from .core.device_data import RecordingFrame
1010

1111

12+
def save_recording(
13+
data: np.ndarray,
14+
path: str | Path,
15+
sfreq: float | None = None,
16+
ch_names: list[str] | None = None,
17+
logger: Logger | None = None,
18+
) -> None:
19+
_log = logger or getLogger(__name__)
20+
dest = Path(path)
21+
dest.parent.mkdir(parents=True, exist_ok=True)
22+
23+
arrays: dict[str, Any] = {
24+
"timestamps": np.array([time.time()]),
25+
"data": data,
26+
}
27+
if sfreq is not None:
28+
arrays["sfreq"] = np.float64(sfreq)
29+
if ch_names is not None:
30+
arrays["ch_names"] = np.array(ch_names, dtype=str)
31+
32+
np.savez_compressed(dest, **arrays)
33+
_log.info("Saved recording to %s", dest)
34+
35+
1236
class EEGRecorder:
1337
"""Rejestrator EEG wykorzystujący wysokowydajny format binarny NumPy."""
1438

1539
def __init__(
1640
self,
1741
device: EEGDevice,
1842
filename: str,
43+
output_dir: str | Path = "recordings",
44+
sfreq: float | None = None,
45+
ch_names: list[str] | None = None,
1946
logger: Logger | None = None,
2047
autosave: bool = True,
2148
connect_device: bool = True,
2249
) -> None:
2350
self._logger: Final[Logger] = logger or getLogger(__name__)
2451
self._device: Final[EEGDevice] = device
2552
self._filename: Final[str] = filename
53+
self._output_dir: Final[Path] = Path(output_dir)
54+
self._sfreq: float | None = sfreq
55+
self._ch_names: list[str] | None = ch_names
2656
self._autosave: Final[bool] = autosave
2757
self._connect_device: Final[bool] = connect_device
2858
self._frames: list[RecordingFrame] = []
@@ -51,14 +81,19 @@ def save(self) -> None:
5181
return
5282

5383
try:
54-
output_dir: Final[Path] = Path("recordings")
55-
output_dir.mkdir(exist_ok=True)
56-
file_path: Final[Path] = output_dir / self._filename
84+
self._output_dir.mkdir(parents=True, exist_ok=True)
85+
file_path: Final[Path] = self._output_dir / self._filename
5786

5887
timestamps: Final[np.ndarray[Any, Any]] = np.array([f.timestamp for f in self._frames])
5988
data_blocks: Final[np.ndarray[Any, Any]] = np.concatenate([f.data for f in self._frames], axis=1)
6089

61-
np.savez_compressed(file_path, timestamps=timestamps, data=data_blocks)
90+
arrays: dict[str, Any] = {"timestamps": timestamps, "data": data_blocks}
91+
if self._sfreq is not None:
92+
arrays["sfreq"] = np.float64(self._sfreq)
93+
if self._ch_names is not None:
94+
arrays["ch_names"] = np.array(self._ch_names, dtype=str)
95+
96+
np.savez_compressed(file_path, **arrays)
6297

6398
self._logger.info("Saved session to binary file: %s", file_path)
6499

0 commit comments

Comments
 (0)