المحطة 9 · الدرس 3

معالجة دفعات (batch)

مهم15 دقيقةقبل هذا الدرس: سكربت لمشارك واحداطبع البطاقة
تبدأ من: sub-002_task-FaceRecognition_eeg.set (D4) · لا شيء: البيانات كما نزّلتها.تنزيل البيانات (يفتح في نافذة جديدة)
سكربت الوصول لهذه الحالة
MATLAB
% Start from a fresh MATLAB session: the batch script loads every subject itself
clear
[ALLEEG, EEG, CURRENTSET, ALLCOM] = eeglab;

الفكرة باختصار

سكربتscriptملف أوامر MATLAB ينفّذ خطوات المعالجة بالترتيب. مشاركparticipantالشخص الذي سُجّلت بياناته في التجربة. الواحد يعمل على D1. مشروعك الحقيقي فيه عشرون مشاركاً أو أكثر، بنفس الخطوات ونفس الإعدادات لكل واحد. تشغيل السكربت عشرين مرة وتغيير اسم الملف بيدك كل مرة بطيء، ويفتح باب الخطأ.

الحل حلقة (loop): قائمةmenuمجموعة أوامر تظهر عند الضغط على اسمها في أعلى النافذة. بأسماء المشاركين، ونفس الخطوات تُطبّق على كل اسم. هذا اسمه معالجة دفعاتbatch processingتطبيق نفس السكربت على ملفات عدة مشاركين تلقائياً. (batch processing).

قبل وبعد

حلقة: ثلاثة مشاركين sub-002 و sub-003 و sub-004 يدخلون واحداً بعد الآخر إلى الدالة process_subject التي تطبّق خطوات البايبلاين، ومخرجاتها في derivatives/eeglab مجلد لكل مشارك مع batch_summary.csv. سهم دائري يعود إلى المشارك التالي، وسهم أحمر من الدالة إلى logs/batch_log.txt عند الخطأ: سجّله وأكمل.

أسئلتك بعد هذا، كل جواب في قسمه:

لماذا أحتاج هذه الخطوة؟

السبب بالتفصيل، والمصطلحات التي تحتاجها

لكن الحلقة البسيطة فيها مشكلة: إذا فشل المشارك السابع لسبب ما (ملف ناقص، قناةchannelالإشارة المسجّلة من قطب واحد، وهي ما يعرضه EEGLAB. غريبة، ذاكرة)، يتوقف كل شيء، وتجد الصباح أن ستة فقط انتهوا. لهذا نلفّ كل مشارك بـ try و catch: إذا فشل، يُسجَّل الخطأ في ملف، وتكمل الحلقة للمشارك التالي.

في هذا الدرس نعالج ثلاثة مشاركين من D4 فقط (حوالي 700 ميجابايت للتنزيل). نفس السكربت يعمل على الثمانية عشر إذا نزّلتهم.

كيف أطبّقها في EEGLAB؟

10 خطوة من القوائم

أولاً: نزّل ثلاثة مشاركين من D4

  1. أنشئ مجلداً اسمه faces_project وفيه مجلد rawdata، واجعل faces_project مجلد العمل في MATLAB (cd). شغّل كل سكربتات المحطة 9 من داخل هذا المجلد. درس تنظيم الملفات يشرح بقية المجلدات.
  2. من صفحة ds002718 في OpenNeuro نزّل إلى rawdata الملفات العامة (dataset_description.json و README و task-FaceRecognition_events.json) ومجلدات sub-002 و sub-003 و sub-004 كاملة. أو من سطر أوامر النظام، إذا كان عندك AWS CLI:
MATLAB
aws s3 sync --no-sign-request s3://openneuro.org/ds002718 rawdata --exclude "sub-*" --include "sub-002/*" --include "sub-003/*" --include "sub-004/*"

ثانياً: جرّب المسار على مشارك واحد من الواجهةGUIالنوافذ والقوائم التي نستخدمها بالفأرة بدل كتابة الأوامر.

  1. افتح File ثم Import data ثم Using EEGLAB functions and plugins ثم From BIDS folder structure، واختر مجلد rawdata. فعّل استيراد حدثeventعلامة في التسجيل تحدد لحظة شيء مهم، مثل ظهور مثير. ومواقع قطبelectrodeالقطعة المعدنية التي توضع على الرأس لالتقاط الإشارة. من ملفات BIDSمعيار موحّد لتسمية ملفات البيانات العصبية وتنظيمها في مجلدات.، واجعل نوع الحدثevent typeاسم أو رقم يميّز نوع الحدث، مثل "square". من العمود trial_type. اضغط Ok. تُنشأ STUDYبنية في EEGLAB تجمع داتاست عدة مشاركين لتحليلها معاً. فيها المشاركون الثلاثة.
  2. اختر مشاركاً واحداً من قائمة Datasets، وطبّق عليه الخطوات من الواجهة كما في المحطات 3 إلى 5، مع تغيير ثلاثة إعدادات فقط: الكهرباء في بريطانيا 50 هرتز لا 60، وقنوات العين EEG061 و EEG062، وقنوات القلبheart (ECG)نبض القلب الكهربائي الذي قد يظهر في EEG كقمم منتظمة. EEG063 و EEG064 تُحذف.
  3. اكتب eegh. هذه السطور هي ما ستضعه داخل الحلقة.

