Skip to main content

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"))
print(mgr.get_param("eeg_system_symbol"))   # e.g. "EEG_10_20" — cap layout, if recorded

# 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:

License

GNU General Public License v3 or later (GPLv3+).

Originally developed by BrainTech and the Faculty of Physics, University of Warsaw.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

readmanager-1.6.0.tar.gz (49.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

readmanager-1.6.0-py3-none-any.whl (68.3 kB view details)

Uploaded Python 3

File details

Details for the file readmanager-1.6.0.tar.gz.

File metadata

  • Download URL: readmanager-1.6.0.tar.gz
  • Upload date:
  • Size: 49.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.0

File hashes

Hashes for readmanager-1.6.0.tar.gz
Algorithm Hash digest
SHA256 3d482250934db9677513ced8e12a7b000a563dc85efa1142aaff9f3eb5e5f62b
MD5 e0515ac1e937b987c6d33280340b0b9b
BLAKE2b-256 411d181d56a98c9fa975ead8c3817e7200b18b35d5889d3475d56736f8a28e50

See more details on using hashes here.

File details

Details for the file readmanager-1.6.0-py3-none-any.whl.

File metadata

  • Download URL: readmanager-1.6.0-py3-none-any.whl
  • Upload date:
  • Size: 68.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.0

File hashes

Hashes for readmanager-1.6.0-py3-none-any.whl
Algorithm Hash digest
SHA256 fedc7d66b14b18c6e414d3761250a6a154173007c5287f8e68cfbf8c2b76e8d5
MD5 e5b824efef5a97110d1f91e917cdec6e
BLAKE2b-256 0f08c1cba40e9911c4870ec2a38b4f51ab2540fa76f774fa83d757991603d4ff

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page