1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211
| import torch import torch.nn as nn import numpy as np from typing import Tuple
class PhysiologicalPreprocessing(nn.Module): """ SpiRadar 预处理模块 论文 Section III-A 解决强制呼气动作中的非自主运动伪影 关键问题: - 强制呼气(FEV1 maneuver)产生大幅躯干运动 - 非自主运动叠加在呼吸信号上 - 需要区分胸腔呼吸运动与整体躯干位移 """ def __init__(self, fs=200, cutoff_freq=5.0): super().__init__() self.fs = fs from scipy.signal import butter, filtfilt self.b_low, self.a_low = butter(4, cutoff_freq / (fs / 2), btype='low') def forward(self, raw_phase: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]: """ Args: raw_phase: 雷达回波相位 (B, T) - 胸壁位移原始信号 Returns: respiratory_signal: 呼吸信号 (B, T) motion_artifact: 运动伪影 (B, T) """ phase_np = raw_phase.cpu().numpy() results = [] artifacts = [] for i in range(phase_np.shape[0]): signal = phase_np[i] signal = self._log_interpolation_correct(signal) resp = filtfilt(self.b_low, self.a_low, signal) artifact = signal - resp results.append(resp) artifacts.append(artifact) return (torch.from_numpy(np.array(results)).to(raw_phase.device), torch.from_numpy(np.array(artifacts)).to(raw_phase.device)) def _log_interpolation_correct(self, signal): """对数插值校正相位跳变""" diff = np.diff(signal) jumps = np.where(np.abs(diff) > np.pi)[0] for j in jumps: if j > 0 and j < len(signal) - 1: signal[j] = (signal[j-1] + signal[j+1]) / 2 return signal
class SparsePolynomialTransform(nn.Module): """ 稀疏多项式变换模型 论文 Section III-B 将雷达测量的胸壁位移转换为肺活量体积曲线 核心创新: 1. 特征依赖的多项式(不同呼吸阶段使用不同系数) 2. 稀疏优化(L1 正则化)实现跨被试泛化 3. 无需个体校准 """ def __init__(self, poly_order=5, num_features=8): super().__init__() self.poly_order = poly_order self.num_features = num_features self.feature_extractor = nn.Sequential( nn.Conv1d(1, 32, kernel_size=5, padding=2), nn.ReLU(), nn.MaxPool1d(2), nn.Conv1d(32, 64, kernel_size=5, padding=2), nn.ReLU(), nn.AdaptiveAvgPool1d(1) ) self.coeff_predictor = nn.Sequential( nn.Linear(64, 128), nn.ReLU(), nn.Linear(128, (poly_order + 1) * num_features) ) self.sparse_gate = nn.Sequential( nn.Linear(64, num_features), nn.Sigmoid() ) def forward(self, displacement: torch.Tensor) -> torch.Tensor: """ Args: displacement: 胸壁位移序列 (B, T) Returns: volume: 肺活量体积曲线 (B, T) """ B, T = displacement.shape x = displacement.unsqueeze(1) feat = self.feature_extractor(x) feat = feat.flatten(1) coeffs = self.coeff_predictor(feat) coeffs = coeffs.view(B, self.num_features, self.poly_order + 1) gate = self.sparse_gate(feat) weighted_coeffs = coeffs * gate.unsqueeze(-1) final_coeffs = weighted_coeffs.sum(dim=1) volume = torch.zeros_like(displacement) for k in range(self.poly_order + 1): volume = volume + final_coeffs[:, k:k+1] * displacement ** k return volume
class SpiRadarModel(nn.Module): """ SpiRadar: 完整的非接触雷达肺活量测定模型 论文:arXiv:2607.19798 应用:座舱呼吸监测、哮喘管理、COPD 监测 """ def __init__(self): super().__init__() self.preprocessing = PhysiologicalPreprocessing(fs=200) self.transform = SparsePolynomialTransform(poly_order=5, num_features=8) def forward(self, radar_phase: torch.Tensor) -> dict: """ Args: radar_phase: 雷达回波相位 (B, T) Returns: results: dict with volume, breathing_rate, clinical_params """ resp_signal, motion = self.preprocessing(radar_phase) volume = self.transform(resp_signal) pev1 = volume[:, :int(0.1 * volume.shape[1])].max(dim=1)[0] fvc = volume.max(dim=1)[0] pef = volume[:, :int(0.05 * volume.shape[1])].max(dim=1)[0] return { 'volume_curve': volume, 'breathing_signal': resp_signal, 'motion_artifact': motion, 'FEV1': pev1, 'FVC': fvc, 'PEF': pef }
if __name__ == "__main__": print("=" * 60) print("SpiRadar 非接触雷达肺活量测定测试") print("=" * 60) model = SpiRadarModel() T = 2000 t = np.linspace(0, 10, T) breathing = 0.5 * np.sin(2 * np.pi * 0.3 * t) motion = 0.2 * np.random.randn(T) radar_phase = torch.tensor(breathing + motion, dtype=torch.float32).unsqueeze(0) results = model(radar_phase) print(f"输入: {radar_phase.shape} (B, T)") print(f"体积曲线: {results['volume_curve'].shape}") print(f"FEV1: {results['FEV1'].item():.3f} L") print(f"FVC: {results['FVC'].item():.3f} L") print(f"PEF: {results['PEF'].item():.3f} L/s") total_params = sum(p.numel() for p in model.parameters()) print(f"\n模型参数量: {total_params / 1e3:.1f}K")
|