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 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339
| """ 隐私保护姿态监测管道 Syntronic 论文项目方案复现
管道步骤: 1. 摄像头采集 → 2. 姿态估计模型 → 3. 骨架序列提取 4. 时序分类器 → 5. 事件检测 → 6. 告警输出
关键设计: - 骨架数据不包含面部特征,无法识别身份 - 原始视频帧在传感器端处理,不存储不传输 - 仅骨架坐标序列被保存和分析 """
import numpy as np from dataclasses import dataclass from typing import List, Tuple, Optional from enum import IntEnum
class PassengerEvent(IntEnum): """乘员事件类型""" NORMAL = 0 FALLING = 1 FALLEN = 2 MEDICAL_DISTRESS = 3 CONFLICT = 4 STANDING = 5 ABNORMAL_POSTURE = 6
@dataclass class Skeleton: """ 人体骨架关键点 基于 COCO 17-keypoint 格式 关键点索引: 0: nose, 1-2: left/right eye, 3-4: left/right ear 5-6: left/right shoulder, 7-8: left/right elbow 9-10: left/right wrist, 11-12: left/right hip 13-14: left/right knee, 15-16: left/right ankle """ keypoints: np.ndarray timestamp: float person_id: int = -1 def get_center_of_mass(self) -> Tuple[float, float]: """计算骨架重心(用躯干关键点)""" torso_points = [5, 6, 11, 12] valid_points = self.keypoints[torso_points] mask = valid_points[:, 2] > 0.3 if mask.sum() == 0: return (0, 0) com_x = np.mean(valid_points[mask, 0]) com_y = np.mean(valid_points[mask, 1]) return (com_x, com_y) def get_height(self) -> float: """估算身高(头到脚距离)""" nose = self.keypoints[0] ankle_l = self.keypoints[15] ankle_r = self.keypoints[16] ankles = np.array([ankle_l, ankle_r]) valid_ankles = ankles[ankles[:, 2] > 0.3] if len(valid_ankles) == 0: return 0 ankle_y = np.mean(valid_ankles[:, 1]) return abs(nose[1] - ankle_y) def is_standing(self) -> bool: """判断是否站立""" height = self.get_height() com_x, com_y = self.get_center_of_mass() nose_y = self.keypoints[0, 1] if height < 50: return False hip_y = (self.keypoints[11, 1] + self.keypoints[12, 1]) / 2 if self.keypoints[11, 2] < 0.3 or self.keypoints[12, 2] < 0.3: return False leg_length = abs(hip_y - ankle_y) if ankle_y > 0 else 0 return leg_length > height * 0.4 def is_fallen(self) -> bool: """判断是否倒地""" com_x, com_y = self.get_center_of_mass() height = self.get_height() width = self.estimate_width() if height == 0 or width == 0: return False aspect_ratio = width / height return aspect_ratio > 1.5 def estimate_width(self) -> float: """估算骨架宽度""" shoulder_l = self.keypoints[5] shoulder_r = self.keypoints[6] if shoulder_l[2] < 0.3 or shoulder_r[2] < 0.3: return 0 return abs(shoulder_l[0] - shoulder_r[0])
class SkeletonSequence: """骨架时间序列""" def __init__(self, skeletons: List[Skeleton] = None): self.skeletons = skeletons or [] def add(self, skeleton: Skeleton): self.skeletons.append(skeleton) def to_feature_vector(self, window_size=30) -> np.ndarray: """ 将骨架序列转为特征向量 用于时序分类器输入 Args: window_size: 时间窗口大小(帧数) Returns: feature_vector: shape=(window_size, 17*3) """ if len(self.skeletons) < window_size: last = self.skeletons[-1] if self.skeletons else Skeleton( np.zeros((17, 3)), 0 ) while len(self.skeletons) < window_size: self.skeletons.append(last) features = [] for skel in self.skeletons[-window_size:]: features.append(skel.keypoints.flatten()) return np.array(features)
class EventClassifier: """ 事件分类器 基于骨架序列的时序分类 使用轻量级 LSTM 网络 优势: - 输入只有 17×3=51 维/帧(vs 1080×1920×3 视频帧) - 计算量极低,可部署在边缘设备 - 不包含身份信息,GDPR 合规 """ def __init__(self, num_classes=7, hidden_size=64, num_layers=1, input_size=51): self.num_classes = num_classes self.hidden_size = hidden_size self.input_size = input_size self.thresholds = { 'fall_velocity': 50.0, 'fall_aspect_ratio': 1.5, 'standing_height': 100.0, 'distress_duration': 3.0, 'conflict_distance': 50.0, } def classify(self, sequence: SkeletonSequence) -> PassengerEvent: """ 基于规则的分类器(基线方案) 实际系统中应使用训练好的 LSTM """ if len(sequence.skeletons) < 2: return PassengerEvent.NORMAL current = sequence.skeletons[-1] previous = sequence.skeletons[-2] com_curr = current.get_center_of_mass() com_prev = previous.get_center_of_mass() dt = current.timestamp - previous.timestamp if dt > 0: com_velocity_y = (com_curr[1] - com_prev[1]) / dt else: com_velocity_y = 0 if current.is_fallen(): return PassengerEvent.FALLEN elif com_velocity_y > self.thresholds['fall_velocity']: return PassengerEvent.FALLING elif current.is_standing(): return PassengerEvent.STANDING else: height = current.get_height() if 0 < height < self.thresholds['standing_height'] * 0.5: return PassengerEvent.ABNORMAL_POSTURE return PassengerEvent.NORMAL
class PrivacyAssessor: """ GDPR 隐私影响评估 对比骨架方案 vs 原始视频方案的数据暴露 """ @staticmethod def compare_data_exposure(): """对比数据暴露面""" print("=== 数据暴露对比 ===\n") video_data_per_frame = 1920 * 1080 * 3 video_fps = 15 video_per_second = video_data_per_frame * video_fps video_per_hour = video_per_second * 3600 print("原始视频方案:") print(f" 每帧数据: {video_data_per_frame/1e6:.1f} MB") print(f" 每秒数据: {video_per_second/1e6:.1f} MB/s") print(f" 每小时数据: {video_per_hour/1e9:.1f} GB/h") print(f" 包含: 面部特征、肤色、衣着、纹身、") print(f" 行李、阅读材料、手机屏幕内容") print(f" GDPR: 需要明确同意、存储限制、") print(f" 数据保护影响评估(DPIA)") print() skeleton_points = 17 * 3 skeleton_fps = 15 skeleton_per_second = skeleton_points * skeleton_fps skeleton_per_hour = skeleton_per_second * 3600 print("骨架数据方案:") print(f" 每帧数据: {skeleton_points*4} bytes") print(f" 每秒数据: {skeleton_per_second*4/1e3:.1f} KB/s") print(f" 每小时数据: {skeleton_per_hour*4/1e6:.1f} MB/h") print(f" 包含: 关节坐标、置信度") print(f" 不包含: 面部特征、肤色、衣着、") print(f" 物品、屏幕内容") print(f" GDPR: 数据最小化原则满足,") print(f" 无身份关联,DPIA 简化") ratio = video_per_hour / (skeleton_per_hour * 4) print(f"\n数据量缩减: {ratio:.0f}x")
if __name__ == "__main__": PrivacyAssessor.compare_data_exposure() print("\n" + "="*50) np.random.seed(42) normal_skeleton = Skeleton( keypoints=np.array([ [500, 200, 0.9], [490, 195, 0.8], [510, 195, 0.8], [480, 195, 0.7], [520, 195, 0.7], [450, 250, 0.9], [550, 250, 0.9], [430, 300, 0.8], [570, 300, 0.8], [420, 350, 0.7], [580, 350, 0.7], [460, 350, 0.9], [540, 350, 0.9], [450, 400, 0.8], [550, 400, 0.8], [440, 450, 0.7], [560, 450, 0.7], ], dtype=np.float32), timestamp=0.0 ) fallen_skeleton = Skeleton( keypoints=np.array([ [400, 350, 0.9], [410, 345, 0.8], [410, 355, 0.8], [420, 345, 0.7], [420, 355, 0.7], [430, 340, 0.9], [440, 360, 0.9], [500, 340, 0.8], [510, 360, 0.8], [550, 340, 0.7], [560, 360, 0.7], [600, 345, 0.9], [610, 355, 0.9], [650, 340, 0.8], [660, 360, 0.8], [700, 340, 0.7], [710, 360, 0.7], ], dtype=np.float32), timestamp=0.1 ) classifier = EventClassifier() seq = SkeletonSequence([ Skeleton(np.random.normal(0.8, 0.05, (17, 3)) * 500 + 400, 0.0), Skeleton(np.random.normal(0.8, 0.05, (17, 3)) * 500 + 400, 0.05), normal_skeleton, fallen_skeleton, ]) event = classifier.classify(seq) print(f"\n跌倒测试: 检测事件 = {event.name}") standing_skeleton = Skeleton( keypoints=np.array([ [500, 100, 0.9], [490, 95, 0.8], [510, 95, 0.8], [480, 95, 0.7], [520, 95, 0.7], [450, 150, 0.9], [550, 150, 0.9], [430, 220, 0.8], [570, 220, 0.8], [420, 290, 0.7], [580, 290, 0.7], [460, 300, 0.9], [540, 300, 0.9], [450, 380, 0.8], [550, 380, 0.8], [440, 450, 0.7], [560, 450, 0.7], ], dtype=np.float32), timestamp=0.0 ) print(f"站立检测: is_standing = {standing_skeleton.is_standing()}") print(f"倒地检测: is_fallen = {fallen_skeleton.is_fallen()}")
|