diff --git a/src/tracking_car/config.yaml b/src/tracking_car/config.yaml index 2e1dae5a58..79759fe126 100644 --- a/src/tracking_car/config.yaml +++ b/src/tracking_car/config.yaml @@ -17,7 +17,7 @@ lidar_points_per_second: 500000 # ======================== 检测模型配置 ======================== yolo_model: "yolov8n.pt" -conf_thres: 0.25 +conf_thres: 0.1 iou_thres: 0.3 device: "cpu" yolo_imgsz_max: 640 @@ -57,3 +57,25 @@ view: satellite_height: 50.0 behind_distance: 10.0 first_person_height: 1.6 + +# ======================== 热重载配置 ======================== +hot_reload: + enabled: true # 是否启用热重载 + check_interval: 100 # 每多少帧检查一次配置文件 + safe_keys_only: true # 是否只更新安全参数(避免重启连接) + +# 可热重载的安全参数列表(这些参数可以安全更新而不需要重启) +hot_reload_safe_keys: + - "conf_thres" # 检测置信度阈值 + - "iou_thres" # IOU阈值 + - "display_fps" # 显示帧率 + - "max_age" # 跟踪最大丢失帧数 + - "min_hits" # 跟踪最小匹配次数 + - "adaptive_fps" # 是否启用自适应帧率 + - "min_fps" # 最小FPS + - "max_fps" # 最大FPS + - "fov" # 相机视野 + - "yolo_imgsz_max" # YOLO输入尺寸 + - "stop_speed_thresh" # 停车速度阈值 + - "danger_dist_thresh" # 危险距离阈值 + - "weather" # 天气设置 \ No newline at end of file diff --git a/src/tracking_car/main.py b/src/tracking_car/main.py index 88cfab5497..06b02aea15 100644 --- a/src/tracking_car/main.py +++ b/src/tracking_car/main.py @@ -35,7 +35,7 @@ def load_config(config_path=None): """ - Load configuration + Load configuration with hot reload support Args: config_path: Path to configuration file @@ -43,6 +43,9 @@ def load_config(config_path=None): Returns: dict: Configuration dictionary """ + import os + import time + # Default configuration default_config = { # CARLA connection @@ -97,20 +100,142 @@ def load_config(config_path=None): # Ego vehicle 'ego_vehicle_filter': 'vehicle.tesla.model3', 'ego_vehicle_color': '255,0,0', + + # ============ 新增:热重载默认配置 ============ + 'hot_reload': { + 'enabled': True, + 'check_interval': 100, + 'safe_keys_only': True, + }, + 'hot_reload_safe_keys': [ + 'conf_thres', 'iou_thres', 'display_fps', + 'max_age', 'min_hits', 'adaptive_fps', + 'min_fps', 'max_fps', 'fov', 'yolo_imgsz_max', + 'stop_speed_thresh', 'danger_dist_thresh', 'weather' + ], + + # 性能优化 + 'adaptive_fps': True, + 'min_fps': 10, + 'max_fps': 60, + + # 调试 + 'debug_detection': False, + 'debug_tracking': False, + # ============================================= } - # If config file is provided, try to load it + # 记录提供的配置文件路径 + provided_config_path = config_path + + # 如果未提供配置文件路径,尝试查找默认文件 + if not config_path: + possible_paths = [ + 'config.yaml', + 'config_optimized.yaml', + './config/config.yaml', + '../config.yaml' + ] + + for path in possible_paths: + if os.path.exists(path): + config_path = path + logger.info(f"[CONFIG] 发现配置文件: {path}") + break + + # 如果找到配置文件,尝试加载它 if config_path and os.path.exists(config_path): - loaded_config = utils.load_yaml_config(config_path) - if loaded_config: - # Merge configurations (loaded config overrides default) - for key, value in loaded_config.items(): - if isinstance(value, dict) and key in default_config and isinstance(default_config[key], dict): - # Recursive dictionary merge - default_config[key].update(value) - else: - default_config[key] = value - logger.info(f"[OK] Configuration file loaded: {config_path}") + try: + loaded_config = utils.load_yaml_config(config_path) + + if loaded_config: + logger.info(f"[CONFIG] 成功加载配置文件: {config_path}") + + # 深度合并配置字典 + def deep_merge(target, source): + """递归合并字典""" + for key, value in source.items(): + if key in target and isinstance(target[key], dict) and isinstance(value, dict): + # 递归合并字典 + deep_merge(target[key], value) + else: + # 直接赋值 + target[key] = value + return target + + # 合并配置 + default_config = deep_merge(default_config, loaded_config) + + # ============ 关键修复:确保配置路径正确记录 ============ + # 使用绝对路径,避免相对路径问题 + absolute_path = os.path.abspath(config_path) + default_config['_config_path'] = absolute_path + + # 获取文件修改时间 + try: + default_config['_config_mtime'] = os.path.getmtime(absolute_path) + logger.info(f"[CONFIG] 配置文件路径: {absolute_path}") + logger.info(f"[CONFIG] 最后修改时间: {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(default_config['_config_mtime']))}") + except Exception as e: + logger.warning(f"[CONFIG] 无法获取文件修改时间: {e}") + default_config['_config_mtime'] = 0 + # ==================================================== + + # 记录实际加载的配置文件路径(用于日志) + default_config['_loaded_config_path'] = config_path + + else: + logger.warning(f"[CONFIG] 配置文件内容为空或无效: {config_path}") + + except Exception as e: + logger.error(f"[CONFIG] 加载配置文件失败 {config_path}: {e}") + logger.info("[CONFIG] 将使用默认配置") + + elif provided_config_path: # 用户指定了路径但文件不存在 + logger.warning(f"[CONFIG] 指定的配置文件不存在: {provided_config_path}") + logger.info("[CONFIG] 将使用默认配置") + + else: + logger.info("[CONFIG] 未指定配置文件,使用默认配置") + + # ============ 修复:确保热重载配置存在 ============ + # 确保 hot_reload 配置存在 + if 'hot_reload' not in default_config: + default_config['hot_reload'] = { + 'enabled': True, + 'check_interval': 100, + 'safe_keys_only': True, + } + logger.info("[CONFIG] 添加默认热重载配置") + + # 确保 hot_reload_safe_keys 存在 + if 'hot_reload_safe_keys' not in default_config: + default_config['hot_reload_safe_keys'] = [ + 'conf_thres', 'iou_thres', 'display_fps', + 'max_age', 'min_hits', 'adaptive_fps', + 'min_fps', 'max_fps', 'fov', 'yolo_imgsz_max', + 'stop_speed_thresh', 'danger_dist_thresh', 'weather' + ] + + # 确保 _config_path 存在(即使没有配置文件) + if '_config_path' not in default_config: + # 如果使用默认配置,创建一个虚拟路径 + if provided_config_path and os.path.exists(provided_config_path): + default_config['_config_path'] = os.path.abspath(provided_config_path) + else: + # 使用当前目录的默认路径 + default_config['_config_path'] = os.path.abspath('config.yaml') + logger.info(f"[CONFIG] 设置默认配置文件路径: {default_config['_config_path']}") + + # 确保 _config_mtime 存在 + if '_config_mtime' not in default_config: + default_config['_config_mtime'] = 0 + + # 记录最终使用的配置路径 + config_path_display = default_config.get('_config_path', 'Unknown') + logger.info(f"[CONFIG] 最终配置路径: {config_path_display}") + logger.info(f"[CONFIG] 热重载状态: {'已启用' if default_config['hot_reload'].get('enabled', True) else '已禁用'}") + # ================================================= return default_config @@ -1072,34 +1197,72 @@ def draw_color_legend(self, image): return image + def _draw_info_panel(self, image, track_count): + """绘制信息面板(添加热重载状态)""" + h, w = image.shape[:2] + + # 信息面板背景(半透明黑色) + panel_height = 120 # 稍微增加高度 + overlay = image.copy() + cv2.rectangle(overlay, (0, 0), (w, panel_height), (0, 0, 0), -1) + image = cv2.addWeighted(overlay, 0.7, image, 0.3, 0) + + # 标题 + title = "CARLA Tracking System" + cv2.putText(image, title, (10, 30), + cv2.FONT_HERSHEY_SIMPLEX, 1.0, (0, 255, 0), 2) + + status_lines = [ + f"Objects: {track_count}", + f"ESC:Exit | W:Weather | S:Screenshot", + f"P:Pause | T:Stats | M:Legend", + f"L:PointCloud | V:View | R:Reload", + ] + + # 绘制状态信息 + font = cv2.FONT_HERSHEY_SIMPLEX + for i, line in enumerate(status_lines): + y_pos = 55 + i * 20 + cv2.putText(image, line, (10, y_pos), + font, 0.5, (255, 255, 255), 1) + + return image + def show(self, image, stats_data=None): """ - Display image and statistics window - - Args: - image: Main window image - stats_data: Statistics data (for updating statistics window) - - Returns: - int: Key value + 显示图像和统计窗口(优化防闪烁) """ - # Display main window + # 双缓冲:创建临时图像副本 if utils.valid_img(image): - cv2.imshow(self.window_name, image) + display_image = image.copy() - # Update statistics window (update every few frames to avoid performance impact) + # 添加FPS显示到图像副本(而不是原图) + fps_text = f"FPS: {stats_data.get('fps', 0):.1f}" if stats_data else "FPS: N/A" + cv2.putText(display_image, fps_text, + (self.config.get('img_width', 640) - 100, 25), + cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) + + # 显示主窗口 + cv2.imshow(self.window_name, display_image) + + # 优化统计窗口更新频率(降低到每5帧更新一次) if self.show_stats_window and stats_data is not None: self.stats_frame_counter += 1 - - if self.stats_frame_counter >= self.stats_update_interval: - self.stats_image = self.create_stats_window_image(stats_data) - if self.stats_image is not None: - cv2.imshow(self.stats_window_name, self.stats_image) - self.stats_frame_counter = 0 - - # Wait for key (brief wait to maintain responsiveness) - return cv2.waitKey(1) + + # 每5帧更新一次统计窗口(原来是每2帧) + if self.stats_frame_counter >= 10: + try: + self.stats_image = self.create_stats_window_image(stats_data) + if self.stats_image is not None: + # 使用双缓冲显示统计窗口 + cv2.imshow(self.stats_window_name, self.stats_image) + self.stats_frame_counter = 0 + except Exception as e: + logger.debug(f"Stats window update failed: {e}") + # 等待键(稍微增加等待时间,减少CPU占用) + return cv2.waitKey(1) & 0xFF # 添加 & 0xFF 确保跨平台兼容性 + def destroy(self): """Destroy all windows""" # 销毁点云可视化器 @@ -1336,6 +1499,155 @@ def _collect_statistics_data(self, fps, detection_time, tracking_time, tracks_in 'tracking_time': tracking_time, } + def _check_and_reload_config(self): + """ + 检查并重新加载配置文件 + """ + try: + # 1. 检查是否启用热重载 + hot_reload_config = self.config.get('hot_reload', {}) + if not hot_reload_config.get('enabled', True): + return + + # 2. 检查间隔 + check_interval = hot_reload_config.get('check_interval', 100) + if self.frame_count % check_interval != 0: + return + + # 3. 获取配置文件路径 + config_path = self.config.get('_config_path') + if not config_path: + logger.debug("[HOT RELOAD] 无配置文件路径") + return + + # 4. 检查文件是否存在 + if not os.path.exists(config_path): + logger.warning(f"[HOT RELOAD] 配置文件不存在: {config_path}") + return + + # 5. 检查文件是否被修改 + try: + current_mtime = os.path.getmtime(config_path) + except Exception as e: + logger.warning(f"[HOT RELOAD] 无法获取文件修改时间: {e}") + return + + last_mtime = self.config.get('_config_mtime', 0) + + if current_mtime <= last_mtime: + return # 文件未修改 + + logger.info("🔄 [HOT RELOAD] 检测到配置文件更新,重新加载...") + + # 6. 重新加载配置 + new_config = load_config(config_path) + + # 7. 安全更新配置参数 + safe_keys_only = hot_reload_config.get('safe_keys_only', True) + updated_keys = [] + + if safe_keys_only: + # 只更新安全参数列表中的键 + safe_keys = self.config.get('hot_reload_safe_keys', []) + for key in safe_keys: + if key in new_config and key in self.config: + old_val = self.config[key] + new_val = new_config[key] + if old_val != new_val: + self.config[key] = new_val + updated_keys.append((key, old_val, new_val)) + else: + # 更新所有参数(危险模式) + for key, new_val in new_config.items(): + if not key.startswith('_'): # 跳过内部键 + old_val = self.config.get(key) + if old_val != new_val: + self.config[key] = new_val + updated_keys.append((key, old_val, new_val)) + + # 8. 更新配置文件修改时间 + self.config['_config_mtime'] = current_mtime + + # 9. 应用配置更新到各组件 + if updated_keys: + self._apply_config_updates(updated_keys) + + # 打印更新日志 + logger.info("✅ [HOT RELOAD] 配置更新完成:") + for key, old_val, new_val in updated_keys: + logger.info(f" {key}: {old_val} → {new_val}") + else: + logger.info("📝 [HOT RELOAD] 配置文件已重新加载,但无参数变化") + + except Exception as e: + logger.warning(f"[HOT RELOAD] 配置更新失败: {e}") + import traceback + traceback.print_exc() + + def _apply_config_updates(self, updated_keys): + """ + 将配置更新应用到各组件 + """ + for key, old_val, new_val in updated_keys: + try: + # 1. 应用到检测器 + if self.detector: + if key == 'conf_thres': + self.detector.conf_thres = new_val + logger.debug(f"更新检测器置信度阈值: {new_val}") + elif key == 'iou_thres': + self.detector.iou_thres = new_val + logger.debug(f"更新检测器IOU阈值: {new_val}") + elif key == 'yolo_imgsz_max': + logger.warning("⚠️ YOLO输入尺寸变更需要重启检测器才能生效") + + # 2. 应用到跟踪器 + if self.tracker: + if key == 'max_age': + self.tracker.max_age = new_val + logger.debug(f"更新跟踪器max_age: {new_val}") + elif key == 'min_hits': + self.tracker.min_hits = new_val + logger.debug(f"更新跟踪器min_hits: {new_val}") + elif key == 'iou_thres': + self.tracker.iou_threshold = new_val + logger.debug(f"更新跟踪器IOU阈值: {new_val}") + + # 3. 应用到传感器 + if self.sensor_manager: + if key == 'fov': + logger.warning("⚠️ 相机FOV变更需要重启传感器才能生效") + elif key == 'use_lidar': + logger.warning("⚠️ LiDAR开关变更需要重启程序才能生效") + + # 4. 应用到行为分析参数 + if key in ['stop_speed_thresh', 'danger_dist_thresh']: + logger.debug(f"更新行为分析参数 {key}: {new_val}") + + # 5. 应用到天气 + if key == 'weather' and self.world: + from sensors import set_weather # 需要导入 + set_weather(self.world, new_val) + logger.info(f"🌤️ 天气已更新: {new_val}") + + # 6. 帧率相关参数已在 _control_frame_rate 中处理 + + except Exception as e: + logger.warning(f"[热重载] 应用配置更新失败 {key}: {e}") + + def _force_reload_config(self): + """ + 强制重新加载配置文件(手动触发) + """ + config_path = self.config.get('_config_path') + if not config_path or not os.path.exists(config_path): + logger.warning("无法强制重载:配置文件路径无效") + return + + logger.info("🔄 [热重载] 手动触发配置重新加载...") + # 强制将修改时间设为0,确保会重新加载 + self.config['_config_mtime'] = 0 + def run(self): """Run main loop""" import time @@ -1424,6 +1736,23 @@ def run(self): tracks_info=tracks_info ) + # 10. 显示结果(传递统计数据) + key = self.visualizer.show(result_image, stats_data=stats_data) + + # 11. 处理键盘输入 + self._handle_keyboard_input(key) + + # 12. 帧率控制 + self._control_frame_rate(fps) + + # ============ 新增:热重载检查 ============ + self._check_and_reload_config() + # ======================================== + + # 13. 更新状态 + self.frame_count += 1 + self.perf_monitor.end_frame() + # 添加点云显示(新增) if sensor_data.get('pointcloud') is not None: self.visualizer.update_pointcloud(sensor_data['pointcloud']) @@ -1527,6 +1856,10 @@ def _handle_keyboard_input(self, key): } mode_name = mode_names.get(current_mode, current_mode) logger.info(f"[VIEW] 切换到 {mode_name}") + + # 新增:R键手动重载配置 + elif key == ord('r') or key == ord('R'): + self._force_reload_config() def _control_frame_rate(self, current_fps): """自适应帧率控制(简单版)"""