ثالثاً: حوّل المسار إلى حلقة

  1. انسخ سكربت المشارك الواحد إلى ملف جديد batch_d4.m. انقل خطوات المعالجة إلى دالة في آخر الملف اسمها process_subject، تأخذ ملف المشارك والإعدادات، وتحفظ النتائج، وتعيد ملخصاً.
  2. في أعلى الملف: قائمة المشاركين، والإعدادات، وحلقة for تستدعي الدالة داخل try.
  3. في catch: اكتب اسم المشارك ورسالة الخطأ في logs/batch_log.txt، وسجّل الحالة failed في جدول الملخص.
  4. شغّل الملف. ICA على 72 قناة (70 EEG واثنتان EOG) وخمسين دقيقة تسجيل يأخذ وقتاً لكل مشارك، فاتركه يعمل. إذا كان مشاركوك كثيرين، انظر درس الحوسبة على HPC.
  5. بعد الانتهاء، افتح batch_summary.csv. كل صف مشارك: الحالة، والقنوات السيئة، والمكوّنات المحذوفة، والـ epochs الباقية.

كيف أكتبها بالكود؟

نفس الخطوات في 118 أسطر MATLAB
MATLAB
%% D4 batch pipeline: same steps for every subject, errors logged
eeglab nogui

%% Settings
projectRoot = pwd;
bidsRoot    = fullfile(projectRoot, 'rawdata');
derivRoot   = fullfile(projectRoot, 'derivatives', 'eeglab');
logRoot     = fullfile(projectRoot, 'logs');
subjects    = {'sub-002', 'sub-003', 'sub-004'};

cfg.lineFreq    = 50;                          % UK recording: 50 Hz mains
cfg.eogLabels   = {'EEG061', 'EEG062'};        % HEOG, VEOG
cfg.dropLabels  = {'EEG063', 'EEG064'};        % ECG channels
cfg.epochEvents = {'famous_new', 'unfamiliar_new', 'scrambled_new'};
cfg.epochWindow = [-1 2];
cfg.thresholdUv = 100;

if ~exist(logRoot, 'dir'), mkdir(logRoot); end
logFile = fullfile(logRoot, 'batch_log.txt');

%% Import BIDS data once: events from trial_type, locations from electrodes.tsv
[STUDY, ALLEEG] = pop_importbids(bidsRoot, 'eventtype', 'trial_type', ...
    'bidsevent', 'on', 'bidschanloc', 'on', 'subjects', subjects, ...
    'outputdir', derivRoot, 'studyName', 'faces');
clear STUDY ALLEEG   % each subject is reloaded from disk inside the loop

%% Loop over subjects
nSub    = numel(subjects);
status  = repmat({'not run'}, nSub, 1);
nBad    = nan(nSub, 1);
nIcRem  = nan(nSub, 1);
nEpochs = nan(nSub, 1);

for s = 1:nSub
    subj = subjects{s};
    fprintf('\n===== %s (%d of %d) =====\n', subj, s, nSub);
    try
        inFile = fullfile(derivRoot, subj, 'eeg', sprintf('%s_task-FaceRecognition_eeg.set', subj));
        res = process_subject(inFile, cfg);
        status{s}  = 'ok';
        nBad(s)    = res.nBadChans;
        nIcRem(s)  = res.nIcRemoved;
        nEpochs(s) = res.nEpochs;
    catch err
        status{s} = 'failed';
        fid = fopen(logFile, 'a');
        fprintf(fid, '%s  %s  %s\n', datestr(now, 'yyyy-mm-dd HH:MM'), subj, err.message);
        fclose(fid);
        fprintf(2, 'FAILED %s: %s\n', subj, err.message);
    end
end

%% Summary table, one row per subject
summary = table(subjects(:), status, nBad, nIcRem, nEpochs, ...
    'VariableNames', {'subject', 'status', 'bad_channels', 'ics_removed', 'epochs_kept'});
disp(summary);
writetable(summary, fullfile(derivRoot, 'batch_summary.csv'));

