comfort.py 40 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951
  1. #!/usr/bin/env python
  2. # -*- coding: utf-8 -*-
  3. ##################################################################
  4. #
  5. # Copyright (c) 2023 CICV, Inc. All Rights Reserved
  6. #
  7. ##################################################################
  8. """
  9. @Authors: zhanghaiwen(zhanghaiwen@china-icv.cn), yangzihao(yangzihao@china-icv.cn)
  10. @Data: 2023/06/25
  11. @Last Modified: 2025/04/25
  12. @Summary: 舒适性指标计算模块
  13. """
  14. import scipy.signal
  15. import pandas as pd
  16. import numpy as np
  17. import os
  18. from pathlib import Path
  19. from typing import Dict, List, Any, Optional, Callable, Union, Tuple
  20. from modules.lib.score import Score
  21. from modules.lib.common import get_interpolation
  22. from modules.lib import data_process
  23. from modules.lib.log_manager import LogManager
  24. from modules.lib.chart_generator import generate_comfort_chart_data
  25. # ----------------------
  26. # 指标计算函数
  27. # ----------------------
  28. def calculate_zigzag(data_processed, plot_path) -> dict:
  29. """计算蛇行指标"""
  30. comfort = ComfortCalculator(data_processed)
  31. zigzag_count = comfort.calculate_zigzag_count(plot_path)
  32. return {"zigzag": float(zigzag_count)}
  33. def calculate_shake(data_processed, plot_path) -> dict:
  34. """计算晃动指标"""
  35. comfort = ComfortCalculator(data_processed)
  36. shake_count = comfort.calculate_shake_count(plot_path)
  37. return {"shake": float(shake_count)}
  38. def calculate_cadence(data_processed, plot_path) -> dict:
  39. """计算顿挫指标"""
  40. comfort = ComfortCalculator(data_processed)
  41. cadence_count = comfort.calculate_cadence_count(plot_path)
  42. return {"cadence": float(cadence_count)}
  43. def calculate_topbrake(data_processed, plot_path) -> dict:
  44. """计算点杀指标"""
  45. comfort = ComfortCalculator(data_processed)
  46. topBrake_count = comfort.calculate_top_brake_count(plot_path)
  47. return {"topBrake": float(topBrake_count)}
  48. def calculate_slambrake(data_processed, plot_path) -> dict:
  49. """计算急刹车指标"""
  50. comfort = ComfortCalculator(data_processed)
  51. slam_brake_count = comfort.calculate_slam_brake_count(plot_path)
  52. return {"slamBrake": float(slam_brake_count)}
  53. def calculate_slamaccelerate(data_processed, plot_path) -> dict:
  54. """计算急加速指标"""
  55. comfort = ComfortCalculator(data_processed)
  56. slam_accel_count = comfort.calculate_slam_accel_count(plot_path)
  57. return {"slamAccelerate": float(slam_accel_count)}
  58. def calculate_sampling_frequency(df, time_column='simTime', default_fs=25):
  59. """计算时间序列数据的采样频率"""
  60. if len(df) < 2 or time_column not in df.columns:
  61. return default_fs
  62. if not df[time_column].is_monotonic_increasing:
  63. df = df.sort_values(time_column)
  64. time_diffs = df[time_column].diff().dropna()
  65. if time_diffs.empty or (time_diffs <= 0).any():
  66. return default_fs
  67. median_time_diff = time_diffs.median()
  68. return 1.0 / median_time_diff if median_time_diff > 0 else default_fs
  69. # ----------------------
  70. # 注册器与管理类
  71. # ----------------------
  72. class ComfortRegistry:
  73. """舒适性指标注册器"""
  74. def __init__(self, data_processed, plot_path):
  75. self.logger = LogManager().get_logger()
  76. self.data = data_processed
  77. self.output_dir = plot_path
  78. if not hasattr(data_processed, 'comfort_config') or not data_processed.comfort_config:
  79. self.logger.warning("舒适性配置为空,跳过舒适性指标计算")
  80. self.comfort_config = {}
  81. self.metrics = []
  82. self._registry = {}
  83. return
  84. self.comfort_config = data_processed.comfort_config.get("comfort", {})
  85. self.metrics = self._extract_metrics(self.comfort_config)
  86. self._registry = self._build_registry()
  87. def _extract_metrics(self, config_node: dict) -> list:
  88. """DFS遍历提取指标"""
  89. metrics = []
  90. def _recurse(node):
  91. if isinstance(node, dict):
  92. if 'name' in node and not any(isinstance(v, dict) for v in node.values()):
  93. metrics.append(node['name'])
  94. for v in node.values():
  95. _recurse(v)
  96. _recurse(config_node)
  97. return metrics
  98. def _build_registry(self) -> dict:
  99. """自动注册指标函数"""
  100. registry = {}
  101. for metric_name in self.metrics:
  102. func_name = f"calculate_{metric_name.lower()}"
  103. try:
  104. registry[metric_name] = globals()[func_name]
  105. except KeyError:
  106. self.logger.error(f"未实现指标函数: {func_name}")
  107. return registry
  108. def batch_execute(self) -> dict:
  109. """批量执行指标计算"""
  110. results = {}
  111. for name, func in self._registry.items():
  112. try:
  113. result = func(self.data, self.output_dir)
  114. results.update(result)
  115. except Exception as e:
  116. self.logger.error(f"{name} 执行失败: {str(e)}", exc_info=True)
  117. results[name] = None
  118. return results
  119. class ComfortManager:
  120. """舒适性指标计算主类"""
  121. def __init__(self, data_processed, plot_path):
  122. self.data = data_processed
  123. self.logger = LogManager().get_logger()
  124. self.plot_path = plot_path
  125. if not hasattr(data_processed, 'comfort_config') or not data_processed.comfort_config:
  126. self.logger.warning("舒适性配置为空,跳过舒适性指标计算初始化")
  127. self.registry = None
  128. else:
  129. self.registry = ComfortRegistry(self.data, self.plot_path)
  130. def report_statistic(self):
  131. """生成舒适性评分报告"""
  132. if self.registry is None:
  133. self.logger.info("舒适性指标管理器未初始化,返回空结果")
  134. return {}
  135. return self.registry.batch_execute()
  136. # ----------------------
  137. # 舒适性计算核心类
  138. # ----------------------
  139. class ComfortCalculator:
  140. """舒适性指标计算核心类"""
  141. def __init__(self, data_processed):
  142. self.data_processed = data_processed
  143. self.logger = LogManager().get_logger()
  144. self.data = data_processed.ego_data
  145. self.ego_df = pd.DataFrame()
  146. self.discomfort_df = pd.DataFrame(columns=['start_time', 'end_time', 'start_frame', 'end_frame', 'type'])
  147. self.COMFORT_INFO = [
  148. "simTime", "simFrame", "speedX", "speedY", "accelX", "accelY",
  149. "curvHor", "lightMask", "v", "speedH", "accelH", "posH",
  150. "lon_v_vehicle", "lat_v_vehicle", "lat_acc_vehicle",
  151. "lon_acc_vehicle", "lat_acc_rate", "lon_acc_rate"
  152. ]
  153. self.calculated_value = {
  154. 'zigzag': 0, 'shake': 0, 'cadence': 0,
  155. 'topBrake': 0, 'slamBrake': 0, 'slamAccelerate': 0
  156. }
  157. self._initialize_data()
  158. self.fs = calculate_sampling_frequency(self.ego_df)
  159. self.logger.info(f"采样频率: {self.fs} Hz")
  160. self.zigzag_count = 0
  161. self.shake_count = 0
  162. self.cadence_count = 0
  163. self.slam_brake_count = 0
  164. self.slam_accel_count = 0
  165. self.zigzag_time_list = []
  166. self.zigzag_stre_list = []
  167. self.shake_events = []
  168. def _initialize_data(self):
  169. """初始化数据"""
  170. self.ego_df = self.data[self.COMFORT_INFO].copy()
  171. self.df = self.ego_df.reset_index(drop=True)
  172. self._prepare_comfort_parameters()
  173. def _prepare_comfort_parameters(self):
  174. """准备舒适性计算所需参数"""
  175. speed_field = 'lon_v_vehicle'
  176. self.ego_df['ip_acc'] = self.ego_df[speed_field].apply(get_interpolation, point1=[18, 4], point2=[72, 2])
  177. self.ego_df['ip_dec'] = self.ego_df[speed_field].apply(get_interpolation, point1=[18, -5], point2=[72, -3.5])
  178. acc_field = 'lon_acc_vehicle'
  179. self.ego_df['slam_brake'] = (self.ego_df[acc_field] - self.ego_df['ip_dec']).apply(
  180. lambda x: 1 if x < 0 else 0)
  181. self.ego_df['slam_accel'] = (self.ego_df[acc_field] - self.ego_df['ip_acc']).apply(
  182. lambda x: 1 if x > 0 else 0)
  183. # ----------------------
  184. # 指标计算公共接口
  185. # ----------------------
  186. def calculate_zigzag_count(self, plot_path):
  187. """计算蛇行指标"""
  188. zigzag_events = self._detect_zigzag_events_new()
  189. self.log_events('zigzag', zigzag_events)
  190. self.generate_metric_chart('zigzag', plot_path)
  191. return len(zigzag_events)
  192. def calculate_shake_count(self, plot_path):
  193. """计算晃动指标"""
  194. shake_events = self._shake_detector()
  195. self.log_events('shake', shake_events)
  196. self.generate_metric_chart('shake', plot_path)
  197. return len(shake_events)
  198. def calculate_cadence_count(self, plot_path):
  199. """计算顿挫指标"""
  200. cadence_events = self._cadence_detector()
  201. self.log_events('cadence', cadence_events)
  202. self.generate_metric_chart('cadence', plot_path)
  203. return len(cadence_events)
  204. def calculate_top_brake_count(self, plot_path):
  205. """计算点杀指标"""
  206. top_brake_events = self._top_brake_detector()
  207. self.log_events('topBrake', top_brake_events)
  208. return len(top_brake_events)
  209. def calculate_slam_brake_count(self, plot_path):
  210. """计算急刹车指标"""
  211. slam_brake_events = self._slam_brake_detector()
  212. self.log_events('slamBrake', slam_brake_events)
  213. self.generate_metric_chart('slamBrake', plot_path)
  214. return len(slam_brake_events)
  215. def calculate_slam_accel_count(self, plot_path):
  216. """计算急加速指标"""
  217. slam_accel_events = self._slam_accel_detector()
  218. self.log_events('slamAccelerate', slam_accel_events)
  219. self.generate_metric_chart('slamaccelerate', plot_path)
  220. return len(slam_accel_events)
  221. # ----------------------
  222. # 事件检测核心方法
  223. # ----------------------
  224. def _detect_zigzag_events_new(self, window_size=10.0, min_zcr=2,
  225. min_theta_range=5.0, max_theta_range=30.0):
  226. """检测画龙事件"""
  227. df = self.ego_df.copy()
  228. if 'speedH' not in df.columns or 'posH' not in df.columns:
  229. self.logger.warning("缺少航向角速度或航向角度数据,无法进行画龙检测")
  230. return []
  231. df = df.sort_values('simTime')
  232. df['time_diff'] = df['simTime'].diff().fillna(0)
  233. fs = self.fs
  234. df['theta'] = df['posH']
  235. df['omega'] = df['speedH']
  236. if 'v' in df.columns:
  237. df['speed_kmh'] = df['v'] * 3.6
  238. else:
  239. df['speed_kmh'] = 0.0
  240. if len(df) > 10:
  241. b, a = scipy.signal.butter(2, 2 / (fs / 2), btype='low')
  242. df['theta_filtered'] = scipy.signal.filtfilt(b, a, df['theta'])
  243. df['omega_filtered'] = scipy.signal.filtfilt(b, a, df['omega'])
  244. else:
  245. df['theta_filtered'] = df['theta']
  246. df['omega_filtered'] = df['omega']
  247. df['theta_diff'] = df['theta_filtered'].diff().fillna(0)
  248. df['theta_diff2'] = df['theta_diff'].diff().fillna(0)
  249. def get_min_omega_threshold(speed_kmh):
  250. if speed_kmh < 10: return 10.0
  251. elif speed_kmh < 30: return 6.0
  252. elif speed_kmh < 60: return 4.0
  253. elif speed_kmh < 90: return 3.0
  254. else: return 2.5
  255. window_points = int(window_size * fs)
  256. if window_points < 5: window_points = 5
  257. zigzag_events = []
  258. current_event = None
  259. for i in range(window_points, len(df)):
  260. window_data = df.iloc[i - window_points:i]
  261. omega_sign = np.sign(window_data['omega_filtered'])
  262. sign_changes = np.sum(np.abs(np.diff(omega_sign)) > 1.5)
  263. if sign_changes == 0: continue
  264. avg_speed_kmh = window_data['speed_kmh'].mean()
  265. dynamic_min_omega = get_min_omega_threshold(avg_speed_kmh)
  266. omega_over_threshold = np.any(np.abs(window_data['omega_filtered']) > dynamic_min_omega)
  267. theta_range = np.max(window_data['theta_filtered']) - np.min(window_data['theta_filtered'])
  268. theta_diff_sum = window_data['theta_diff'].sum()
  269. theta_diff_abs_sum = np.abs(window_data['theta_diff']).sum()
  270. direction_consistency = np.abs(theta_diff_sum) / theta_diff_abs_sum if theta_diff_abs_sum > 0 else 0
  271. theta_diff2_sign = np.sign(window_data['theta_diff2'])
  272. theta_diff2_sign_changes = np.sum(np.abs(np.diff(theta_diff2_sign)) > 1.5)
  273. is_likely_turn = direction_consistency > 0.7 and theta_range > 10.0
  274. has_zigzag_pattern = direction_consistency < 0.5 and theta_diff2_sign_changes >= 3
  275. is_zigzag = (
  276. sign_changes >= min_zcr and
  277. omega_over_threshold and
  278. min_theta_range <= theta_range <= max_theta_range and
  279. not is_likely_turn and
  280. has_zigzag_pattern
  281. )
  282. current_time = df.iloc[i]['simTime']
  283. if is_zigzag and current_event is None:
  284. current_event = {
  285. 'start_time': current_time - window_size,
  286. 'start_frame': df.iloc[i - window_points]['simFrame'],
  287. 'end_time': current_time,
  288. 'end_frame': df.iloc[i]['simFrame'],
  289. }
  290. elif is_zigzag and current_event is not None:
  291. current_event['end_time'] = current_time
  292. current_event['end_frame'] = df.iloc[i]['simFrame']
  293. elif not is_zigzag and current_event is not None:
  294. duration = current_event['end_time'] - current_event['start_time']
  295. if duration >= window_size:
  296. zigzag_events.append(current_event)
  297. self._add_event_to_df(current_event, 'zigzag')
  298. current_event = None
  299. if current_event is not None:
  300. duration = current_event['end_time'] - current_event['start_time']
  301. if duration >= window_size:
  302. zigzag_events.append(current_event)
  303. self._add_event_to_df(current_event, 'zigzag')
  304. self.zigzag_count = len(zigzag_events)
  305. return zigzag_events
  306. def _shake_detector(self, T_diff=0.5):
  307. """检测晃动事件"""
  308. df = self.ego_df.copy()
  309. if 'lat_acc_vehicle' not in df.columns:
  310. self.logger.warning("缺少计算晃动指标所需的数据列")
  311. return []
  312. window_size = 25
  313. df['speedH_std'] = df['speedH'].rolling(window=window_size, min_periods=2).std()
  314. v0 = 20 * 5 / 18
  315. k = 0.008 * 3.6
  316. df['lat_acc_threshold'] = df['v'].apply(
  317. lambda speed: max(1.0, min(1.8, 1.8 - k * (speed - v0))
  318. ))
  319. df['speedH_threshold'] = df['v'].apply(
  320. lambda speed: max(1.5, min(3.0, 2.0 * (1 + (speed - 20) / 60))
  321. ))
  322. condition_A = df['lat_acc_vehicle'].abs() > df['lat_acc_threshold']
  323. condition_B = df['lat_acc_rate'].abs() > 0.5
  324. condition_C = (df['speedH_std'] > df['speedH_threshold'])
  325. shake_condition = condition_A & (condition_B | condition_C)
  326. event_groups = (shake_condition != shake_condition.shift()).cumsum()
  327. shake_events = []
  328. for _, group in df[shake_condition].groupby(event_groups):
  329. if len(group) >= 10:
  330. start_time = group['simTime'].iloc[0]
  331. end_time = group['simTime'].iloc[-1]
  332. duration = end_time - start_time
  333. if duration >= T_diff:
  334. shake_events.append({
  335. 'start_time': start_time,
  336. 'end_time': end_time,
  337. 'start_frame': group['simFrame'].iloc[0],
  338. 'end_frame': group['simFrame'].iloc[-1],
  339. })
  340. self._add_event_to_df({
  341. 'start_time': start_time,
  342. 'end_time': end_time,
  343. 'start_frame': group['simFrame'].iloc[0],
  344. 'end_frame': group['simFrame'].iloc[-1],
  345. }, 'shake')
  346. self.shake_count = len(shake_events)
  347. self.ego_df = df.copy()
  348. self.shake_events = shake_events
  349. return shake_events
  350. def _cadence_detector(self):
  351. """检测顿挫事件 - 通过连续的加减速动作来识别顿挫。
  352. 要求:
  353. 1. 连续加减速动作间隔不超过2个采样周期
  354. 2. 每组连续加减速动作至少要有3个及以上
  355. 3. 两个顿挫事件之间的时间间隔至少要1秒以上
  356. """
  357. # import matplotlib.pyplot as plt
  358. # import matplotlib.dates as mdates
  359. # from matplotlib.patches import Rectangle
  360. df = self.ego_df.copy()
  361. required_fields = ['simTime', 'simFrame', 'lon_v_vehicle', 'lon_acc_vehicle', 'lon_acc_rate']
  362. if not all(field in df.columns for field in required_fields):
  363. missing_fields = [field for field in required_fields if field not in df.columns]
  364. self.logger.warning(f"顿挫检测缺少必要字段: {missing_fields}")
  365. return []
  366. # 确保数据按时间排序
  367. df = df.sort_values('simTime')
  368. if len(df) < 10:
  369. return []
  370. # 计算采样频率和周期
  371. fs = self.fs
  372. sample_period = 1.0 / fs
  373. max_interval = 2 * sample_period # 最大允许间隔为2个采样周期
  374. min_event_interval = 1.0 # 两个顿挫事件之间的最小间隔(秒)
  375. # 数据预处理和平滑
  376. if len(df) > 10:
  377. # b, a = scipy.signal.butter(2, 2 / (fs / 2), btype='low')
  378. # df['acc_filtered'] = scipy.signal.filtfilt(b, a, df['lon_acc_vehicle'])
  379. # ============ 双通道滤波 ============
  380. cutoff = 5 # Hz,具体可根据采样频率和车辆动力特性调整
  381. b, a = scipy.signal.butter(2, cutoff / (fs / 2), btype='low')
  382. df['acc_filtered'] = scipy.signal.filtfilt(b, a, df['lon_acc_vehicle'])
  383. else:
  384. df['acc_filtered'] = df['lon_acc_vehicle']
  385. # 检测加速度方向变化
  386. df['acc_direction'] = np.sign(df['acc_filtered'])
  387. df['acc_direction_change'] = (df['acc_direction'] != df['acc_direction'].shift(1)).astype(int)
  388. # 设置阈值
  389. acc_threshold = 0.05 # 加速度阈值
  390. min_changes = 3 # 最小连续变化次数
  391. # 检测顿挫事件
  392. cadence_events = []
  393. current_changes = []
  394. last_event_end = df['simTime'].iloc[0] - min_event_interval # 初始化为第一个时间点减去间隔
  395. for i in range(1, len(df)):
  396. current_time = df.iloc[i]['simTime']
  397. current_frame = df.iloc[i]['simFrame']
  398. if df.iloc[i]['acc_direction_change'] and abs(df.iloc[i]['acc_filtered']) > acc_threshold:
  399. if not current_changes or (current_time - current_changes[-1]['time'] <= max_interval):
  400. current_changes.append({
  401. 'time': current_time,
  402. 'frame': current_frame
  403. })
  404. else:
  405. # 如果间隔过大,检查之前的变化是否构成顿挫事件
  406. if len(current_changes) >= min_changes:
  407. start_time = current_changes[0]['time']
  408. end_time = current_changes[-1]['time']
  409. # 检查与上一个事件的时间间隔
  410. if start_time - last_event_end >= min_event_interval:
  411. event = {
  412. 'start_time': start_time,
  413. 'start_frame': current_changes[0]['frame'],
  414. 'end_time': end_time,
  415. 'end_frame': current_changes[-1]['frame']
  416. }
  417. cadence_events.append(event)
  418. self._add_event_to_df(event, 'cadence')
  419. last_event_end = end_time
  420. # 开始新的变化序列
  421. current_changes = [{
  422. 'time': current_time,
  423. 'frame': current_frame
  424. }]
  425. # 处理最后一组变化
  426. if len(current_changes) >= min_changes:
  427. start_time = current_changes[0]['time']
  428. end_time = current_changes[-1]['time']
  429. if start_time - last_event_end >= min_event_interval:
  430. event = {
  431. 'start_time': start_time,
  432. 'start_frame': current_changes[0]['frame'],
  433. 'end_time': end_time,
  434. 'end_frame': current_changes[-1]['frame']
  435. }
  436. cadence_events.append(event)
  437. self._add_event_to_df(event, 'cadence')
  438. self.cadence_count = len(cadence_events)
  439. # # ======================== New plotting code added ========================
  440. # if len(df) > 0:
  441. # try:
  442. # # Create figure and axes
  443. # plt.figure(figsize=(15, 8))
  444. # # Plot original and filtered acceleration
  445. # plt.plot(df['simTime'], df['lon_acc_vehicle'],
  446. # label='Original Acceleration', alpha=0.6, color='blue')
  447. # plt.plot(df['simTime'], df['acc_filtered'],
  448. # label='Filtered Acceleration', linewidth=2, color='red')
  449. # # Mark direction change points
  450. # change_points = df[df['acc_direction_change'] == 1]
  451. # plt.scatter(change_points['simTime'], change_points['acc_filtered'],
  452. # color='green', s=50, zorder=5, label='Direction Change Points')
  453. # # Mark points exceeding threshold
  454. # threshold_points = df[abs(df['acc_filtered']) > acc_threshold]
  455. # plt.scatter(threshold_points['simTime'], threshold_points['acc_filtered'],
  456. # color='purple', s=20, zorder=4, alpha=0.5, label='Exceeds Threshold Points')
  457. # # Plot event regions
  458. # for event in cadence_events:
  459. # start = event['start_time']
  460. # end = event['end_time']
  461. # plt.axvspan(start, end, alpha=0.3, color='orange', label='cadence_events' if 'cadence' not in plt.gca().get_legend_handles_labels()[1] else "")
  462. # # Add event labels
  463. # mid_time = start + (end - start) / 2
  464. # plt.text(mid_time, plt.ylim()[1]*0.9, f'Cadence {len(cadence_events.index(event))+1}',
  465. # ha='center', fontsize=10, bbox=dict(facecolor='white', alpha=0.8))
  466. # # Add threshold lines
  467. # plt.axhline(y=acc_threshold, color='gray', linestyle='--', alpha=0.7)
  468. # plt.axhline(y=-acc_threshold, color='gray', linestyle='--', alpha=0.7)
  469. # plt.text(df['simTime'].iloc[-1], acc_threshold+0.05, f'Threshold: {acc_threshold} m/s²',
  470. # ha='right', color='gray')
  471. # # Set legend and title
  472. # plt.title(f'Cadence Event Detection (Total Events Detected: {len(cadence_events)})')
  473. # plt.xlabel('Time (s)')
  474. # plt.ylabel('Longitudinal Acceleration (m/s²)')
  475. # plt.legend(loc='upper right')
  476. # plt.grid(True, linestyle='--', alpha=0.6)
  477. # # Auto-adjust time axis format
  478. # if (df['simTime'].max() - df['simTime'].min()) > 60:
  479. # plt.gca().xaxis.set_major_formatter(mdates.DateFormatter('%H:%M:%S'))
  480. # else:
  481. # plt.gca().xaxis.set_major_formatter(plt.FuncFormatter(lambda x, _: f'{x:.1f}s'))
  482. # plt.tight_layout()
  483. # # Save plot
  484. # plot_path = f"./cadence_detection.png"
  485. # plt.savefig(plot_path, dpi=150)
  486. # plt.close()
  487. # self.logger.info(f"Cadence detection results saved to: {plot_path}")
  488. # except Exception as e:
  489. # self.logger.error(f"Error plotting cadence detection graph: {str(e)}")
  490. # # ======================== End of plotting code ========================
  491. return cadence_events
  492. def _top_brake_detector(self):
  493. """
  494. Point Brake Detector (Top Brake) - Waveform analysis based on jerk signal
  495. This function identifies "point brake" events, defined as independent, sudden braking actions
  496. with intensity between normal braking and emergency braking. It distinguishes from continuous
  497. acceleration/deceleration "cadence" phenomena by analyzing individual complete waveforms in
  498. the jerk signal.
  499. Point Brake Characteristics:
  500. 1. Independent event: Starts and ends in a relatively stable state (acceleration near zero).
  501. 2. Braking intensity: Deceleration stronger than normal braking but weaker than emergency braking.
  502. 3. Suddenness: Peak jerk must be large enough to indicate rapid acceleration change.
  503. 4. Duration: Reasonable duration (e.g., 0.2 to 1.5 seconds).
  504. """
  505. # import matplotlib.pyplot as plt
  506. # from matplotlib.patches import Rectangle
  507. # import matplotlib.dates as mdates
  508. # 1. Data Preparation & Checks
  509. df = self.ego_df.copy()
  510. required_cols = ['simTime', 'simFrame', 'lon_acc_rate', 'lon_acc_vehicle', 'v', 'ip_dec']
  511. if any(col not in df.columns for col in required_cols):
  512. missing = [c for c in required_cols if c not in df.columns]
  513. self.logger.warning(f"Point brake detection missing required fields: {missing}")
  514. return []
  515. fs = self.fs
  516. if len(df) < fs: # Need at least 1 second of data
  517. return []
  518. # Define Key Thresholds
  519. NORMAL_BRAKE_THRESHOLD = -1.2 # m/s^2
  520. MIN_PEAK_JERK = 3.0 # m/s^3
  521. MERGE_TIME_GAP_THRESHOLD = (1/fs)*2 # seconds
  522. # 2. Smooth Jerk Signal
  523. window_length = max(5, int(fs * 0.2) // 2 * 2 + 1) # Must be odd
  524. if len(df) > window_length:
  525. df['jerk_filtered'] = scipy.signal.savgol_filter(df['lon_acc_rate'], window_length, 2)
  526. else:
  527. df['jerk_filtered'] = df['lon_acc_rate']
  528. # 3. Find Jerk Zero-Crossings
  529. df['jerk_sign'] = np.sign(df['jerk_filtered'])
  530. zero_crossings = df.index[df['jerk_sign'].ne(df['jerk_sign'].shift(1)) & df['jerk_sign'].ne(0)].tolist()
  531. if len(zero_crossings) < 2:
  532. return []
  533. # 4. Identify Waveforms and Validate Events
  534. top_brake_events = []
  535. for i in range(len(zero_crossings) - 1):
  536. start_idx = zero_crossings[i]
  537. end_idx = zero_crossings[i+1]
  538. event_slice = df.loc[start_idx:end_idx]
  539. if len(event_slice) < 2:
  540. continue
  541. # a. Duration Validation
  542. duration = event_slice['simTime'].iloc[-1] - event_slice['simTime'].iloc[0]
  543. if not ((1/fs)*3 <= duration <= 1.5):
  544. continue
  545. min_acc = event_slice['lon_acc_vehicle'].min()
  546. emergency_brake_threshold = df.loc[start_idx, 'ip_dec']
  547. # b. Braking Intensity Validation
  548. is_top_brake_strength = min_acc < NORMAL_BRAKE_THRESHOLD and min_acc > emergency_brake_threshold
  549. if not is_top_brake_strength:
  550. continue
  551. # c. Waveform Amplitude Validation
  552. peak_jerk = event_slice['jerk_filtered'].abs().max()
  553. if peak_jerk < MIN_PEAK_JERK:
  554. continue
  555. # d. Isolated Event Validation
  556. acc_at_start = df.loc[start_idx, 'lon_acc_vehicle']
  557. acc_at_end = df.loc[end_idx, 'lon_acc_vehicle']
  558. if not (abs(acc_at_start) < 0.5 or abs(acc_at_end) < 0.5):
  559. continue
  560. # 5. Create Valid Event
  561. event = {
  562. 'start_time': df.loc[start_idx, 'simTime'],
  563. 'end_time': df.loc[end_idx, 'simTime'],
  564. 'start_frame': df.loc[start_idx, 'simFrame'],
  565. 'end_frame': df.loc[end_idx, 'simFrame'],
  566. 'peak_jerk': peak_jerk,
  567. 'min_acc': min_acc
  568. }
  569. top_brake_events.append(event)
  570. # 6. Post-processing: Merge very close events
  571. if not top_brake_events:
  572. return []
  573. merged_events = []
  574. current_event = top_brake_events[0]
  575. for next_event in top_brake_events[1:]:
  576. time_gap = next_event['start_time'] - current_event['end_time']
  577. if time_gap < MERGE_TIME_GAP_THRESHOLD:
  578. current_event['end_time'] = next_event['end_time']
  579. current_event['end_frame'] = next_event['end_frame']
  580. current_event['peak_jerk'] = max(current_event['peak_jerk'], next_event['peak_jerk'])
  581. current_event['min_acc'] = min(current_event['min_acc'], next_event['min_acc'])
  582. else:
  583. merged_events.append(current_event)
  584. current_event = next_event
  585. merged_events.append(current_event)
  586. for event in merged_events:
  587. self._add_event_to_df(event, 'topBrake_merged')
  588. # # ===================== Visualization =====================
  589. # try:
  590. # plt.figure(figsize=(14, 10))
  591. # plt.suptitle(f'Point Brake Detection Analysis (Detected: {len(merged_events)} events)', fontsize=16)
  592. # # 1. Jerk Analysis
  593. # ax1 = plt.subplot(3, 1, 1)
  594. # plt.plot(df['simTime'], df['lon_acc_rate'], 'b-', alpha=0.5, label='Raw Jerk')
  595. # plt.plot(df['simTime'], df['jerk_filtered'], 'r-', label='Filtered Jerk', linewidth=1.5)
  596. # # Mark zero crossings
  597. # zero_points = df.loc[zero_crossings]
  598. # plt.scatter(zero_points['simTime'], zero_points['jerk_filtered'],
  599. # c='green', s=40, marker='o', label='Zero Crossings', zorder=5)
  600. # # Threshold lines
  601. # plt.axhline(y=MIN_PEAK_JERK, color='gray', linestyle='--', alpha=0.7)
  602. # plt.axhline(y=-MIN_PEAK_JERK, color='gray', linestyle='--', alpha=0.7)
  603. # plt.text(df['simTime'].iloc[-1], MIN_PEAK_JERK+0.2, f'Jerk Threshold: ±{MIN_PEAK_JERK} m/s³',
  604. # ha='right', color='gray')
  605. # plt.ylabel('Jerk (m/s³)')
  606. # plt.title('Jerk Signal Analysis')
  607. # plt.grid(True, linestyle='--', alpha=0.6)
  608. # plt.legend(loc='upper right')
  609. # # 2. Acceleration Analysis
  610. # ax2 = plt.subplot(3, 1, 2, sharex=ax1)
  611. # plt.plot(df['simTime'], df['lon_acc_vehicle'], 'g-', label='Longitudinal Acceleration')
  612. # # Threshold lines
  613. # plt.axhline(y=0, color='k', linestyle='-', alpha=0.5)
  614. # plt.axhline(y=NORMAL_BRAKE_THRESHOLD, color='orange', linestyle='--', alpha=0.8)
  615. # # Mark emergency brake threshold (varies over time)
  616. # plt.plot(df['simTime'], df['ip_dec'], 'm--', label='Emergency Brake Threshold')
  617. # plt.ylabel('Acceleration (m/s²)')
  618. # plt.title('Acceleration Analysis')
  619. # plt.grid(True, linestyle='--', alpha=0.6)
  620. # plt.legend(loc='upper right')
  621. # # 3. Velocity Analysis
  622. # ax3 = plt.subplot(3, 1, 3, sharex=ax1)
  623. # plt.plot(df['simTime'], df['v'], 'b-', label='Vehicle Speed')
  624. # plt.ylabel('Speed (m/s)')
  625. # plt.xlabel('Time (s)')
  626. # plt.title('Vehicle Speed')
  627. # plt.grid(True, linestyle='--', alpha=0.6)
  628. # # Highlight detected events across all subplots
  629. # for event in merged_events:
  630. # start = event['start_time']
  631. # end = event['end_time']
  632. # duration = end - start
  633. # # Add shaded region
  634. # for ax in [ax1, ax2, ax3]:
  635. # ax.axvspan(start, end, alpha=0.2, color='red')
  636. # # Add event info on acceleration plot
  637. # ax2.text((start+end)/2, event['min_acc']-0.2,
  638. # f"Min Acc: {event['min_acc']:.2f} m/s²\nPeak Jerk: {event['peak_jerk']:.2f} m/s³",
  639. # ha='center', va='top', fontsize=9,
  640. # bbox=dict(facecolor='white', alpha=0.8, edgecolor='red'))
  641. # # Add event info on speed plot
  642. # ax3.text((start+end)/2, df['v'].min() + 0.1*(df['v'].max()-df['v'].min()),
  643. # f"Point Brake\n{duration:.2f}s",
  644. # ha='center', fontsize=10, color='red',
  645. # bbox=dict(facecolor='white', alpha=0.8))
  646. # # Format x-axis for time
  647. # if (df['simTime'].max() - df['simTime'].min()) > 60:
  648. # plt.gca().xaxis.set_major_formatter(mdates.DateFormatter('%H:%M:%S'))
  649. # else:
  650. # plt.gca().xaxis.set_major_formatter(plt.FuncFormatter(lambda x, _: f'{x:.1f}s'))
  651. # plt.tight_layout(rect=[0, 0, 1, 0.96]) # Make room for suptitle
  652. # # Save the plot
  653. # plot_path = f"./top_brake_analysis.png"
  654. # plt.savefig(plot_path, dpi=150)
  655. # plt.close()
  656. # self.logger.info(f"Point brake analysis plot saved to: {plot_path}")
  657. # except Exception as e:
  658. # self.logger.error(f"Error generating point brake analysis plot: {str(e)}")
  659. # # ===================== End Visualization =====================
  660. return merged_events
  661. def _slam_brake_detector(self):
  662. """检测急刹车事件"""
  663. df = self.ego_df.copy()
  664. if 'slam_brake' not in df.columns:
  665. self.logger.warning("缺少计算急刹车指标所需的数据列")
  666. return
  667. min_duration = 0.5
  668. slam_brake_events = []
  669. in_event = False
  670. start_idx = 0
  671. for i, row in df.iterrows():
  672. if row['slam_brake'] == 1 and not in_event:
  673. in_event = True
  674. start_idx = i
  675. elif row['slam_brake'] == 0 and in_event:
  676. in_event = False
  677. end_idx = i - 1
  678. start_time = df.loc[start_idx, 'simTime']
  679. end_time = df.loc[end_idx, 'simTime']
  680. duration = end_time - start_time
  681. if duration >= min_duration:
  682. slam_brake_events.append({
  683. 'start_time': start_time,
  684. 'end_time': end_time,
  685. 'start_frame': df.loc[start_idx, 'simFrame'],
  686. 'end_frame': df.loc[end_idx, 'simFrame'],
  687. })
  688. self._add_event_to_df({
  689. 'start_time': start_time,
  690. 'end_time': end_time,
  691. 'start_frame': df.loc[start_idx, 'simFrame'],
  692. 'end_frame': df.loc[end_idx, 'simFrame'],
  693. }, 'slam_brake')
  694. if in_event:
  695. end_idx = len(df) - 1
  696. start_time = df.loc[start_idx, 'simTime']
  697. end_time = df.loc[end_idx, 'simTime']
  698. duration = end_time - start_time
  699. if duration >= min_duration:
  700. slam_brake_events.append({
  701. 'start_time': start_time,
  702. 'end_time': end_time,
  703. 'start_frame': df.loc[start_idx, 'simFrame'],
  704. 'end_frame': df.loc[end_idx, 'simFrame'],
  705. })
  706. self._add_event_to_df({
  707. 'start_time': start_time,
  708. 'end_time': end_time,
  709. 'start_frame': df.loc[start_idx, 'simFrame'],
  710. 'end_frame': df.loc[end_idx, 'simFrame'],
  711. }, 'slam_brake')
  712. self.slam_brake_count = len(slam_brake_events)
  713. return slam_brake_events
  714. def _slam_accel_detector(self):
  715. """检测急加速事件"""
  716. df = self.ego_df.copy()
  717. if 'slam_accel' not in df.columns:
  718. self.logger.warning("缺少计算急加速指标所需的数据列")
  719. return
  720. min_duration = 0.5
  721. slam_accel_events = []
  722. in_event = False
  723. start_idx = 0
  724. for i, row in df.iterrows():
  725. if row['slam_accel'] == 1 and not in_event:
  726. in_event = True
  727. start_idx = i
  728. elif row['slam_accel'] == 0 and in_event:
  729. in_event = False
  730. end_idx = i - 1
  731. start_time = df.loc[start_idx, 'simTime']
  732. end_time = df.loc[end_idx, 'simTime']
  733. duration = end_time - start_time
  734. if duration >= min_duration:
  735. slam_accel_events.append({
  736. 'start_time': start_time,
  737. 'end_time': end_time,
  738. 'start_frame': df.loc[start_idx, 'simFrame'],
  739. 'end_frame': df.loc[end_idx, 'simFrame'],
  740. })
  741. self._add_event_to_df({
  742. 'start_time': start_time,
  743. 'end_time': end_time,
  744. 'start_frame': df.loc[start_idx, 'simFrame'],
  745. 'end_frame': df.loc[end_idx, 'simFrame'],
  746. }, 'slam_accel')
  747. if in_event:
  748. end_idx = len(df) - 1
  749. start_time = df.loc[start_idx, 'simTime']
  750. end_time = df.loc[end_idx, 'simTime']
  751. duration = end_time - start_time
  752. if duration >= min_duration:
  753. slam_accel_events.append({
  754. 'start_time': start_time,
  755. 'end_time': end_time,
  756. 'start_frame': df.loc[start_idx, 'simFrame'],
  757. 'end_frame': df.loc[end_idx, 'simFrame'],
  758. })
  759. self._add_event_to_df({
  760. 'start_time': start_time,
  761. 'end_time': end_time,
  762. 'start_frame': df.loc[start_idx, 'simFrame'],
  763. 'end_frame': df.loc[end_idx, 'simFrame'],
  764. }, 'slam_accel')
  765. self.slam_accel_count = len(slam_accel_events)
  766. return slam_accel_events
  767. # ----------------------
  768. # 辅助方法
  769. # ----------------------
  770. def _add_event_to_df(self, event, event_type):
  771. """添加事件到数据框"""
  772. new_row = pd.DataFrame([{
  773. 'start_time': event['start_time'],
  774. 'end_time': event['end_time'],
  775. 'start_frame': event['start_frame'],
  776. 'end_frame': event['end_frame'],
  777. 'type': event_type
  778. }])
  779. self.discomfort_df = pd.concat([self.discomfort_df, new_row], ignore_index=True)
  780. def log_events(self, metric_name: str, events: list):
  781. """记录指标事件到日志"""
  782. if not events:
  783. self.logger.info(f"未检测到 {metric_name} 事件")
  784. return
  785. self.logger.info(f"检测到 {len(events)} 个 {metric_name} 事件:")
  786. for i, event in enumerate(events):
  787. duration = event.get('end_time', 0) - event.get('start_time', 0)
  788. self.logger.info(
  789. f"{metric_name} 事件 #{i+1}: "
  790. f"开始时间={event.get('start_time', 'N/A'):.2f}s, "
  791. f"结束时间={event.get('end_time', 'N/A'):.2f}s, "
  792. f"持续时间={duration:.2f}s, "
  793. f"开始帧={event.get('start_frame', 'N/A')}, "
  794. f"结束帧={event.get('end_frame', 'N/A')}"
  795. )
  796. def generate_metric_chart(self, metric_name: str, plot_path: Path) -> None:
  797. """生成指标图表"""
  798. if not plot_path:
  799. plot_path = os.path.join(os.getcwd(), 'data')
  800. os.makedirs(plot_path, exist_ok=True)
  801. chart_path = generate_comfort_chart_data(self, metric_name, plot_path)
  802. if chart_path:
  803. self.logger.info(f"{metric_name}图表已生成: {chart_path}")