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.2.tar.gz (61.7 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.2-py3-none-any.whl (80.9 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: readmanager-1.6.2.tar.gz
  • Upload date:
  • Size: 61.7 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.2.tar.gz
Algorithm Hash digest
SHA256 e308dbd412121b07e452a9488d3416000a4bfde1694c75c4a9fc412c8b781640
MD5 c6fc4d416c6b0538039ffa3e510e69e2
BLAKE2b-256 b6d87b6221ba808b07dd91d880b7dc712f0eb4845b4adccdd344e9983ebfe214

See more details on using hashes here.

File details

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

File metadata

  • Download URL: readmanager-1.6.2-py3-none-any.whl
  • Upload date:
  • Size: 80.9 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.2-py3-none-any.whl
Algorithm Hash digest
SHA256 e65ef4f9c13e8fbb902f1ef19bd8f89265d8ef9c34425506780ca0b712ab9d22
MD5 64b35b8e13f1d7fbc754e7e36f2c731b
BLAKE2b-256 57a2c675682b45135bdad3dc451ffd3833d270d195b6b38f0e1b0fde0655d8da

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