# -*- coding: utf-8 -*- from __future__ import annotations """ run_metrics_and_figs.py 1) 自动读取 mat_dir 中排序后的第一个 .mat 2) 调用模型预测(HC/MDD)并写 ResultData.txt 3) 同时保存图片:EEG.png / psd.png / average_topomap.png / topomaps.png """ import matplotlib matplotlib.use('Agg') import numpy as np import os import shutil import scipy.io import scipy.signal as signal import matplotlib.pyplot as plt import mne from mne.preprocessing import ICA # ========================== # Config # ========================== PREPROCESS_BANDPASS = (0.8, 30.0) PREPROCESS_NOTCH = [50, 100] PREPROCESS_ICA_N = 0.99 PREPROCESS_ICA_SEED = 97 PREPROCESS_APPLY_AVG_REF = True PREPROCESS_BAD_PTP_UV = 350.0 # 坏段阈值 (μV) DEFAULT_FS = 250.0 EEG_PLOT_SECONDS = 10 PSD_FMIN, PSD_FMAX = 0.8, 45.0 EPS = 1e-12 FIXED_EEG_IDXS = [23, 47, 39, 6, 2, 21, 35, 57] # 0-based index, 按重要性排序 FIXED_EEG_LABELS = ["C5", "O1", "TP7", "FPZ", "PO6", "P4", "AF7", "AF3"] BANDS_METRICS = { "Delta": (1.0, 4.0), "Theta": (4.0, 8.0), "Alpha": (8.0, 13.0), "Beta": (13.0, 30.0), } TOTAL_POWER_BAND = (1.0, 50.0) BANDS_TOPOMAP = { "delta": (0.8, 3.9), "theta": (4.0, 7.9), "alpha": (8.0, 12.9), "beta": (13.0, 30.0), "broad": (0.8, 30.0), } # ========================== # 预处理逻辑 # ========================== def annotate_bad_segments(raw, peak_to_peak_uv=250.0): """ 简单坏段检测:按固定窗口计算峰峰值,超过阈值标为 bad。 """ peak_to_peak_v = peak_to_peak_uv * 1e-6 win = int(raw.info["sfreq"] * 1.0) step = int(raw.info["sfreq"] * 0.5) data = raw.get_data() n_times = data.shape[1] onsets = [] durations = [] descriptions = [] for start in range(0, n_times - win, step): seg = data[:, start:start + win] ptp = np.ptp(seg, axis=1) if np.any(ptp > peak_to_peak_v): onsets.append(start / raw.info["sfreq"]) durations.append(win / raw.info["sfreq"]) descriptions.append("BAD_PTP") if len(onsets) > 0: ann = mne.Annotations(onset=onsets, duration=durations, description=descriptions) raw.set_annotations(ann) print(f"[INFO] Annotated bad segments: {len(onsets)} windows") else: print("[INFO] No bad segments detected by PTP rule") def run_preprocess_on_raw(raw: mne.io.RawArray) -> mne.io.RawArray: """ 核心预处理:滤波 + 平均参考 + 坏段标注 + ICA """ # 1) 滤波 raw.filter(PREPROCESS_BANDPASS[0], PREPROCESS_BANDPASS[1], fir_design="firwin", verbose=False) raw.notch_filter(PREPROCESS_NOTCH, fir_design="firwin", verbose=False) # 2) 平均参考 if PREPROCESS_APPLY_AVG_REF: raw.set_eeg_reference("average", verbose=False) # 3) 坏段标注 annotate_bad_segments(raw, peak_to_peak_uv=PREPROCESS_BAD_PTP_UV) # 4) ICA ica = ICA( n_components=PREPROCESS_ICA_N, random_state=PREPROCESS_ICA_SEED, max_iter=800, method="fastica" ) ica.fit(raw, reject_by_annotation=True, verbose=False) try: eog_inds, _ = ica.find_bads_eog(raw, verbose=False) if eog_inds: ica.exclude.extend(eog_inds) print(f"[INFO] ICA exclude EOG comps: {eog_inds}") except Exception as e: print(f"[WARN] ICA find_bads_eog skipped: {e}") raw_clean = ica.apply(raw.copy(), verbose=False) return raw_clean def preprocess_mat_file(src_mat_path: str, temp_out_dir: str) -> str: """ 读取原始mat -> 预处理 -> 保存到 temp_out_dir -> 返回新路径 """ os.makedirs(temp_out_dir, exist_ok=True) # 1. 读原始 mat # 注意:这里我们只要数据部分转成 MNE Raw,然后处理,再存回 # 复用现有的 load_eeg_from_mat 拿到 ndarray eeg_uV, fs, ch_names, xyz = load_eeg_from_mat(src_mat_path) # 转 MNE (注意单位:uV -> V) if not ch_names: ch_names = [f"CH{i+1}" for i in range(eeg_uV.shape[1])] info = mne.create_info(ch_names=ch_names, sfreq=fs, ch_types=["eeg"] * len(ch_names)) raw = mne.io.RawArray(eeg_uV.T * 1e-6, info, verbose=False) if xyz is not None and isinstance(xyz, np.ndarray): # 尝试设 montage(虽然对滤波不关键,但尽量保留信息) try: ch_pos = {ch_names[i]: xyz[i, :] for i in range(len(ch_names))} montage = mne.channels.make_dig_montage(ch_pos=ch_pos, coord_frame="head") raw.set_montage(montage, on_missing="ignore") except Exception: pass # 2. 执行预处理 print(f"[INFO] Start preprocessing: {src_mat_path}") raw_clean = run_preprocess_on_raw(raw) # 3. 存回 .mat (保持结构兼容,以便后续 run_all 读取) # 这里我们需要读取原始 mat 的结构体,把 data 替换掉 try: mat_struct = scipy.io.loadmat(src_mat_path, struct_as_record=False, squeeze_me=True) if "eeg" in mat_struct: eeg_obj = mat_struct["eeg"] # 替换数据:MNE (V) -> uV -> (T, C) clean_data_uV = (raw_clean.get_data() * 1e6).T eeg_obj.data = clean_data_uV base_name = os.path.basename(src_mat_path) new_path = os.path.join(temp_out_dir, base_name) scipy.io.savemat(new_path, {"eeg": eeg_obj}, do_compression=True) print(f"[INFO] Preprocessed file saved to: {new_path}") return new_path except Exception as e: print(f"[WARN] Failed to preserve original struct structure: {e}") # Fallback: 如果读原始结构失败,就存一个简单的 mat clean_data_uV = (raw_clean.get_data() * 1e6).T out_dict = { "eeg": { "data": clean_data_uV, "sample_rate": fs, "electrode_name": ch_names, "electrode_xyz": xyz if xyz is not None else [] } } base_name = os.path.basename(src_mat_path) new_path = os.path.join(temp_out_dir, base_name) scipy.io.savemat(new_path, out_dict, do_compression=True) print(f"[INFO] Preprocessed file saved (fallback mode) to: {new_path}") return new_path # ========================== # 输出目录 # ========================== def ensure_outdir(out_root: str) -> str: """ 确保输出目录存在,并清空除 ResultData.txt 之外的旧文件。 不再创建 timestamp 子文件夹,直接输出到 out_root。 """ if os.path.exists(out_root): # 清空目录,但保留 ResultData.txt for filename in os.listdir(out_root): if filename == "ResultData.txt": continue file_path = os.path.join(out_root, filename) try: if os.path.isfile(file_path) or os.path.islink(file_path): os.unlink(file_path) elif os.path.isdir(file_path): shutil.rmtree(file_path) except Exception as e: print(f"[WARN] Failed to delete {file_path}. Reason: {e}") else: os.makedirs(out_root, exist_ok=True) return out_root # ========================== # 单位自动识别:统一到 μV # ========================== def _auto_scale_to_uV(data_nt_nc: np.ndarray): data = np.asarray(data_nt_nc) p95 = float(np.percentile(np.abs(data), 95)) if p95 <= 0.5: data_uV = data * 1e6 msg = f"[UNIT] p95={p95:.3g} -> assume V, convert to μV by *1e6" elif p95 > 5000: data_uV = data * 1e-3 msg = f"[UNIT] p95={p95:.3g} -> assume nV, convert to μV by /1000" else: data_uV = data msg = f"[UNIT] p95={p95:.3g} -> assume μV, no scaling" p95_uV = float(np.percentile(np.abs(data_uV), 95)) warn = None if p95_uV > 5000: warn = f"[WARN] After scaling, p95 still large: {p95_uV:.3g} μV" elif p95_uV < 0.1: warn = f"[WARN] After scaling, p95 still small: {p95_uV:.3g} μV" return data_uV, msg, warn # ========================== # mat 读取(支持 struct.data / electrode_name / electrode_xyz / sample_rate) # ========================== def _unwrap_singleton(x): while True: if isinstance(x, np.ndarray): if x.dtype == object and x.size == 1: x = x.item() continue if x.size == 1 and x.ndim >= 1: try: x = x.reshape(-1)[0] continue except Exception: pass break return x def _try_get_struct_field(v, field_name="data"): if hasattr(v, "_fieldnames") and field_name in getattr(v, "_fieldnames", []): return getattr(v, field_name) if isinstance(v, np.ndarray) and v.dtype.names and field_name in v.dtype.names: try: return v[field_name] except Exception: return None return None def _extract_electrode_names(st): nf = _try_get_struct_field(st, "electrode_name") if nf is None: return None nf = _unwrap_singleton(nf) if isinstance(nf, (list, tuple)): names = [str(x).strip() for x in nf] return names if names else None if isinstance(nf, np.ndarray): flat = nf.reshape(-1) names = [str(_unwrap_singleton(x)).strip() for x in flat] return names if names else None s = str(nf).strip() return [s] if s else None def _extract_sample_rate(st): sr = _try_get_struct_field(st, "sample_rate") if sr is None: return None sr = _unwrap_singleton(sr) try: return float(sr) except Exception: return None def _extract_xyz(st): xyz = _try_get_struct_field(st, "electrode_xyz") if xyz is None: return None xyz = _unwrap_singleton(xyz) try: xyz = np.asarray(xyz, dtype=float) if xyz.ndim == 2 and xyz.shape[1] == 3: return xyz if xyz.ndim == 2 and xyz.shape[0] == 3: return xyz.T return None except Exception: return None def load_eeg_from_mat(mat_path: str): mat = scipy.io.loadmat(mat_path, struct_as_record=False, squeeze_me=True) candidates = [] st_for_meta = None for k, v in mat.items(): if k.startswith("__"): continue if isinstance(v, np.ndarray) and v.ndim == 2 and np.issubdtype(v.dtype, np.number): candidates.append((k, v, None)) continue data_field = _try_get_struct_field(v, "data") if data_field is not None: data_field = _unwrap_singleton(data_field) if isinstance(data_field, np.ndarray) and data_field.ndim == 2: candidates.append((f"{k}.data", data_field, v)) continue if isinstance(v, np.ndarray) and v.dtype == object: vv = _unwrap_singleton(v) if isinstance(vv, np.ndarray) and vv.ndim == 2 and np.issubdtype(vv.dtype, np.number): candidates.append((k, vv, None)) continue data2 = _try_get_struct_field(vv, "data") if data2 is not None: data2 = _unwrap_singleton(data2) if isinstance(data2, np.ndarray) and data2.ndim == 2: candidates.append((f"{k}.data", data2, vv)) continue if not candidates: raise RuntimeError(f"mat 里没找到可用 EEG 二维矩阵或 struct.data:{mat_path}") def score(arr: np.ndarray) -> int: s = 0 if 64 in arr.shape: s += 10 if 32 in arr.shape: s += 9 if 128 in arr.shape: s += 8 if 129 in arr.shape: s += 7 s += int(np.prod(arr.shape) // 100000) return s candidates.sort(key=lambda x: score(x[1]), reverse=True) key, eeg, st = candidates[0] st_for_meta = st eeg = np.asarray(_unwrap_singleton(eeg), dtype=np.float32) if eeg.ndim != 2: raise RuntimeError(f"解析结果不是二维: key={key}, shape={eeg.shape}, file={mat_path}") # 统一成 (T, C) if eeg.shape[0] in (32, 64, 128, 129) and eeg.shape[1] not in (32, 64, 128, 129): eeg = eeg.T elif eeg.shape[1] in (32, 64, 128, 129): if eeg.shape[0] in (32, 64, 128, 129) and eeg.shape[0] < eeg.shape[1]: eeg = eeg.T fs = DEFAULT_FS ch_names = None xyz = None if st_for_meta is not None: fs2 = _extract_sample_rate(st_for_meta) if fs2 is not None and fs2 > 1: fs = float(fs2) ch_names = _extract_electrode_names(st_for_meta) xyz = _extract_xyz(st_for_meta) eeg_uV, msg, warn = _auto_scale_to_uV(eeg) print(msg) if warn: print(warn) return eeg_uV.astype(np.float32), float(fs), ch_names, xyz # ========================== # 预测接口:导入 predict_hc_mdd # ========================== def _predict_label_by_model(model_path: str, mat_dir: str) -> str: try: from infer_pth import predict_hc_mdd except Exception as e: raise RuntimeError( "无法导入 predict_hc_mdd(请确保 pre.py 或 infer_pth.py 与本文件同目录)。\n" f"原始错误: {e}" ) try: out = predict_hc_mdd(mat_dir, model_path) except TypeError: out = predict_hc_mdd(model_path, mat_dir) label = str(out.get("pred_label", "")).strip().upper() if label not in ("HC", "MDD"): raise RuntimeError(f"predict_hc_mdd 返回 pred_label 非法: {label},原始返回: {out}") return label # ========================== # 通道分区 # ========================== def _norm_name(s: str) -> str: return str(s).strip().upper().replace(" ", "") def build_channel_index_map(ch_names, n_channels: int): if not ch_names or len(ch_names) != n_channels: return {} return {_norm_name(nm): i for i, nm in enumerate(ch_names)} def pick_indices_by_names(name_to_idx, names): idx = [] for n in names: nn = _norm_name(n) if nn in name_to_idx: idx.append(name_to_idx[nn]) return sorted(list(set(idx))) def _fallback_region_indices(n_channels: int): a = int(n_channels * 0.33) b = int(n_channels * 0.66) frontal = list(range(0, a)) central = list(range(a, b)) parietal = list(range(b, n_channels)) prefrontal = list(range(0, max(2, a // 2))) posterior = list(range(b, n_channels)) left = [i for i in range(n_channels) if i % 2 == 0] right = [i for i in range(n_channels) if i % 2 == 1] return frontal, central, parietal, prefrontal, posterior, left, right def get_region_indices(name_to_idx, n_channels: int): if not name_to_idx: return _fallback_region_indices(n_channels) central_names = ["CZ","C1","C2","C3","C4","C5","C6","CP1","CP2","CP3","CP4","CP5","CP6","FC1","FC2","FC3","FC4","FC5","FC6"] frontal_names = ["FZ","F1","F2","F3","F4","F5","F6","F7","F8","AF3","AF4","AF7","AF8","FPZ","FP1","FP2","FCZ"] parietal_names = ["PZ","P1","P2","P3","P4","P5","P6","POZ","PO3","PO4","PO5","PO6","PO7","PO8","CPZ"] prefrontal_names = ["FP1","FP2","FPZ","AF3","AF4","AF7","AF8"] posterior_names = ["O1","O2","OZ","PO7","PO8","PO3","PO4","PZ","P3","P4","P1","P2"] central = pick_indices_by_names(name_to_idx, central_names) frontal = pick_indices_by_names(name_to_idx, frontal_names) parietal = pick_indices_by_names(name_to_idx, parietal_names) prefrontal = pick_indices_by_names(name_to_idx, prefrontal_names) posterior = pick_indices_by_names(name_to_idx, posterior_names) left_names = ["FP1","AF3","AF7","F3","F5","F7"] right_names = ["FP2","AF4","AF8","F4","F6","F8"] left = pick_indices_by_names(name_to_idx, left_names) right = pick_indices_by_names(name_to_idx, right_names) if not (central and frontal and parietal and prefrontal and posterior): fb = _fallback_region_indices(n_channels) frontal2, central2, parietal2, prefrontal2, posterior2, left2, right2 = fb frontal = frontal if frontal else frontal2 central = central if central else central2 parietal = parietal if parietal else parietal2 prefrontal = prefrontal if prefrontal else prefrontal2 posterior = posterior if posterior else posterior2 left = left if left else left2 right = right if right else right2 return frontal, central, parietal, prefrontal, posterior, left, right # ========================== # Welch PSD + band power # ========================== def welch_psd(eeg_tc: np.ndarray, fs: float): nperseg = min(1024, eeg_tc.shape[0]) if nperseg < 128: nperseg = min(256, eeg_tc.shape[0]) freqs, pxx = signal.welch( eeg_tc, fs=fs, nperseg=nperseg, noverlap=nperseg // 2, axis=0, scaling="density", ) return freqs, pxx def band_power_from_psd(freqs, pxx_fc, band): lo, hi = band m = (freqs >= lo) & (freqs < hi) if not np.any(m): return np.zeros((pxx_fc.shape[1],), dtype=np.float32) # 兼容处理:numpy 2.0+ 推荐使用 trapezoid,旧版本用 trapz if hasattr(np, "trapezoid"): return np.trapezoid(pxx_fc[m, :], freqs[m], axis=0).astype(np.float32) else: return np.trapz(pxx_fc[m, :], freqs[m], axis=0).astype(np.float32) def region_mean_power(freqs, pxx_fc, idx, band) -> float: if not idx: return 0.0 pw = band_power_from_psd(freqs, pxx_fc, band) return float(np.mean(pw[idx])) def compute_iaf(freqs, pxx_fc, posterior_idx): lo, hi = BANDS_METRICS["Alpha"] m = (freqs >= lo) & (freqs <= hi) if not np.any(m) or not posterior_idx: return 0.0 spec = np.mean(pxx_fc[:, posterior_idx], axis=1) sub = spec[m] fsub = freqs[m] return float(fsub[int(np.argmax(sub))]) # ========================== # 图:EEG波形、PSD # ========================== def plot_eeg_waveforms(data_uv_tc: np.ndarray, fs: float, ch_names, out_dir: str, seconds: int = 10): """ 固定用 FIXED_EEG_IDXS 画 EEG.png(按重要性排序) data_uv_tc: (T, C) μV """ T, C = data_uv_tc.shape # 1) 过滤越界索引(避免你的数据通道数不足时报错) idxs = [i for i in FIXED_EEG_IDXS if 0 <= i < C] if len(idxs) < len(FIXED_EEG_IDXS): missing = [i for i in FIXED_EEG_IDXS if not (0 <= i < C)] print(f"[WARN] Some fixed EEG indices out of range (C={C}): {missing}") if len(idxs) == 0: raise RuntimeError(f"No valid indices in FIXED_EEG_IDXS for current data (C={C}).") # 2) 通道显示 picked_names = [] for idx in idxs: # 找 idx 在 FIXED_EEG_IDXS 的位置,用对应标签 pos = FIXED_EEG_IDXS.index(idx) std_label = FIXED_EEG_LABELS[pos] if pos < len(FIXED_EEG_LABELS) else f"CH{idx}" if ch_names and idx < len(ch_names): picked_names.append(f"{std_label}") else: picked_names.append(std_label) # 3) 截取前 seconds 秒 max_samples = int(min(T, seconds * fs)) x = np.arange(max_samples) / fs fig_h = 1.4 * len(idxs) + 1 fig, axes = plt.subplots(len(idxs), 1, figsize=(10, fig_h), sharex=True) if len(idxs) == 1: axes = [axes] # 4) 分位数定范围,避免尖峰撑爆 seg = data_uv_tc[:max_samples, idxs].T # (n_ch, samples) lo = float(np.percentile(seg, 1)) hi = float(np.percentile(seg, 99)) m = max(abs(lo), abs(hi)) m = max(m, 50.0) for ax, ch_idx, nm in zip(axes, idxs, picked_names): y = data_uv_tc[:max_samples, ch_idx] ax.plot(x, y, linewidth=1.2) ax.set_ylabel("μV") ax.set_title(nm, loc="left", fontsize=10) ax.grid(True, alpha=0.3) ax.set_ylim(-m, m) axes[-1].set_xlabel("Time (s)") plt.tight_layout() out_path = os.path.join(out_dir, "EEG.png") plt.savefig(out_path, dpi=200) plt.close(fig) print(f"[OK] EEG waveform saved: {out_path}") def plot_psd(eeg_uV_tc, fs, ch_names, out_dir): C = eeg_uV_tc.shape[1] chosen_idx = [] if ch_names: mp = {n.upper(): i for i, n in enumerate(ch_names)} for p in ["C3","C4","CZ"]: if p in mp: chosen_idx.append(mp[p]) if len(chosen_idx) < 3: stds = [(i, float(np.std(eeg_uV_tc[:, i]))) for i in range(C)] stds.sort(key=lambda x: x[1], reverse=True) for i, _ in stds: if i not in chosen_idx: chosen_idx.append(i) if len(chosen_idx) == 3: break chosen_name = [ch_names[i] for i in chosen_idx] else: stds = [(i, float(np.std(eeg_uV_tc[:, i]))) for i in range(C)] stds.sort(key=lambda x: x[1], reverse=True) chosen_idx = [i for i, _ in stds[:3]] chosen_name = [f"CH{i}" for i in chosen_idx] fig = plt.figure(figsize=(7.5, 4.8)) for idx, nm in zip(chosen_idx, chosen_name): f, pxx = signal.welch(eeg_uV_tc[:, idx], fs=fs, nperseg=int(2*fs), noverlap=int(1*fs)) mask = (f >= PSD_FMIN) & (f <= PSD_FMAX) p_db = 10 * np.log10(pxx[mask] + 1e-20) plt.plot(f[mask], p_db, linewidth=1.8, label=nm) plt.xlabel("Hz") plt.ylabel("Power (dB)") plt.title("PSD") plt.grid(True, alpha=0.3) plt.legend() plt.tight_layout() out_path = os.path.join(out_dir, "psd.png") plt.savefig(out_path, dpi=200) plt.close(fig) print(f"[OK] psd.png -> {out_path}") # ========================== # Topomap(如果有 xyz) # ========================== def build_mne_raw_from_uV(eeg_uV_tc, fs, ch_names, xyz): C = eeg_uV_tc.shape[1] if not ch_names: ch_names = [f"CH{i+1}" for i in range(C)] data_v_ct = eeg_uV_tc.T * 1e-6 # (C,T) V info = mne.create_info(ch_names=ch_names, sfreq=fs, ch_types=["eeg"] * C) raw = mne.io.RawArray(data_v_ct, info, verbose=False) if xyz is not None and isinstance(xyz, np.ndarray) and xyz.shape == (C, 3): try: ch_pos = {ch_names[i]: xyz[i, :] for i in range(C)} montage = mne.channels.make_dig_montage(ch_pos=ch_pos, coord_frame="head") raw.set_montage(montage, on_missing="ignore") except Exception as e: print(f"[WARN] set_montage failed (ignore): {e}") else: print("[WARN] electrode_xyz missing/invalid -> skip topomap") return raw def _raw_has_positions(raw): try: locs = np.array([ch["loc"][:3] for ch in raw.info["chs"]]) ok = np.isfinite(locs).all() and (np.linalg.norm(locs, axis=1) > 0).any() return bool(ok) except Exception: return False def compute_band_powers_for_topomap(raw, bands): data = raw.get_data() # (C,T) V fs = raw.info["sfreq"] psds, freqs = mne.time_frequency.psd_array_welch( data, sfreq=fs, fmin=min(v[0] for v in bands.values()), fmax=max(v[1] for v in bands.values()), n_fft=int(2 * fs), n_overlap=int(1 * fs), average="mean", verbose=False ) out = {} for k, (fmin, fmax) in bands.items(): idx = np.where((freqs >= fmin) & (freqs <= fmax))[0] # 兼容处理:numpy 2.0+ 推荐使用 trapezoid,旧版本用 trapz if hasattr(np, "trapezoid"): bp = np.trapezoid(psds[:, idx], freqs[idx], axis=1) # (C,) else: bp = np.trapz(psds[:, idx], freqs[idx], axis=1) # (C,) v = np.log10(bp + 1e-30) v = v - np.mean(v) out[k] = v return out def plot_average_topomap(raw, values, out_dir): fig, ax = plt.subplots(1, 1, figsize=(6.5, 4.6)) im, _ = mne.viz.plot_topomap(values, raw.info, axes=ax, show=False, contours=0,sphere=(0, 0, 0, 0.11)) ax.set_title("0.8-30 Hz", fontsize=12) plt.colorbar(im, ax=ax, shrink=0.85) plt.tight_layout() out_path = os.path.join(out_dir, "average_topomap.png") plt.savefig(out_path, dpi=200) plt.close(fig) print(f"[OK] average_topomap.png -> {out_path}") def plot_band_topomaps(raw, band_values, out_dir): order = [ ("delta", "δ (0.8-3.9Hz)"), ("theta", "θ (4-7.9Hz)"), ("alpha", "α (8-12.9Hz)"), ("beta", "β (13-30Hz)"), ("broad", "0.8-30 Hz"), ] fig, axes = plt.subplots(1, 5, figsize=(16, 4.2)) ims = [] for ax, (k, title) in zip(axes, order): im, _ = mne.viz.plot_topomap(band_values[k], raw.info, axes=ax, show=False, contours=0,extrapolate='head',sphere=(0, 0, 0, 0.11)) ax.set_title(title, fontsize=11) ims.append(im) fig.subplots_adjust(left=0.02, right=0.85, top=0.88, bottom=0.05, wspace=0.35) cax = fig.add_axes([0.87, 0.15, 0.015, 0.7]) fig.colorbar(ims[-1], cax=cax) out_path = os.path.join(out_dir, "topomaps.png") plt.savefig(out_path, dpi=200) plt.close(fig) print(f"[OK] topomaps.png -> {out_path}") # ========================== # 生成 ResultData.txt # ========================== def compute_and_save_txt(model_path, mat_dir, out_dir, eeg_uV_tc, fs, ch_names): pred_label = _predict_label_by_model(model_path, mat_dir) recommend = "是" if pred_label == "MDD" else "否" T, C = eeg_uV_tc.shape mp = build_channel_index_map(ch_names, C) frontal_idx, central_idx, parietal_idx, prefrontal_idx, posterior_idx, left_idx, right_idx = \ get_region_indices(mp, C) freqs, pxx = welch_psd(eeg_uV_tc, fs) central_alpha = region_mean_power(freqs, pxx, central_idx, BANDS_METRICS["Alpha"]) central_beta = region_mean_power(freqs, pxx, central_idx, BANDS_METRICS["Beta"]) frontal_alpha = region_mean_power(freqs, pxx, frontal_idx, BANDS_METRICS["Alpha"]) frontal_beta = region_mean_power(freqs, pxx, frontal_idx, BANDS_METRICS["Beta"]) par_alpha = region_mean_power(freqs, pxx, parietal_idx, BANDS_METRICS["Alpha"]) par_beta = region_mean_power(freqs, pxx, parietal_idx, BANDS_METRICS["Beta"]) central_ab = (central_alpha / (central_beta + EPS)) if central_beta > 0 else 0.0 frontal_ab = (frontal_alpha / (frontal_beta + EPS)) if frontal_beta > 0 else 0.0 par_ab = (par_alpha / (par_beta + EPS)) if par_beta > 0 else 0.0 central_theta = region_mean_power(freqs, pxx, central_idx, BANDS_METRICS["Theta"]) par_theta = region_mean_power(freqs, pxx, parietal_idx, BANDS_METRICS["Theta"]) central_tb = (central_theta / (central_beta + EPS)) if central_beta > 0 else 0.0 par_tb = (par_theta / (par_beta + EPS)) if par_beta > 0 else 0.0 if not left_idx or not right_idx: left_idx = [i for i in prefrontal_idx if (i % 2 == 0)] right_idx = [i for i in prefrontal_idx if (i % 2 == 1)] left_alpha = region_mean_power(freqs, pxx, left_idx, BANDS_METRICS["Alpha"]) right_alpha = region_mean_power(freqs, pxx, right_idx, BANDS_METRICS["Alpha"]) prefrontal_alpha_asym = float(np.log(right_alpha + EPS) - np.log(left_alpha + EPS)) iaf = compute_iaf(freqs, pxx, posterior_idx) pre_td = region_mean_power(freqs, pxx, prefrontal_idx, (BANDS_METRICS["Delta"][0], BANDS_METRICS["Theta"][1])) pre_total = region_mean_power(freqs, pxx, prefrontal_idx, TOTAL_POWER_BAND) pre_td_rel = (pre_td / (pre_total + EPS)) * 100.0 if pre_total > 0 else 0.0 def f1(x): return f"{x:.1f}" txt = ( f"中央区α/β波比值:{f1(central_ab)}\n" f"额区α/β波比值:{f1(frontal_ab)}\n" f"顶区α/β波比值:{f1(par_ab)}\n" f"中央区θ/β波比值:{f1(central_tb)}\n" f"顶区θ/β波比值:{f1(par_tb)}\n" f"前额叶α波不对称性:{f1(prefrontal_alpha_asym)}\n" f"个体化α峰值频率:{f1(iaf)}\n" f"前额叶θ+δ波功率:{f1(pre_td_rel)}\n" f"是否推荐治疗:{recommend}\n" ) out_path = os.path.join(out_dir, "ResultData.txt") with open(out_path, "w", encoding="utf-8") as f: f.write(txt) print(f"[OK] ResultData.txt -> {out_path}") # ========================== # 一个函数:一次性跑完(txt + 图片) # ========================== def run_all(model_path: str, mat_dir: str, out_root: str, seconds: int = EEG_PLOT_SECONDS): # 1) 选第一个 mat if not os.path.exists(mat_dir): raise RuntimeError(f"输入目录不存在: {mat_dir}") mats = [f for f in os.listdir(mat_dir) if f.lower().endswith(".mat")] if not mats: raise RuntimeError(f"mat_dir 下找不到 .mat: {mat_dir}") mats.sort() mat_file = os.path.join(mat_dir, mats[0]) print(f"[INFO] Found mat: {mat_file}") # 2) 创建输出目录 out_dir = ensure_outdir(out_root) print(f"[INFO] Output dir: {out_dir}") # --- 总是进行预处理 (默认模式) --- print("[INFO] Mode: Raw Data (Default). Running preprocessing...") temp_dir = os.path.join(out_dir, "temp_preprocessed") mat_file = preprocess_mat_file(mat_file, temp_dir) # 更新 mat_dir 指向临时目录(为了传给 compute_and_save_txt 里的 predict 接口) mat_dir = temp_dir # 3) 读 EEG(μV) eeg_uV_tc, fs, ch_names, xyz = load_eeg_from_mat(mat_file) print(f"[INFO] eeg shape(T,C)={eeg_uV_tc.shape}, fs={fs}") # 5) 画图:PSD + EEG plot_psd(eeg_uV_tc, fs, ch_names, out_dir) plot_eeg_waveforms(eeg_uV_tc, fs, ch_names, out_dir, seconds=seconds) # 6) topomap(有 xyz 才画) try: raw = build_mne_raw_from_uV(eeg_uV_tc, fs, ch_names, xyz) if _raw_has_positions(raw): band_vals = compute_band_powers_for_topomap(raw, BANDS_TOPOMAP) plot_average_topomap(raw, band_vals["broad"], out_dir) plot_band_topomaps(raw, band_vals, out_dir) else: print("[WARN] No valid positions -> skip topomap.") except Exception as e: print(f"[WARN] topomap failed -> skip. reason: {e}") # 4) 指标写 txt compute_and_save_txt(model_path, mat_dir, out_dir, eeg_uV_tc, fs, ch_names) print("[DONE] txt + figures generated.") return out_dir if __name__ == "__main__": import multiprocessing multiprocessing.freeze_support() import argparse import sys # 1. 路径锚定:获取资源绝对路径 def get_resource_path(relative_path): """ 获取资源的绝对路径。 策略:优先在当前执行目录(EXE所在目录)寻找。 这适用于“绿色软件”模式,即资源文件(model/raw_data)直接放在EXE旁边。 """ if getattr(sys, 'frozen', False): # PyInstaller 打包后的 EXE 所在目录 base_path = os.path.dirname(sys.executable) else: # 开发环境:当前脚本所在目录 base_path = os.path.dirname(os.path.abspath(__file__)) return os.path.join(base_path, relative_path) # 设置默认路径 DEFAULT_MODEL = get_resource_path(os.path.join("model", "Model_1.pth")) # 这里我们保持 mat_dir 和 out_root 相对于 EXE 所在目录(或当前工作目录) if getattr(sys, 'frozen', False): EXE_DIR = os.path.dirname(sys.executable) else: EXE_DIR = os.path.dirname(os.path.abspath(__file__)) DEFAULT_MAT = os.path.join(EXE_DIR, "raw_data") DEFAULT_OUT = os.path.join(EXE_DIR, "out") # 2. 解析命令行参数 parser = argparse.ArgumentParser(description="EEG Depression Assessment Algorithm Integration") parser.add_argument("--model_path", type=str, default=DEFAULT_MODEL, help="模型文件的路径 (.pth)") parser.add_argument("--mat_dir", type=str, default=DEFAULT_MAT, help="输入文件夹路径 (包含原始EEG .mat)") parser.add_argument("--out_root", type=str, default=DEFAULT_OUT, help="结果输出的根目录") parser.add_argument("--seconds", type=int, default=10, help="画波形图截取的秒数") args = parser.parse_args() # 3. 检查关键路径 if not os.path.exists(args.mat_dir): print(f"[WARN] 输入文件夹不存在: {args.mat_dir}") if not os.path.exists(args.model_path): print(f"[WARN] 模型文件不存在: {args.model_path}") # 4. 执行主流程 print(f"[*] 运行配置:") print(f" - Model : {args.model_path}") print(f" - Input : {args.mat_dir}") print(f" - Output: {args.out_root}") print(f" - Mode : RAW (Auto Preprocess)") run_all(args.model_path, args.mat_dir, args.out_root, seconds=args.seconds)