mne-tools / mne-tools/mne-python

Importing Neuroscan Evoked and Epochs-like files

Open
#12,367 9 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

ENH
Dominant language
Python
Stars
3.5k
Forks
1.6k
Avg merge
1d 6h
Merged PRs (30d)
100

Description

Describe the new feature or enhancement

Adding support for importing .avg and .eeg neuroscan files as evoked and epochs objects.

Describe your proposed implementation

I have written simple scripts using struct to read the raw byte data. I could add these scripts to mne in the form of a mne.read_neuroscan_epochs() function. Here is an example:

def eeg_to_ascii(
        file_name, chanlist='all', triallist='all', typerange='all',
        accepttype='all', rtrange='all', responsetype='all',
        data_format='auto'):
    """This function reads the data from a binary EEG file, extracts and scales the data, and returns it in ASCII format in volts.


    # Check if file ends with .eeg
    if not file_name.endswith('.eeg'):
        raise ValueError("File must be a binary EEG file (.eeg).")

    if not os.path.isfile(file_name):
        raise ValueError(f"File {file_name} not found.")

    with open(file_name, 'rb') as f:
        try:
            # Read general part of the ERP header and set variables
            f.read(20)  # skip revision number
            f.read(342)  # skip the first 362 bytes

            nsweeps = struct.unpack('<H', f.read(2))[0]  # number of sweeps
            f.read(4)  # skip 4 bytes
            # number of points per waveform
            pnts = struct.unpack('<H', f.read(2))[0]
            chan = struct.unpack('<H', f.read(2))[0]  # number of channels
            f.read(4)  # skip 4 bytes
            rate = struct.unpack('<H', f.read(2))[0]  # sample rate (Hz)
            f.read(127)  # skip 127 bytes
            xmin = struct.unpack('<f', f.read(4))[0]  # in s
            xmax = struct.unpack('<f', f.read(4))[0]  # in s
            f.read(387)  # skip 387 bytes

            # Read electrode configuration
            chan_names = []
            baselines = []
            sensitivities = []
            calibs = []
            factors = []
            for elec in range(chan):
                chan_name = f.read(10).decode('ascii').strip('\x00')
                chan_names.append(chan_name)
                f.read(37)  # skip 37 bytes
                baseline = struct.unpack('<H', f.read(2))[0]
                baselines.append(baseline)
                f.read(10)  # skip 10 bytes
                sensitivity = struct.unpack('<f', f.read(4))[0]
                sensitivities.append(sensitivity)
                f.read(8)  # skip 8 bytes
                calib = struct.unpack('<f', f.read(4))[0]
                calibs.append(calib)
                factor = calib * sensitivity / 204.8
                factors.append(factor)

        except struct.error:
            raise ValueError(
                "Error reading binary file. File may be corrupted or not in the expected format.")
        except Exception as e:
            raise ValueError(f"Error reading file: {e}")

    # Read and process epoch datapoints data
    data = np.empty((nsweeps, len(chan_names), pnts), dtype=float)
    sweep_headers = []

    # Constants for the sweep header size in bytes and data point size in bytes
    SWEEP_HEAD_SIZE = 13
    DATA_POINT_SIZE = 4

    with open(file_name, 'rb') as f:
        # Ensure the file pointer is at the beginning of the EEG data
        f.seek((900 + chan * 75))

        for sweep in range(nsweeps):
            # Read the sweep header
            try:
                # f.read(SWEEP_HEAD_SIZE)
                accept = struct.unpack('<c', f.read(1))[0]
                ttype = struct.unpack('<h', f.read(2))[0]
                correct = struct.unpack('<h', f.read(2))[0]
                rt = struct.unpack('<f', f.read(4))[0]
                response = struct.unpack('<h', f.read(2))[0]
                # reserved  struct.unpack('<h', f.read(2))[0]
                f.read(2)  # skip 2 bytes
                sweep_headers.append(
                    (accept, ttype, correct, rt, response, sweep))
            except struct.error:
                raise ValueError(
                    "Error reading sweep header. File may be corrupted or not in the expected format.")
            except Exception as e:
                raise ValueError(f"Error reading sweep header: {e}")

            for point in range(pnts):
                for channel in range(chan):
                    try:
                        # Read the data point as a 4-byte integer
                        value = struct.unpack('<l', f.read(DATA_POINT_SIZE))[0]

                        # Scale the data point to microvolts and store it in the data array
                        data[sweep, channel, point] = value * factors[channel]
                    except struct.error:
                        raise ValueError(
                            "Error reading data points. File may be corrupted or not in the expected format.")
                    except Exception as e:
                        raise ValueError(f"Error reading data points: {e}")

    # Convert data from microvolts to volts
    data = data * 1e-6
    # Return relevant data in ASCII format
    return data, chan_names, rate, xmin, sweep_headers
Describe possible alternatives

I am also working on writing some C++ code to do this as well that mne could make use of.

Ultimately, it may also be simpler to write a separate library myself and just create EpochsArrays from scratch.

Additional context

No response

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

The payload names no repository files, tests, or entry points. Start by reviewing the proposed Python struct-based reader and the .avg/.eeg format details, then determine how the data should map to evoked and epochs objects. Done means MNE can import both file types into the requested objects with appropriate data and metadata.

Written by the indexing model from the issue text.

Assessment

Tech stack
cpp, python
Domain
data
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
30/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.