annotating bad segments, then cropping signal, annotations don't get shifted?

MNE 1.9.0

Hi MNE Team,

I manually annotated bad segments in patient EEG data.
Next, I cropped the early and late parts of the resting state data, as they were most noisy.
I assumed that the annotations in the remaining time window would be shifted accordingly. Luckily, my colleague compared the pre/post cropping annotations and noted that they didn`t get shifted. There is probably a good reason for this behaviour, which I am curious to hear about.
I suggest a warning that cautions the user that annotations are not updated automatically when cropping signals containing annotations. I think that can be easily implemented and might prevent users from potentially rejecting good segments and keeping bad segments.

Happy to hear your thoughts.

Cheers,

Carina

1 Like

Hello, simply speaking, annotation onset times are stored as the time of day. They’re not based on time relative to recording onset.

So if a recording starts at 09:00.00 and an annotation is added for a segment 10 mins after recording start, it will be tied to 09:10.00.

If you crop, say, the first five minutes, it only “takes” 5 min (instead of previously 10) to “reach” the annotation.

This is intentional behavior, though.

A fix I could imagine for your use case is opti ally adjusting the recording start time if one crops a segment from the beginning of the recording.

Richard

Then again, perhaps we are already shifting onset when cropping, and this is exactly what’s causing your issue? Could you please provide a reproducible example that demonstrates the issue?

Richard

1 Like

I think annotations should get shifted accordingly when cropping. If this is not the case, please open an issue.

1 Like

Here is a quick example using MNE sample data:

import os
import mne

sample_data_folder = mne.datasets.sample.data_path()
sample_data_raw_file = os.path.join(
    sample_data_folder, "MEG", "sample", "sample_audvis_filt-0-40_raw.fif"
)
raw = mne.io.read_raw_fif(sample_data_raw_file, verbose=False)

# define bad data onsets and durations
onset = [40., 220., 260.]
duration = [5., 7., 10.]
description = ['bad', 'bad', 'bad']

bad_annot = mne.Annotations(
    onset, duration, description, orig_time=raw.info["meas_date"]
)
raw.set_annotations(bad_annot)

# that's the bad segments we want to annotate
raw.plot()

# save pre crop annotations
annotations_pre_crop = raw.annotations

# crop the data
raw_cropped = raw.copy().crop(60,250)

# let's look at post cropping annotations
annotations_post_crop = raw_cropped.annotations

# seems that the annotations do get updated somewhere
raw_cropped.plot()

# loop over annotations and print onset times
for annot in annotations_pre_crop:
    print(annot['onset'])

for annot in annotations_post_crop:
    print(annot['onset'])

# annotations do not get updated (onset times are out of cropped window)

The raw.plot() shows that the bad segments are shifted and align with where we would want them. The annotations dict however shows the pre cropping onsets. I do think that the annotations do get correctly shifted, the annotations object might just not be updated accordingly. This is mainly an issue if you share data like in my case and your colleague would like to use the annotations for further processing.

Cheers,

Carina

1 Like

Hello there!
I’ve been looking into this too and I think the critical lines for this are:

When dealing with orig_time = None, the onsets of all annots are corrected, since they are relative to first_time. This is fine.

Then they are passed to

Which also does some cropping and reintroduces first_time:

It seems like the correction for first_time is doesn’t quite work as it’s supposed to when not having orig_time set, but maybe I am wrong. I am trying to write an MWE now :slight_smile:

1 Like

I am now realizing that the example posted by @CarinaFo, does have orig_time set. So my comment might not be too related to this issue.

Here is the MWE to proof my earlier concerns wrong:

import mne
import numpy as np

  # Create mock raw data: 1 EEG channel, 150 seconds at 100 Hz
  sfreq = 100
  n_channels = 1
  n_secs = 150
  info = mne.create_info(ch_names=["EEG 001"], sfreq=sfreq)
  data = np.random.randn(n_channels, sfreq * n_secs)
  raw = mne.io.RawArray(data, info)

  # Crop to 0–120s for clarity
  raw.crop(0, 120)

  # Print key info
  print("Raw info:")
  print(f"  meas_date: {raw.info['meas_date']}")
  print(f"  annotations.orig_time: {raw.annotations.orig_time}")
  print(f"  first_time: {raw.first_time}")
  print(f"  first_samp: {raw.first_samp}")

  # Add new annotations at fixed latencies
  latencies = np.arange(20, 120, 20)  # 20, 40, ..., 100
  descriptions = [f"Event {i}" for i in range(len(latencies))]
  durations = np.full_like(latencies, 1.0, dtype=float)
  annotations = mne.Annotations(onset=latencies, duration=durations, description=descriptions)
  raw.set_annotations(annotations)

  # Zero the signal during annotated events
  for onset, duration in zip(raw.annotations.onset, raw.annotations.duration):
      start = int((onset - raw.first_time) * sfreq)
      stop = int((onset + duration - raw.first_time) * sfreq)
      raw._data[:, start:stop] = 0

  # Plot the result
  raw.plot(title="Original Raw with Annotations Zeroed")

  # Crop a copy from 10s onward
  raw_cropped = raw.copy().crop(10, 120)
  raw_cropped.plot()
  raw.plot(title="Cropped Raw with Annotations Zeroed")

  # Compare annotation onsets
  print("\nAnnotation onset comparison (original vs cropped):")
  for i in range(len(raw_cropped.annotations)):
      print(f"  original: {raw.annotations.onset[i]:.1f}, cropped: {raw_cropped.annotations.onset[i]:.1f}")

  # Time checks
  print("\nTime info:")
  print(f"  raw.first_time: {raw.first_time}, raw.times[0]: {raw.times[0]}")
  print(f"  cropped.first_time: {raw_cropped.first_time}, cropped.times[0]: {raw_cropped.times[0]}")

# %%

Nevermind my comment earlier. I misunderstood which timebase is used for annotations and this works as intended :slight_smile:

Related post: Events array altered after using concatenate_raws