%% ------------------------------------------------------------------
function res = process_subject(inFile, cfg)
% Single-subject pipeline from stations 3 to 5, with project settings in cfg

[inDir, baseName] = fileparts(inFile);
baseName = strrep(baseName, '_eeg', '');
EEG = pop_loadset('filename', [baseName '_eeg.set'], 'filepath', inDir);

% Remove ECG channels, keep EOG for ICA
EEG = pop_select(EEG, 'rmchannel', cfg.dropLabels);
fullLocs = EEG.chanlocs;

% High-pass: passband edge at 0.1 Hz (removes slow drift)
EEG = pop_eegfiltnew(EEG, 'locutoff', 0.1);
% Notch: remove a narrow band around the line frequency (revfilt = 1)
EEG = pop_eegfiltnew(EEG, 'locutoff', cfg.lineFreq - 2, 'hicutoff', cfg.lineFreq + 2, 'revfilt', 1);

% Detection with clean_rawdata channel criteria only:
% no drift removal, no ASR, no window rejection, EOG channels ignored
EEG_test = pop_clean_rawdata(EEG, 'FlatlineCriterion', 5, 'ChannelCriterion', 0.8, ...
    'LineNoiseCriterion', 4, 'Highpass', 'off', 'BurstCriterion', 'off', ...
    'WindowCriterion', 'off', 'BurstRejection', 'off', 'Distance', 'Euclidian', ...
    'channels_ignore', cfg.eogLabels);
badChans = setdiff({EEG.chanlocs.labels}, {EEG_test.chanlocs.labels}, 'stable');
fprintf('Bad channels: %s\n', strjoin(badChans, ', '));
if ~isempty(badChans)
    EEG = pop_select(EEG, 'rmchannel', badChans);
end
if ~isempty(badChans)
    EEG = pop_interp(EEG, fullLocs, 'spherical');
end
clear EEG_test

% Average reference, EOG channels excluded
eogIdx = find(ismember({EEG.chanlocs.labels}, cfg.eogLabels));
EEG = pop_reref(EEG, [], 'exclude', eogIdx);

% ICA copy: same data, high-pass at 1 Hz
EEG_main = EEG;
EEG_ica = pop_eegfiltnew(EEG_main, 'locutoff', 1);
EEG_ica = pop_runica(EEG_ica, 'icatype', 'runica', 'extended', 1);

% Copy the decomposition back to the main dataset
EEG_main.icaweights  = EEG_ica.icaweights;
EEG_main.icasphere   = EEG_ica.icasphere;
EEG_main.icachansind = EEG_ica.icachansind;
EEG_main.icawinv     = [];   % recomputed by eeg_checkset
EEG_main.icaact      = [];
EEG = eeg_checkset(EEG_main);
clear EEG_main EEG_ica

% Flag components above 90% Muscle or Eye, then remove them
EEG = pop_iclabel(EEG, 'default');
thresh = [NaN NaN; 0.9 1; 0.9 1; NaN NaN; NaN NaN; NaN NaN; NaN NaN];
EEG = pop_icflag(EEG, thresh);
artifactComps = find(EEG.reject.gcompreject);
if ~isempty(artifactComps)
    EEG = pop_subcomp(EEG, artifactComps, 0);
end

% Low-pass at 30 Hz, postponed until after ICA cleaning
EEG = pop_eegfiltnew(EEG, 'hicutoff', 30);
EEG.setname = [baseName ' clean'];
EEG = pop_saveset(EEG, 'filename', [baseName '_desc-clean_eeg.set'], 'filepath', inDir);

% Epochs, baseline, rejection of epochs beyond the threshold
EEG = pop_epoch(EEG, cfg.epochEvents, cfg.epochWindow, 'epochinfo', 'yes');
EEG = pop_rmbase(EEG, [-200 0]);
EEG = pop_eegthresh(EEG, 1, 1:EEG.nbchan, -cfg.thresholdUv, cfg.thresholdUv, EEG.xmin, EEG.xmax, 0, 0);
if any(EEG.reject.rejthresh)
    EEG = pop_rejepoch(EEG, EEG.reject.rejthresh, 0);
end
EEG.setname = [baseName ' epochs'];
EEG = pop_saveset(EEG, 'filename', [baseName '_desc-epochs_eeg.set'], 'filepath', inDir);

res.nBadChans  = numel(badChans);
res.nIcRemoved = numel(artifactComps);
res.nEpochs    = EEG.trials;
end

أي قيمة أختار؟

