Read, write and convert OpenBCI signal recordings (.raw/.xml/.tag)
Project description
readmanager
Read, write and convert OBCI signal
recordings stored in the OBCI file format (.raw / .xml / .tag).
Installation
pip install readmanager # core (numpy only)
pip install readmanager[mne] # + MNE-Python conversion
pip install readmanager[balance] # + Wii Balance Board analysis (scipy)
pip install readmanager[all] # everything
Requires Python 3.10+.
Quick start
from obci_readmanager.signal_processing.read_manager import ReadManager
# Open a recording (three files: .raw, .xml, .tag)
mgr = ReadManager(
"recording.obci.xml",
"recording.obci.raw",
"recording.obci.tag",
)
# Access signal parameters
print(mgr.get_param("sampling_frequency"))
print(mgr.get_param("channels_names"))
# Get all samples as a numpy array (channels x samples)
samples = mgr.get_samples()
# Get samples for a single channel
fp1 = mgr.get_channel_samples("Fp1")
# Get samples in microvolts (applies gain and offset calibration)
uv = mgr.get_microvolt_samples()
# Iterate over tags (event markers)
for tag in mgr.iter_tags():
print(tag["name"], tag["start_timestamp"])
Cropping for memory-efficient partial reads
For long recordings that don't fit comfortably in RAM, crop() restricts
all subsequent reads to a half-open [from, to) window. No I/O happens
at crop time — bounds are stored and every later read honours them, so
discarded samples are never loaded from disk. The semantics mirror
mne.io.Raw.crop:
mgr = ReadManager("recording.obci.xml", "recording.obci.raw", "recording.obci.tag")
# Keep seconds 30..90 of the recording
mgr.crop(30.0, 90.0)
mgr.duration # 60.0
mgr.get_samples() # only the 60 s window — no full-file load
mgr.iter_samples() # streamed in 1024-sample chunks via the proxy
mgr.save_to_file(out_dir, "trimmed") # writes only the cropped signal
After crop, an explicit offset passed to get_samples is interpreted
inside the cropped view (offset 0 == the crop start), again matching
MNE. Tag timestamps are also shifted to the cropped timeline: tags
outside the window are dropped and surviving onsets are measured from
the new t=0, so mgr.get_mne_raw() after a narrow crop produces an MNE
Raw with annotations correctly aligned to its data.
Crop bounds are clamped against the per-channel sample count derived
from the data file size, not the .xml sampleCount field (which
conflates total values vs. per-channel count on some recordings).
Smart tags
Smart tags extract signal segments aligned to event markers:
from obci_readmanager.signal_processing.smart_tags_manager import SmartTagsManager
from obci_readmanager.signal_processing.tags.smart_tag_definition import SmartTagDurationDefinition
# Define: 1 second of signal after each "stimulus" tag
tag_def = SmartTagDurationDefinition(
start_tag_name="stimulus",
start_offset=0.0,
end_offset=0.0,
duration=1.0,
)
smart_mgr = SmartTagsManager(
tag_def,
"recording.obci.xml",
"recording.obci.raw",
"recording.obci.tag",
)
for smart_tag in smart_mgr.iter_smart_tags():
data = smart_tag.get_samples() # channels x samples for this epoch
print(data.shape)
MNE-Python conversion
# ReadManager -> MNE Raw (eager — loads everything into RAM)
raw_mne = mgr.get_mne_raw()
# MNE Raw -> ReadManager
mgr2 = ReadManager.from_mne(raw_mne)
# Smart tags -> MNE Epochs
epochs = smart_mgr.get_mne_epochs()
Lazy MNE bridge (preload=False)
For multi-hour recordings, get_mne_raw(preload=False) returns a real
mne.io.BaseRaw subclass instead of a fully materialised RawArray.
Sample reads are deferred until MNE actually requests a span — slicing,
get_data(start, stop), filtering, plotting — matching the
mne.io.read_raw_edf(path, preload=False) idiom:
raw_lazy = mgr.get_mne_raw(preload=False)
chunk = raw_lazy.get_data(start=0, stop=1000) # only 1000 samples hit RAM
raw_lazy.crop(tmin=10.0, tmax=20.0) # MNE shifts annotations
raw_lazy.load_data() # opt-in preload at any time
Channel parity with the eager path is preserved: the synthetic
OBCI_STIM channel is included and populated lazily from a precomputed
event index, so raw_lazy.ch_names == raw_mne.ch_names. Annotations
are absolute (relative to recording start), so MNE's own
Raw.crop/Raw.set_annotations machinery handles tag-time shifting.
Channel type heuristic
When converting to MNE, get_mne_raw() and friends need to assign an
MNE channel type ('eeg', 'eog', 'emg', 'ecg', 'bio', 'stim',
'misc') to every channel. If you pass an explicit channel_types list,
it's used as-is; otherwise readmanager applies a name-based heuristic
via chtype_heuristic(name) from the mne_utils submodule.
The heuristic recognises, in order of priority:
| Rule | Example inputs | Returns |
|---|---|---|
Substring eog |
EOG Left Horiz, VEOG, EOG Fp1-M2 |
eog |
Substring emg |
EMG Chin1, EMG Ant Tibia-0 |
emg |
Substring ecg / ekg |
ECG ECGI, EKG_lead1 |
ecg |
Substring resp / sao2 / spo2 |
Resp Thermistor, SaO2 SaO2 |
bio |
Substring stim / trig / marker / status / sync or prefix sti |
STIM, Trigger, STI 014, STATUS |
stim |
| Tokenised 10-05 position lookup | Fp1, C3, EEG F3-CLE, Fp1-M2, F3-CAR |
eeg |
| Fall-through | anything else (Channel_42, Aux1, Photo) |
misc |
The 10-05 position check tokenises the channel name on any
non-alphanumeric boundary and matches each token against MNE's
standard_1005 montage position set. This catches prefixed/suffixed
variants common in EDF polysomnography recordings (EEG F3-CLE,
EEG Fp1-M2) without false-positives on short position names
embedded in unrelated words (e.g. audio1, Data1, misc3).
Non-EEG substring rules take priority over the position lookup, so
EOG Fp1-M2 (an EOG reference channel using Fp1/M2 as the reference
montage) is correctly typed as eog rather than eeg.
If your recording has channels the heuristic doesn't classify to your taste, pass an explicit list:
raw = mgr.get_mne_raw(channel_types=["eeg"] * 32 + ["ecg", "stim"])
See the chtype_heuristic docstring for the full decision order and
edge cases, and test/signal_processing/test_chtype_heuristic.py for
the complete parametrised test matrix.
OBCI file format
An OBCI recording consists of three files:
| File | Content |
|---|---|
.obci.raw |
Binary signal data (channels interleaved, float64 by default) |
.obci.xml |
Recording metadata: channel names, sampling frequency, gains, offsets |
.obci.tag |
Event markers in XML format (name, timestamp, duration, description) |
Part of the OBCI ecosystem
readmanager is used by OBCI for
signal file replay and post-recording correction. Related packages:
- obci-server — EEG acquisition server (depends on readmanager)
- obci-desktop — desktop launcher and LSL streaming
- obci-psychopy — PsychoPy tag integration
- SVAROG4 — Java signal viewer/recorder
License
GNU General Public License v3 or later (GPLv3+).
Originally developed by BrainTech and the Faculty of Physics, University of Warsaw.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file readmanager-1.5.0.tar.gz.
File metadata
- Download URL: readmanager-1.5.0.tar.gz
- Upload date:
- Size: 48.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.0
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1034e0e4c19fd5af4466692099d7514fa669c9d3916ca0763ddb55f13b93d992
|
|
| MD5 |
ad93cdb443515be7dd7831c01055ed2a
|
|
| BLAKE2b-256 |
0b97452d8a037286ffb4d0ed5e87eacc2bee1d3b050432cf0f67c9be273a94c6
|
File details
Details for the file readmanager-1.5.0-py3-none-any.whl.
File metadata
- Download URL: readmanager-1.5.0-py3-none-any.whl
- Upload date:
- Size: 67.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.0
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a2d8acf49c96236874f838eaf78053f7cc9deffd84907a93a052837990749807
|
|
| MD5 |
5a2301c9764b6fd5de087fead1c45944
|
|
| BLAKE2b-256 |
28c8d669e1008e9b80b32e93842dbbe52ca7dbe603488c909e31dc4a285d8fed
|