mne-tools / mne-bids-pipeline

Automatically process entire electrophysiological datasets using MNE-Python.
https://mne.tools/mne-bids-pipeline/
BSD 3-Clause "New" or "Revised" License
134 stars 65 forks source link

Bad channel future? #835

Open larsoner opened 6 months ago

larsoner commented 6 months ago

In https://mne.tools/mne-bids-pipeline/stable/settings/preprocessing/autobads.html it says:

Warning

This functionality will soon be removed from the pipeline, and will be integrated into MNE-BIDS.

This is from 3 years ago -- @hoechenberger is it still relevant? Any objection to adding bad channel detection for EEG in the meantime (e.g., https://github.com/mne-tools/mne-python/issues/12384)?

larsoner commented 6 months ago

Quoting from @sappelhoff here:

I would also suggest comparing against pyprep's: pyprep.NoisyChannels(raw).find_all_bads() ... you can use pyprep.NoisyChannels.get_bads with as_dict=True to then get info on why channels were marked bad :-)

and @hoechenberger :

I have to say that find_all_Bads marks way too many channels as bad in my datasets. I usually only use a subset of the functions in pyprep (I can share which those are specifically in case you're interested)

I have never tried pyprep -- @hoechenberger do you think it would make sense to add an option to MNE-BIDS-Pipeline to use pyprep with a user-selected subset of "why"s (maybe suggesting users try your subset of functions as a starting point)?

hoechenberger commented 5 months ago

This is from 3 years ago -- @hoechenberger is it still relevant?

I don't think so. We should leave it in the pipeline, even though I'd prefer users to run bad channel detection on the BIDS dataset before they run the pipeline. But there are situations where you might want to work with a dataset that was provided to you that doesn't have all problematic channels marked as bad, and then this feature comes in handy. WDYT?

Any objection to adding bad channel detection for EEG in the meantime

Not at all!

I have never tried pyprep -- @hoechenberger do you think it would make sense to add an option to MNE-BIDS-Pipeline to use pyprep with a user-selected subset of "why"s (maybe suggesting users try your subset of functions as a starting point)?

Yes. Users may provide a tuple of processing steps to run. FWIW here's what I currently use before converting to BIDS; I don't use the full set of pyprep features, but iterate 3 times (cc @sappelhoff); importantly, I mark break periods first so they'll get excluded from data quality assessment:

class BadChannels(TypedDict):
    Subject: str
    Run: int
    Bads: str  # "Ch1, Ch2, …"

def run_bads_detection(subject: str, subject_run_path: Path) -> BadChannels:
    # run number is appended at the end of the input filename ater the 2nd underscore,
    # or after a dash, or after one underscore
    print(f"Processing file: {subject_run_path.name}")
    if subject_run_path.stem.count("_") == 2:
        run = int(subject_run_path.stem.split("_")[2])
    elif "-" in subject_run_path.stem:
        run = int(subject_run_path.stem.split("-")[1])
    else:
        run = int(subject_run_path.stem.split("_")[1])

    # read input data
    raw = mne.io.read_raw(subject_run_path, preload=True, verbose=False)
    assert isinstance(raw, mne.io.BaseRaw)

    raw.set_montage("easycap-M1")

    annot_breaks = mne.preprocessing.annotate_break(
        raw=raw,
        min_break_duration=10,
        t_start_after_previous=2,
        t_stop_before_next=2,
        ignore=(
            "bad",
            "edge",
            "New Segment",
        ),
    )
    raw.set_annotations(raw.annotations + annot_breaks)

    all_bads: list[str] = []

    for _ in range(3):
        nc = pyprep.NoisyChannels(raw=raw, random_state=42)
        nc.find_bad_by_deviation()
        nc.find_bad_by_correlation()
        nc.find_bad_by_ransac()

        bads = nc.get_bads()
        all_bads.extend(bads)
        all_bads = sorted(all_bads)
        raw.info["bads"] = all_bads

    print(all_bads)
    return {"Subject": subject, "Run": run, "Bads": ", ".join(sorted(all_bads))}