Initialize NeuroAnalyzer
using NeuroAnalyzer
eeg = import_edf("files/eeg/eeg.edf")using NeuroAnalyzer
eeg = import_edf("files/eeg/eeg.edf")This tutorial shows the basic operations on NEURO objects: copying, notes, channels, signal segments and epochs, and creating objects from scratch (for markers, see Markers). The structure of the object is described in NEURO objects.
Other editing operations are described in separate tutorials: bad channels and epochs, virtual channels, channel formulas and resampling.
Mutators and copies
Most editing functions have two variants: delete_channel() returns a modified copy and leaves the input unchanged, while delete_channel!() (a mutator, with !) modifies the object in place:
# new object, eeg is unchanged
eeg_new = delete_channel(eeg; ch = "ECG")
# eeg is modified
delete_channel!(eeg; ch = "ECG")Mutators save memory, which matters for long or high-density recordings.
To copy an object, use deepcopy():
eeg_copy = deepcopy(eeg)Tip: eeg_copy = eeg does not copy the object: both names refer to the same object, so changes made through one of them (e.g. with a mutator) are visible through the other.
An object is removed from memory by removing all references to it, e.g. eeg_copy = nothing; the memory is freed by the garbage collector.
Processing history
Each operation is recorded, with its parameters, in the processing history:
e = keep_channel(eeg; ch = "eeg")
e = epoch(e; ep_len = 10)foreach(println, history(e))keep_channel(obj; ch=["Fp1", "Fp2", "F3", "F4", "C3", "C4", "P3", "P4", "O1", "O2", "F7", "F8", "T3", "T4", "T5", "T6", "Fz", "Cz", "Pz"])
epoch(obj; marker=, offset=0, ep_len=2560)
Notes
A text note can be stored with the recording (it is saved with the object):
add_note!(eeg; note = "eyes closed, resting state")
println(view_note(eeg))
delete_note!(eeg)
println("note after deleting: \"", view_note(eeg), "\"")eyes closed, resting state
note after deleting: ""
Properties
println("data type: ", datatype(eeg))
println("sampling rate: ", sr(eeg), " Hz")
println("channels: ", nchannels(eeg))
println("epochs: ", nepochs(eeg))
println("signal length: ", signal_len(eeg), " samples (",
signal_duration(eeg), " s)")
println("epoch length: ", epoch_len(eeg), " samples (",
epoch_duration(eeg), " s)")
println("data size: ", size(eeg))data type: eeg
sampling rate: 256 Hz
channels: 24
epochs: 1
signal length: 308480 samples (1205.0 s)
epoch length: 308480 samples (1205.0 s)
data size: (24, 308480, 1)
header(eeg) and info(eeg) print a summary of the object, and NeuroAnalyzer.describe(eeg) basic statistics of each channel.
Selecting channels
Channels are selected by label, vector of labels, type, "all", "bad" or regular expression (see the Glossary). get_channel() returns the channel numbers of a selection, or the labels of channels of a given type:
println(labels(eeg))
println(get_channel(eeg; ch = ["Fp1", "Fz"]))
println(get_channel(eeg; ch = r"^F"))
println(get_channel(eeg; type = "eog"))
println(get_channel(eeg; ch = "eeg", exclude = ["A1", "A2"]))["Fp1", "Fp2", "F3", "F4", "C3", "C4", "P3", "P4", "O1", "O2", "F7", "F8", "T3", "T4", "T5", "T6", "A1", "A2", "Fz", "Cz", "Pz", "EOG1", "EOG2", "ECG"]
[1, 19]
[1, 2, 3, 4, 11, 12, 19]
["EOG1", "EOG2"]
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 19, 20, 21]
Channel type and label
println("type of A1: ", channel_type(eeg; ch = "A1"))
e = set_channel_type(eeg; ch = "A1", type = "eeg")
println("type of A1: ", channel_type(e; ch = "A1"))type of A1: ref
type of A1: eeg
e = rename_channel(eeg; ch = "T3", name = "T7")
println(labels(e)[11:16])["F7", "F8", "T7", "T4", "T5", "T6"]
edit_channel() changes either field (field = :label or field = :channel_type), and add_label() replaces all labels at once:
e = edit_channel(eeg; ch = "T4", field = :label, value = "T8")
e = add_label(eeg; clabels = ["ch$idx" for idx in 1:nchannels(eeg)])Renaming a channel also renames its entry in the channel locations.
Deleting and keeping channels
e = delete_channel(eeg; ch = ["EOG1", "EOG2", "ECG"])
println(labels(e))["Fp1", "Fp2", "F3", "F4", "C3", "C4", "P3", "P4", "O1", "O2", "F7", "F8", "T3", "T4", "T5", "T6", "A1", "A2", "Fz", "Cz", "Pz"]
e = keep_channel(eeg; ch = ["Fp1", "Fp2", "Fz", "Cz", "Pz"])
println(labels(e))["Fp1", "Fp2", "Fz", "Cz", "Pz"]
All per-channel information (header fields, locations, bad channel flags, MEG sensor geometry, …) is updated. NIRS signal channels are deleted together with their optodes, using delete_optode():
nirs = import_nirs("files/nirs/fnirs-test-nirs.nirs")
println("optodes: ", optode_labels(nirs), ", channels: ",
labels(nirs)[1:4])
nirs = delete_optode(nirs; opt = 3)
println("optodes: ", optode_labels(nirs), ", channels: ",
labels(nirs)[1:2])optodes: ["S1", "D1", "D2"], channels: ["S1_D1 690", "S1_D2 690", "S1_D1 830", "S1_D2 830"]
optodes: ["S1", "D1"], channels: ["S1_D1 690", "S1_D1 830"]
Extracting, replacing and adding channels
extract_channel() returns the data of one channel (channel × samples × epochs), extract_data() the data of several channels and epochs, optionally with the time points:
s = extract_channel(eeg; ch = "Cz")
println("extract_channel: ", size(s))
d = extract_data(eeg; ch = ["Fp1", "Fp2"])
println("extract_data: ", size(d))
d, t = extract_data(eeg; ch = "Fp1", time = true)
println("time points: ", t[1:3], " …")extract_channel: (1, 308480, 1)
extract_data: (2, 308480, 1)
time points: [0.0, 0.00390625, 0.0078125] …
replace_channel() replaces the data of a channel, e.g. with the data of another channel or a processed version:
e = replace_channel(eeg; ch = "Fp1", s = s)println("Fp1 == Cz: ", extract_channel(e; ch = "Fp1") == s)Fp1 == Cz: true
add_channel() appends new channels (data: channels × samples × epochs), with their labels, types and units, e.g. the difference of two channels:
d = extract_channel(eeg; ch = "Fp1") .- extract_channel(eeg; ch = "F7")
e = add_channel(eeg; data = d, label = "Fp1-F7", type = "eeg", unit = "μV")
println(labels(e)[end-2:end])["EOG2", "ECG", "Fp1-F7"]
Channels computed from formulas are created more conveniently with virtual channels.
Adding a signal to channels
add_signal() adds a signal vector (of the length of one epoch) to the selected channels in all epochs, e.g. to simulate an artifact or a known signal and test whether a processing pipeline detects or removes it:
# 10 Hz sine wave of 10 μV added to O1 and O2
tt = eeg.epoch_time
e = add_signal(eeg; ch = ["O1", "O2"], s = 10 .* sin.(2π * 10 .* tt))
d = extract_channel(e; ch = "O1") .- extract_channel(eeg; ch = "O1")
println("added to O1: ", round.(extrema(d); digits = 2), " μV")added to O1: (-10.0, 10.0) μV
Trimming and cropping
trim() removes a segment (start and end in seconds); with keep = true, only the segment is kept. crop() is the same as trim(…; keep = true):
dur(obj) = string(signal_duration(obj), " s")
println("original: ", dur(eeg))
println("trim (10, 20): ", dur(trim(eeg; seg = (10, 20))))
println("trim (10, 20), keep: ",
dur(trim(eeg; seg = (10, 20), keep = true)))
println("crop (10, 20): ", dur(crop(eeg; seg = (10, 20))))original: 1205.0 s
trim (10, 20): 1194.99609375 s
trim (10, 20), keep: 10.00390625 s
crop (10, 20): 10.00390625 s
trim() and crop() work on continuous objects only; in epoched objects, whole epochs are removed with delete_epoch() (see below).
Joining
join!() appends the signal of another object (with the same channels and sampling rate) to the first one:
part1 = crop(eeg; seg = (0, 10))
part2 = crop(eeg; seg = (60, 70))
join!(part1, part2)
println("joined: ", dur(part1))joined: 20.0078125 s
Reflecting
reflect() extends the signal at both ends with its mirror image (1234 → 432112344321), and chop!() removes the extension. This reduces edge artifacts of filters and other operations applied to short signals:
r = reflect(eeg; n = sr(eeg))
println("reflected: ", dur(r))
chop!(r; n = sr(eeg))
println("chopped: ", dur(r), ", identical to the original: ",
r.data == eeg.data)reflected: 1207.0 s
chopped: 1205.0 s, identical to the original: true
Epoching
Epoching divides a continuous recording into segments (epochs); the data change from channels × samples (1 epoch) to channels × samples × epochs. Epochs can have a fixed length:
e10 = epoch(eeg; ep_len = 10)
println("epochs: ", nepochs(e10), ", epoch length: ",
epoch_duration(e10), " s, data size: ", size(e10))epochs: 120, epoch length: 10.0 s, data size: (24, 2560, 120)
or be time-locked to events (markers): marker selects the marker value, offset the time before the event and ep_len the epoch length (in seconds). A face perception experiment, epochs from 200 ms before to 800 ms after the s71 stimuli:
faces = import_recording("files/eeg/s1_faces.vhdr")
ep71 = epoch(faces; marker = "s71", offset = 0.2, ep_len = 1.0)println("epochs: ", nepochs(ep71))
println("epoch time: ", ep71.epoch_time[1], " … ",
ep71.epoch_time[end], " s")epochs: 100
epoch time: -0.2 … 0.798046875 s
The epoch time axis starts at \(-0.2\) s, so that 0 is the stimulus onset.
Tip: Filtering, ICA and other operations sensitive to the signal length should be applied to the continuous signal, before epoching.
The shorter the epoch, the coarser the frequency resolution of its spectrum (\(\Delta f = 1 / T\), e.g. 0.5 Hz for 2-second and 0.1 Hz for 10-second epochs).
Epoch time axis and sub-epochs
epoch_ts() shifts the epoch time axis (e.g. to set the event onset to 0 for fixed-length epochs) and subepoch() keeps a part of each epoch:
e = epoch_ts(e10; ts = -2)
println("shifted: ", e.epoch_time[1], " … ", e.epoch_time[end], " s")
e = subepoch(e10; ep_start = 2, ep_end = 4)
println("sub-epoch: ", e.epoch_time[1], " … ", e.epoch_time[end],
" s, ", epoch_len(e), " samples")shifted: -2.0 … 7.99609375 s
sub-epoch: 2.0 … 4.0 s, 513 samples
Selecting epochs
println("delete 1: ", nepochs(delete_epoch(e10; ep = 1)))
println("delete 8:10: ", nepochs(delete_epoch(e10; ep = 8:10)))
println("keep [1, 3, 5]: ", nepochs(keep_epoch(e10; ep = [1, 3, 5])))
e1 = extract_epoch(e10; ep = 1)
println("extract 1: ", nepochs(e1), " epoch, ", dur(e1))delete 1: 119
delete 8:10: 117
keep [1, 3, 5]: 3
extract 1: 1 epoch, 10.0 s
Tip: Non-contiguous ranges are written as [1:5; 9; 10].
Averaging epochs
average_epochs() averages EEG/MEG epochs into an event-related potential (field); see ERP:
erp = average_epochs(ep71)
println("data type: ", datatype(erp), ", epochs: ", nepochs(erp),
" (average + ", nepochs(ep71), " epochs)")data type: erp, epochs: 101 (average + 100 epochs)
Markers (events) are stored in obj.markers; viewing, adding, editing and deleting markers, creating markers from an event channel and their timing are described in Markers.
A NEURO object can be created from data generated or imported by other means. create_object() creates an empty object of the given data type, and create_data!() adds the data (channels × samples × epochs), the sampling rate and the type of the channels, with automatic labels:
fs = 256
t = collect(0:1/fs:10-1/fs)
# 2 channels × samples × 1 epoch
data = zeros(2, length(t), 1)
data[1, :, 1] = sin.(2π * 10 .* t)
data[2, :, 1] = cos.(2π * 6 .* t)
obj = create_object(; data_type = "eeg")
create_data!(obj; data = data, fs = fs, type = "eeg")
println(labels(obj), ", ", sr(obj), " Hz, ", signal_duration(obj), " s")["ch-1", "ch-2"], 256 Hz, 10.0 s
Alternatively, add_channel!() adds channels with chosen labels, types and units to an empty object; the time axes are then created with create_time!():
obj = create_object(; data_type = "eeg")
obj.header.recording[:sampling_rate] = fs
add_channel!(obj;
data = reshape(sin.(2π * 10 .* t), 1, :, 1),
label = "alpha",
type = "eeg",
unit = "μV")
create_time!(obj; fs = fs)
println(labels(obj), ", ", sr(obj), " Hz, ", signal_duration(obj), " s")["alpha"], 256 Hz, 10.0 s