القيم المناسبة حسب هدف تحليلك
الإعدادالقيمة في الدرسلماذا
تردد الكهرباء50 هرتزD4 مسجّلة في كامبريدج ببريطانيا، و PowerLineFrequency في ملف eeg.json يساوي 50. بياناتك من السعودية 60. هذا الإعداد هو الفرق الأوضح بين D1 و D4.
قنوات العين والقلبEEG061 و EEG062 للعين، و EEG063 و EEG064 للقلبمن ملف channels.tsv في D4. قنوات القلب تُحذف، وقنوات العين تبقى لتساعد ICA ولا تدخل المرجع.
الأحداث للتقطيعfamous_new و unfamiliar_new و scrambled_newالعرض الأول لكل صورة، 150 لكل نوع في sub-002. التكرارات لها أسماء أخرى (_second_early و _second_late).
الاستيرادpop_importbids مرة واحدة قبل الحلقةيقرأ الأحداث من events.tsv ومواقع الأقطاب من electrodes.tsv، كما في درس EEGLAB الرسمي لنفس البيانات.
مكان الإعداداتالمتغيّر cfg في الأعلىكل ما يختلف بين المشاريع في مكان واحد، والدالة نفسها لا تتغيّر.
ما يحدث عند الخطأسطر في batch_log.txt، والحالة failed، ثم المشارك التاليتعرف في الصباح من فشل ولماذا، وتعيد تشغيله وحده.

كيف أتأكد أنها نجحت؟

4 فحوص سريعة
  • الجدول المطبوع في النهاية فيه ثلاثة صفوف، والحالة ok في كل صف.
  • logs/batch_log.txt غير موجود أو فارغ. إذا وُجد سطر، فاسم المشارك والسبب فيه.
  • في مجلد كل مشارك داخل derivatives/eeglab ملفان جديدان: desc-clean و desc-epochs.
  • عدد الـ epochs الباقية لكل مشارك قريب من 450 (150 لكل نوع)، ناقص المرفوضة وأي epoch يقطع حدث boundary (5 في sub-002)، لأن pop_epoch يحذفها. إذا كان أقل بكثير لمشارك واحد، فراجعه.

ما الأخطاء التي يقع فيها كثيرون؟

5 أخطاء، وكيف تتجنّبها
  • حلقة بلا try و catch. خطأ واحد في مشارك واحد يوقف المعالجة كلها، وتضيع ساعات الليل.
  • catch صامت. التقاط الخطأ بلا تسجيله أسوأ من التوقف: تظن أن كل المشاركين انتهوا. سجّل الاسم والرسالة دائماً، وانظر جدول الملخص.
  • إعدادات مكتوبة داخل الدالة. إذا كتبت 60 داخل process_subject ونسيتها، تعالج بيانات بريطانية بفلتر سعودي. كل إعداد في cfg.
  • نسيان الفرق بين مجموعات البيانات. قنوات العين في D1 اسمها EOG1 و EOG2، وفي D4 EEG061 و EEG062. نسخ السكربت بلا مراجعة هذه الأسماء يجعل ICA والمرجع يعاملان قنوات العين كقنوات رأس.
  • الثقة بالحلقة بلا فحص. ملخص يقول ok لا يعني أن التنظيف جيد. افتح ملفاً أو اثنين من الواجهة، وانظر عدد المكوّنات المحذوفة والـ epochs الباقية لكل مشارك. القيمة الشاذة في الجدول هي المشارك الذي تراجعه أولاً.

كيف أكتبها في ورقتي؟

قالب Methods: عدّل البيانات والإعدادات والإصدارات لتطابق ما نفّذته

قالب: عدّل البيانات والإعدادات والإصدارات لتطابق ما نفّذته.

Data from [n] participants (ds002718; EEG-BIDS plugin 10.5, EEGLAB 2026.1) were high-pass filtered (passband edge 0.1 Hz; firfilt plugin 2.8), and line noise was attenuated with a band-stop filter (passband edges 48 and 52 Hz). Bad channels were identified with clean_rawdata (clean_rawdata plugin 2.11; flatline 5 s, channel correlation 0.8) and interpolated, then data were average-referenced, EOG excluded. Independent component analysis (extended Infomax; Lee et al., 1999) was computed on a 1 Hz high-passed copy, and components labelled Eye or Muscle with probability ≥ 0.9 (ICLabel 1.7; Pion-Tonachini et al., 2019) were removed. After low-pass filtering (passband edge 30 Hz), epochs from −1 to 2 s were baseline-corrected (−200 to 0 ms), and epochs beyond ±100 µV were rejected.

أين أقرأ أكثر؟

3 مراجع: التوثيق الرسمي أولاً

مكتوب لـ EEGLAB 2026.1 · 2026-10-28

تحتاج مساعدة؟

اسأل، أو شاركنا رأيك، أو بلّغ عن خطأ