diff --git a/backend/app.py b/backend/app.py index 5b45c797..2a5e590a 100644 --- a/backend/app.py +++ b/backend/app.py @@ -242,61 +242,81 @@ def api_health_check(): @app.route('/api/auth/login', methods=['POST']) def login(): """用户登录""" - try: - data = flask_request.get_json() - username = data.get('username') - password = data.get('password') - remember = data.get('remember', False) - if not username or not password: - return jsonify({ - 'success': False, - 'message': '用户名或密码不能为空' - }), 400 - - # 使用数据库验证用户 - user = db_manager.authenticate_user(username, password) - - if user: - # 检查用户是否已激活 - if not user['is_active']: + max_retries = 3 + retry_delay = 1 # 1秒延迟 + + for attempt in range(max_retries): + try: + data = flask_request.get_json() + username = data.get('username') + password = data.get('password') + if not username or not password: return jsonify({ 'success': False, - 'message': '账户未激活,请联系管理员审核' - }), 403 + 'message': '用户名或密码不能为空' + }), 400 + + # 使用数据库验证用户 + user = db_manager.authenticate_user(username, password) - # 构建用户数据 - user_data = { - 'id': user['id'], - 'username': user['username'], - 'name': user['name'], - 'role': 'admin' if user['user_type'] == 'admin' else 'user', - 'user_type': user['user_type'], - 'avatar': '' - } + if user: + # 检查用户是否已激活 + if not user['is_active']: + return jsonify({ + 'success': False, + 'message': '账户未激活,请联系管理员审核' + }), 403 + + # 构建用户数据 + user_data = { + 'id': user['id'], + 'username': user['username'], + 'name': user['name'], + 'role': 'admin' if user['user_type'] == 'admin' else 'user', + 'user_type': user['user_type'], + 'avatar': '' + } + + # 生成token(实际项目中应使用JWT等安全token) + token = f"token_{username}_{int(time.time())}" + + logger.info(f'用户 {username} 登录成功') + + return jsonify({ + 'success': True, + 'data': { + 'token': token, + 'user': user_data + }, + 'message': '登录成功' + }) + else: + logger.warning(f'用户 {username} 登录失败:用户名或密码错误') + return jsonify({ + 'success': False, + 'message': '用户名或密码错误' + }), 401 + + except Exception as e: + logger.error(f'登录失败 (尝试 {attempt + 1}/{max_retries}): {e}') - # 生成token(实际项目中应使用JWT等安全token) - token = f"token_{username}_{int(time.time())}" + # 如果是最后一次尝试,直接返回错误 + if attempt == max_retries - 1: + logger.error(f'登录失败,已达到最大重试次数 ({max_retries})') + return jsonify({ + 'success': False, + 'message': '系统异常,请稍后重试' + }), 500 - logger.info(f'用户 {username} 登录成功') - - return jsonify({ - 'success': True, - 'data': { - 'token': token, - 'user': user_data - }, - 'message': '登录成功' - }) - else: - logger.warning(f'用户 {username} 登录失败:用户名或密码错误') - return jsonify({ - 'success': False, - 'message': '用户名或密码错误' - }), 401 - - except Exception as e: - logger.error(f'登录失败: {e}') - return jsonify({'success': False, 'message': '登录失败'}), 500 + # 延迟后重试 + logger.info(f'等待 {retry_delay} 秒后重试...') + time.sleep(retry_delay) + + # 理论上不会执行到这里,但为了安全起见 + return jsonify({ + 'success': False, + 'message': '系统异常,请稍后重试' + }), 500 @app.route('/api/auth/register', methods=['POST']) def register(): diff --git a/backend/config.ini b/backend/config.ini index 7545327a..ec05a4df 100644 --- a/backend/config.ini +++ b/backend/config.ini @@ -16,7 +16,7 @@ max_backups = 7 [CAMERA] enabled = True -device_index = 3 +device_index = 1 width = 1280 height = 720 fps = 30 @@ -26,12 +26,12 @@ backend = directshow [FEMTOBOLT] enabled = True -algorithm_type = opencv +algorithm_type = plt color_resolution = 1080P depth_mode = NFOV_2X2BINNED camera_fps = 20 -depth_range_min = 1000 -depth_range_max = 1400 +depth_range_min = 800 +depth_range_max = 1200 fps = 15 synchronized_images_only = False diff --git a/backend/devices/camera_manager.py b/backend/devices/camera_manager.py index 4b15e365..1b8c4dc9 100644 --- a/backend/devices/camera_manager.py +++ b/backend/devices/camera_manager.py @@ -259,18 +259,18 @@ class CameraManager(BaseDevice): self.logger.debug(f"缓冲区设置耗时: {buffer_time:.1f}ms") # 性能优化设置:禁用可能导致延迟的自动功能 - optimization_start = time.time() - try: - # 禁用自动曝光以减少处理时间 - self.cap.set(cv2.CAP_PROP_AUTO_EXPOSURE, 0.25) # 手动曝光模式 - # 禁用自动白平衡 - self.cap.set(cv2.CAP_PROP_AUTO_WB, 0) - # 设置较低的曝光值以减少延迟 - self.cap.set(cv2.CAP_PROP_EXPOSURE, -6) - except Exception as e: - self.logger.debug(f"设置性能优化参数时出现警告: {e}") - optimization_time = (time.time() - optimization_start) * 1000 - self.logger.debug(f"性能优化设置耗时: {optimization_time:.1f}ms") + # optimization_start = time.time() + # try: + # # 禁用自动曝光以减少处理时间 + # self.cap.set(cv2.CAP_PROP_AUTO_EXPOSURE, 0.25) # 手动曝光模式 + # # 禁用自动白平衡 + # self.cap.set(cv2.CAP_PROP_AUTO_WB, 0) + # # 设置较低的曝光值以减少延迟 + # self.cap.set(cv2.CAP_PROP_EXPOSURE, -6) + # except Exception as e: + # self.logger.debug(f"设置性能优化参数时出现警告: {e}") + # optimization_time = (time.time() - optimization_start) * 1000 + # self.logger.debug(f"性能优化设置耗时: {optimization_time:.1f}ms") # 激进优化:跳过非关键属性设置,只设置必要属性 resolution_start = time.time() diff --git a/backend/devices/device_coordinator.py b/backend/devices/device_coordinator.py index df87bbf2..c7cb2412 100644 --- a/backend/devices/device_coordinator.py +++ b/backend/devices/device_coordinator.py @@ -442,7 +442,7 @@ class DeviceCoordinator: for device_name, device in self.devices.items(): try: # 对深度相机(femtobolt)和普通相机(camera)直接调用初始化和启动推流 - if device_name in ['femtobolt', 'camera']: + if device_name in ['femtobolt', 'camera',"imu"]: continue if hasattr(device, '_start_connection_monitor'): @@ -476,7 +476,7 @@ class DeviceCoordinator: for device_name, device in self.devices.items(): try: # 对深度相机(femtobolt)和普通相机(camera)直接调用停止推流 - if device_name in ['femtobolt', 'camera']: + if device_name in ['femtobolt', 'camera',"imu"]: self.logger.info(f"停止{device_name}设备推流") # # 调用设备的cleanup方法清理资源,停止推流 diff --git a/backend/devices/femtobolt_manager.py b/backend/devices/femtobolt_manager.py index e5a97949..771bf449 100644 --- a/backend/devices/femtobolt_manager.py +++ b/backend/devices/femtobolt_manager.py @@ -120,25 +120,37 @@ class FemtoBoltManager(BaseDevice): self.reconnect_delay = 3.0 # 发送频率控制(内存优化) - self.send_fps = self.config.get('send_fps', 20) # 默认20FPS发送 + self.send_fps = self.config.get('send_fps', 15) # 默认20FPS发送 self._min_send_interval = 1.0 / self.send_fps if self.send_fps > 0 else 0.05 self._last_send_time = 0 - # 编码参数缓存(避免每帧创建数组) - self._encode_param = [int(cv2.IMWRITE_JPEG_QUALITY), int(self.config.get('jpeg_quality', 60))] + # 编码参数缓存(避免每帧创建数组)- 为plt模式优化编码质量 + jpeg_quality = 40 if self.algorithm_type == 'plt' else int(self.config.get('jpeg_quality', 60)) + self._encode_param = [int(cv2.IMWRITE_JPEG_QUALITY), jpeg_quality] # 预计算伽马LUT(避免每帧计算) self._gamma_lut = None self._current_gamma = None self._update_gamma_lut() - # 预生成网格背景(避免每帧创建) + # 预分配缓冲区和变量(减少内存分配) + self._grid_background = None + self._reusable_buffer = None + self._crop_params = None # 缓存裁剪参数 + + # 帧预测和插值机制 + self._last_frame = None + self._frame_history = deque(maxlen=3) # 保存最近3帧用于预测 + self._prediction_enabled = True if self.algorithm_type == 'plt' else False self._grid_bg = None self._grid_size = (480, 640) # 默认尺寸 self.background = None # 用于缓存等高线渲染的背景 # 自定义彩虹色 colormap(参考testfemtobolt.py) - colors = ['fuchsia', 'red', 'yellow', 'lime', 'cyan', 'blue', + colors = [ + 'fuchsia', 'red', 'yellow', 'lime', 'cyan', 'blue', + 'fuchsia', 'red', 'yellow', 'lime', 'cyan', 'blue', + 'fuchsia', 'red', 'yellow', 'lime', 'cyan', 'blue', 'fuchsia', 'red', 'yellow', 'lime', 'cyan', 'blue', 'fuchsia', 'red', 'yellow', 'lime', 'cyan', 'blue', 'fuchsia', 'red', 'yellow', 'lime', 'cyan', 'blue'] @@ -148,7 +160,7 @@ class FemtoBoltManager(BaseDevice): matplotlib.use('Agg') # 创建matplotlib图形对象(复用以提高性能) - self.fig, self.ax = plt.subplots(figsize=(7, 7)) + self.fig, self.ax = plt.subplots(figsize=(9, 6)) self.ax.set_aspect('equal') plt.subplots_adjust(left=0, right=1, top=1, bottom=0) self.logger.info(f"FemtoBolt设备配置完成 - 算法类型: {self.algorithm_type}, 深度模式: {self.depth_mode}, FPS: {self.fps}") @@ -230,94 +242,118 @@ class FemtoBoltManager(BaseDevice): self.logger.error(f"优化等高线生成失败: {e}") return None - def _create_grid_background(self, height, width): - """创建网格背景缓存""" - bg = np.ones((height, width, 3), dtype=np.uint8) * 128 - # 绘制白色网格线 - grid_spacing = 50 - for x in range(0, width, grid_spacing): - cv2.line(bg, (x, 0), (x, height-1), (255, 255, 255), 1) - for y in range(0, height, grid_spacing): - cv2.line(bg, (0, y), (width-1, y), (255, 255, 255), 1) - - self._grid_bg = bg - self._grid_size = (height, width) + def _generate_contour_image_plt(self, depth): - """使用matplotlib生成等高线图像(完全采用display_x.py的逻辑)""" + """使用matplotlib生成等高线图像(性能优化版本)""" + try: + # 设置图形背景色和边距 + self.fig.patch.set_facecolor((50/255, 50/255, 50/255)) # 设置深灰色背景 rgb(50, 50, 50) + self.fig.tight_layout(pad=0) # 移除所有边距 + # 清除之前的绘图 self.ax.clear() + self.ax.set_facecolor((50/255, 50/255, 50/255)) # 设置坐标区域背景色为黑色 # 深度数据过滤(与display_x.py完全一致) depth[depth > self.depth_range_max] = 0 depth[depth < self.depth_range_min] = 0 - # 背景图(与display_x.py完全一致) - background = np.ones_like(depth) * 0.5 # 设定灰色背景 + # 背景图(深灰色背景) + # 创建RGB格式的背景图,确保颜色准确性 + background_gray_value = 50 # RGB(50, 50, 50) + background = np.full((*depth.shape, 3), background_gray_value, dtype=np.uint8) - # 使用 np.ma.masked_equal() 来屏蔽深度图中的零值(与display_x.py完全一致) + # 使用 np.ma.masked_equal() 来屏蔽深度图中的零值 depth = np.ma.masked_equal(depth, 0) - # 绘制背景(与display_x.py完全一致) - self.ax.imshow(background, origin='lower', cmap='gray', alpha=0.3) + # 绘制背景(不使用colormap,直接显示RGB图像) + self.ax.imshow(background, origin='lower', alpha=1.0) - # 绘制白色栅格线,并将其置于底层(网格密度加大一倍) - self.ax.grid(True, which='both', axis='both', color='white', linestyle='-', linewidth=0.5, zorder=0) - self.ax.minorticks_on() - self.ax.grid(True, which='minor', axis='both', color='white', linestyle='-', linewidth=0.3, zorder=0) + # 优化:简化网格绘制,减少绘制操作,使用深灰色网格以适应黑色背景 + self.ax.grid(True, which='major', axis='both', color='#333333', linestyle='-', linewidth=0.5, zorder=0) # 隐藏坐标轴 - # self.ax.set_xticks([]) - # self.ax.set_yticks([]) + self.ax.set_xticks([]) + self.ax.set_yticks([]) - # 绘制等高线图并设置原点在上方(与display_x.py完全一致) - import time + # 优化:减少等高线级别从100到30,大幅提升性能 start_time = time.perf_counter() - self.ax.contourf(depth, levels=100, cmap=self.custom_cmap, vmin=self.depth_range_min, vmax=self.depth_range_max, origin='upper', zorder=2) - contourf_time = time.perf_counter() - start_time - # self.logger.info(f"contourf绘制耗时: {contourf_time*1000:.2f}ms") - - - # 将matplotlib图形转换为numpy数组 - buf = io.BytesIO() - savefig_start = time.perf_counter() - savefig_start = time.perf_counter() - self.fig.savefig(buf, format='png',bbox_inches='tight', pad_inches=0, dpi=75) - savefig_time = time.perf_counter() - savefig_start - # self.logger.info(f"savefig保存耗时: {savefig_time*1000:.2f}ms") - - buf_start = time.perf_counter() - buf.seek(0) - - # 读取PNG数据并转换为OpenCV格式 - img_array = np.frombuffer(buf.getvalue(), dtype=np.uint8) - buf.close() - buf_time = time.perf_counter() - buf_start - # self.logger.info(f"缓冲区操作耗时: {buf_time*1000:.2f}ms") - - # 解码PNG图像 - decode_start = time.perf_counter() - img = cv2.imdecode(img_array, cv2.IMREAD_COLOR) - decode_time = time.perf_counter() - decode_start - # self.logger.info(f"PNG解码耗时: {decode_time*1000:.2f}ms") - # return img - if img is not None: - # 裁剪宽度(与原逻辑保持一致) - height, width = img.shape[:2] - target_width = round(height // 2) - if width > target_width: - left = (width - target_width) // 2 - right = left + target_width - img = img[:, left:right] - return img - else: - self.logger.error("无法解码matplotlib生成的PNG图像") + self.ax.contourf(depth, levels=80, cmap=self.custom_cmap, vmin=self.depth_range_min, vmax=self.depth_range_max, origin='upper', zorder=2) + # 优化:直接从canvas获取图像数据,避免PNG编码/解码 + try: + + # 绘制到canvas + self.fig.canvas.draw() + # 兼容不同版本的matplotlib获取图像数据 + canvas_width, canvas_height = self.fig.canvas.get_width_height() + # 尝试使用新版本的方法 + try: + # matplotlib 3.8+ 使用 buffer_rgba() + buf = np.frombuffer(self.fig.canvas.buffer_rgba(), dtype=np.uint8) + buf = buf.reshape((canvas_height, canvas_width, 4)) + # 转换RGBA到RGB + img = cv2.cvtColor(buf, cv2.COLOR_RGBA2BGR) + except AttributeError: + try: + # matplotlib 3.0-3.7 使用 tostring_argb() + buf = np.frombuffer(self.fig.canvas.tostring_argb(), dtype=np.uint8) + buf = buf.reshape((canvas_height, canvas_width, 4)) + # 转换ARGB到BGR + img = cv2.cvtColor(buf, cv2.COLOR_BGRA2BGR) + except AttributeError: + # 旧版本matplotlib使用 tostring_rgb() + buf = np.frombuffer(self.fig.canvas.tostring_rgb(), dtype=np.uint8) + buf = buf.reshape((canvas_height, canvas_width, 3)) + # 转换RGB到BGR + img = cv2.cvtColor(buf, cv2.COLOR_RGB2BGR) + return img + except Exception as e: + self.logger.error(f"Canvas图像转换失败: {e}") + # 降级到PNG方法 return None except Exception as e: self.logger.error(f"生成等高线图像失败: {e}") return None + + def _generate_contour_image_source(self, depth): + """直接读取深度相机图片数据,不做处理返回深度相机图片(用于对比分析原始图像的延时情况)""" + generation_start = time.perf_counter() + + try: + # 将深度数据转换为可视化图像 + # 深度数据通常是单通道的,需要转换为3通道BGR格式 + if len(depth.shape) == 2: + # 深度数据过滤(与其他方法保持一致) + depth_filtered = depth.copy() + depth_filtered[depth_filtered > self.depth_range_max] = 0 + depth_filtered[depth_filtered < self.depth_range_min] = 0 + + # 将深度数据归一化到0-255范围 + depth_normalized = cv2.normalize(depth_filtered, None, 0, 255, cv2.NORM_MINMAX, dtype=cv2.CV_8U) + + # 应用彩色映射创建彩色深度图 + # 使用JET色彩映射:蓝色(近)到红色(远) + img_colored = cv2.applyColorMap(depth_normalized, cv2.COLORMAP_JET) + + # 将零值区域设为黑色(无效深度) + mask = (depth_filtered == 0) + img_colored[mask] = [0, 0, 0] # 黑色背景 + + img = img_colored + else: + # 如果已经是3通道,直接使用 + img = depth.astype(np.uint8) + + return img + + except Exception as e: + self.logger.error(f"生成原始深度图像失败: {e}") + return None + + def initialize(self) -> bool: """ @@ -436,10 +472,10 @@ class FemtoBoltManager(BaseDevice): # 配置FemtoBolt设备参数 self.femtobolt_config = self.pykinect.default_configuration - self.femtobolt_config.depth_mode = self.pykinect.K4A_DEPTH_MODE_NFOV_UNBINNED + self.femtobolt_config.depth_mode = self.pykinect.K4A_DEPTH_MODE_NFOV_2X2BINNED self.femtobolt_config.color_format = self.pykinect.K4A_IMAGE_FORMAT_COLOR_BGRA32 - self.femtobolt_config.color_resolution = self.pykinect.K4A_COLOR_RESOLUTION_1080P - self.femtobolt_config.camera_fps = self.pykinect.K4A_FRAMES_PER_SECOND_15 + self.femtobolt_config.color_resolution = self.pykinect.K4A_COLOR_RESOLUTION_720P + self.femtobolt_config.camera_fps = self.pykinect.K4A_FRAMES_PER_SECOND_30 # 30FPS self.femtobolt_config.synchronized_images_only = False return True @@ -490,18 +526,8 @@ class FemtoBoltManager(BaseDevice): self.logger.info('✓ FemtoBolt深度相机初始化成功!') else: self.logger.warning('FemtoBolt设备启动返回None,设备可能未连接') - return False + return False - # # 等待设备稳定 - # time.sleep(1.0) - - # # 测试捕获(可选,失败不抛异常,只作为稳定性判断) - # try: - # if not self._test_capture(): - # self.logger.warning('FemtoBolt设备捕获测试失败') - # return False - # except Exception: - # return False self.logger.info('FemtoBolt设备启动成功') return True @@ -639,12 +665,46 @@ class FemtoBoltManager(BaseDevice): self.logger.info("FemtoBolt流工作线程启动") frame_count = 0 + last_generation_time = 0.0 + skip_frame_count = 0 + + # 根据算法类型设置自适应采样推流频率 + if self.algorithm_type == 'source': + # source算法:图像生成时间很短,提高推流频率以减少延时 + adaptive_fps = 25 # 提高到25fps,接近Camera的30fps + self.logger.info(f"FemtoBolt使用source算法,设置推流频率为{adaptive_fps}fps(优化延时)") + elif self.algorithm_type == 'plt': + # plt算法:图像生成时间约0.3-0.4秒,设置较低fps推流以减少延时 + adaptive_fps = 5 + self.logger.info(f"FemtoBolt使用plt算法,设置推流频率为{adaptive_fps}fps(优化延时)") + elif self.algorithm_type == 'opencv': + # opencv算法:图像生成时间约0.006秒,设置高fps推流 + adaptive_fps = 20 + self.logger.info(f"FemtoBolt使用opencv算法,设置推流频率为{adaptive_fps}fps") + else: + # 默认使用配置文件中的设置 + adaptive_fps = self.send_fps + self.logger.info(f"FemtoBolt使用默认推流频率{adaptive_fps}fps") + + # 计算自适应最小发送间隔 + adaptive_min_interval = 1.0 / adaptive_fps if adaptive_fps > 0 else 0.05 try: while self.is_streaming: - # 发送频率限制 + # 动态调整发送间隔,避免延时累积 now = time.time() - if now - self._last_send_time < self._min_send_interval: + + # 对于source算法,简化间隔计算,减少延时 + if self.algorithm_type == 'source': + # source算法处理时间很短,使用固定间隔 + dynamic_interval = adaptive_min_interval + elif self.algorithm_type == 'plt' and last_generation_time > 0: + # 根据实际生成时间动态调整间隔,避免延时累积 + dynamic_interval = max(adaptive_min_interval, last_generation_time * 0.8) + else: + dynamic_interval = adaptive_min_interval + + if now - self._last_send_time < dynamic_interval: time.sleep(0.001) continue @@ -656,32 +716,77 @@ class FemtoBoltManager(BaseDevice): ret, depth_image = capture.get_depth_image() if ret and depth_image is not None: - # 更新心跳时间,防止连接监控线程判定为超时 - self.update_heartbeat() + # 对于source算法,跳过复杂的跳帧逻辑,直接处理 + if self.algorithm_type == 'source': + # source算法处理时间很短,不需要跳帧 + pass + elif self.algorithm_type == 'plt': + # 对于plt算法,如果延时累积过多,跳过部分帧 + time_since_last = now - self._last_send_time + if time_since_last > adaptive_min_interval * 2: + skip_frame_count += 1 + if skip_frame_count % 2 == 0: # 每2帧跳过1帧 + # 如果启用帧预测,发送预测帧而不是跳过 + if self._prediction_enabled and self._last_frame is not None: + self._send_predicted_frame(frame_count, now) + frame_count += 1 + continue + + # 测试:记录图像生成开始时间 + generation_start_time = time.time() # 根据配置选择不同的等高线生成方法 - if self.algorithm_type == 'plt': + if self.algorithm_type == 'source': + depth_colored_final = self._generate_contour_image_source(depth_image) + elif self.algorithm_type == 'plt': depth_colored_final = self._generate_contour_image_plt(depth_image) elif self.algorithm_type == 'opencv': depth_colored_final = self._generate_contour_image_opencv(depth_image) + + # 测试:计算并打印图像生成时间 + generation_time = time.time() - generation_start_time + last_generation_time = generation_time + # print(f"[FemtoBolt] 帧图像生成时间: {generation_time:.4f}秒 ({self.algorithm_type}算法) - 跳帧数: {skip_frame_count}") if depth_colored_final is None: # 如果等高线生成失败,跳过这一帧 continue - # 裁剪处理(推迟到显示阶段) + # 优化裁剪处理(缓存裁剪参数) h, w = depth_colored_final.shape[:2] + # self.logger.info(f"深度图像尺寸: 宽={w}, 高={h}") target_width = h // 2 - display_image = depth_colored_final - if w > target_width: - left = (w - target_width) // 2 - right = left + target_width - display_image = depth_colored_final[:, left:right] - # 推送SocketIO + # 缓存裁剪参数,避免重复计算 + if self._crop_params is None or self._crop_params[0] != (h, w): + if w > target_width: + left = (w - target_width) // 2 + right = left + target_width + self._crop_params = ((h, w), left, right, target_width) + else: + self._crop_params = ((h, w), None, None, w) + + # 应用裁剪 + if self._crop_params[1] is not None: + display_image = depth_colored_final[:, self._crop_params[1]:self._crop_params[2]] + else: + display_image = depth_colored_final + + # 优化编码:使用更低的质量和更快的编码 + encode_start = time.time() success, buffer = cv2.imencode('.jpg', display_image, self._encode_param) + encode_time = time.time() - encode_start + if success and self._socketio: - jpg_as_text = base64.b64encode(memoryview(buffer).tobytes()).decode('utf-8') + # 优化base64编码:直接使用memoryview避免额外拷贝 + base64_start = time.time() + jpg_as_text = base64.b64encode(memoryview(buffer)).decode('utf-8') + base64_time = time.time() - base64_start + + # 添加编码时间监控 + if encode_time > 0.01 or base64_time > 0.01: # 超过10ms记录 + print(f"[FemtoBolt] 编码时间: JPEG={encode_time:.4f}s, Base64={base64_time:.4f}s") + self._socketio.emit('femtobolt_frame', { 'depth_image': jpg_as_text, 'frame_count': frame_count, @@ -695,9 +800,31 @@ class FemtoBoltManager(BaseDevice): }, namespace='/devices') frame_count += 1 self._last_send_time = now - - # 更新统计 - self._update_statistics() + + # 更新帧历史用于预测(仅对需要预测的算法) + if self._prediction_enabled and self.algorithm_type in ['plt', 'opencv']: + # 优化内存管理:重用缓冲区 + if self._reusable_buffer is None or self._reusable_buffer.shape != display_image.shape: + self._reusable_buffer = np.empty_like(display_image) + + # 使用预分配的缓冲区复制图像 + np.copyto(self._reusable_buffer, display_image) + self._last_frame = self._reusable_buffer + + # 限制历史帧数量,减少内存占用 + frame_data = { + 'image': display_image.copy(), # 这里仍需要复制,因为display_image可能被修改 + 'timestamp': now, + 'depth_range': { + 'min': self.depth_range_min, + 'max': self.depth_range_max + } + } + self._frame_history.append(frame_data) + + # 主动触发垃圾回收(仅在必要时) + if frame_count % 100 == 0: # 每100帧触发一次 + gc.collect() else: time.sleep(0.005) except Exception as e: @@ -725,6 +852,48 @@ class FemtoBoltManager(BaseDevice): finally: self.is_streaming = False self.logger.info("FemtoBolt流工作线程结束") + + def _send_predicted_frame(self, frame_count: int, timestamp: float): + """ + 发送预测帧以减少延时感知 + + Args: + frame_count: 帧计数 + timestamp: 时间戳 + """ + try: + if self._last_frame is None: + return + + # 简单的帧预测:使用最后一帧 + predicted_frame = self._last_frame + + # 如果有足够的历史帧,可以进行简单的运动预测 + if len(self._frame_history) >= 2: + # 这里可以实现更复杂的预测算法 + # 目前使用最新帧作为预测帧 + predicted_frame = self._frame_history[-1]['image'] + + # 编码并发送预测帧 + success, buffer = cv2.imencode('.jpg', predicted_frame, self._encode_param) + if success and self._socketio: + jpg_as_text = base64.b64encode(memoryview(buffer)).decode('utf-8') + self._socketio.emit('femtobolt_frame', { + 'depth_image': jpg_as_text, + 'frame_count': frame_count, + 'timestamp': timestamp, + 'fps': self.actual_fps, + 'device_id': self.device_id, + 'predicted': True, # 标记为预测帧 + 'depth_range': { + 'min': self.depth_range_min, + 'max': self.depth_range_max + } + }, namespace='/devices') + self._last_send_time = timestamp + + except Exception as e: + self.logger.error(f"发送预测帧失败: {e}") def _update_statistics(self): diff --git a/backend/devices/imu_manager.py b/backend/devices/imu_manager.py index 252fdfe4..b95ce1cc 100644 --- a/backend/devices/imu_manager.py +++ b/backend/devices/imu_manager.py @@ -469,7 +469,7 @@ class IMUManager(BaseDevice): # 设备配置 self.port = config.get('port', 'COM7') self.baudrate = config.get('baudrate', 9600) - self.device_type = config.get('device_type', 'mock') # 'real' | 'mock' | 'ble' + self.device_type = config.get('device_type', 'ble') # 'real' | 'mock' | 'ble' self.use_mock = config.get('use_mock', False) # 保持向后兼容 self.mac_address = config.get('mac_address', '') # IMU设备实例 @@ -701,7 +701,7 @@ class IMUManager(BaseDevice): # self.last_valid_data = data # 更新心跳时间,防止连接监控线程判定为超时 - self.update_heartbeat() + # self.update_heartbeat() # 发送数据到前端 if self._socketio: diff --git a/backend/devices/utils/config.ini b/backend/devices/utils/config.ini index 18167216..959ea88c 100644 --- a/backend/devices/utils/config.ini +++ b/backend/devices/utils/config.ini @@ -26,12 +26,12 @@ backend = directshow [FEMTOBOLT] enabled = True -algorithm_type = opencv +algorithm_type = plt color_resolution = 1080P depth_mode = NFOV_2X2BINNED camera_fps = 20 -depth_range_min = 1000 -depth_range_max = 1400 +depth_range_min = 800 +depth_range_max = 1200 fps = 15 synchronized_images_only = False diff --git a/backend/dll/femtobolt/Log/OrbbecSDK.log.txt b/backend/dll/femtobolt/Log/OrbbecSDK.log.txt index 9b70bde3..545e5114 100644 Binary files a/backend/dll/femtobolt/Log/OrbbecSDK.log.txt and b/backend/dll/femtobolt/Log/OrbbecSDK.log.txt differ diff --git a/backend/dll/femtobolt/femtobolt.zip b/backend/dll/femtobolt/femtobolt.zip new file mode 100644 index 00000000..345effa7 Binary files /dev/null and b/backend/dll/femtobolt/femtobolt.zip differ diff --git a/backend/tests/test44femtobolt.py b/backend/tests/test_femtobolt.py similarity index 100% rename from backend/tests/test44femtobolt.py rename to backend/tests/test_femtobolt.py diff --git a/frontend/src/renderer/main/main.js b/frontend/src/renderer/main/main.js index ded19563..775e8b39 100644 --- a/frontend/src/renderer/main/main.js +++ b/frontend/src/renderer/main/main.js @@ -119,12 +119,12 @@ function createWindow() { } else { // 启动后端服务 startBackendService(); - // 延迟1秒后启动本地HTTP服务器 + // 延迟2秒后启动本地HTTP服务器 setTimeout(() => { startLocalServer(() => { mainWindow.loadURL('http://localhost:3000'); }); - }, 1000); + }, 2000); } // 窗口就绪后再显示,避免白屏/闪烁 diff --git a/frontend/src/renderer/src/views/Detection.vue b/frontend/src/renderer/src/views/Detection.vue index 80903a48..44a0a7ab 100644 --- a/frontend/src/renderer/src/views/Detection.vue +++ b/frontend/src/renderer/src/views/Detection.vue @@ -136,7 +136,7 @@
左最大:{{ - headPoseMaxValues.rotationLeftMax.toFixed(1) }}°
+ headPoseMaxValues.rotationRightMax.toFixed(1) }}°
@@ -164,7 +164,7 @@
俯最大:{{ - headPoseMaxValues.pitchDownMax.toFixed(1) }}°
+ headPoseMaxValues.pitchUpMax.toFixed(1) }}°
diff --git a/frontend/src/renderer/src/views/Login.vue b/frontend/src/renderer/src/views/Login.vue index 2b3b04e6..67aed137 100644 --- a/frontend/src/renderer/src/views/Login.vue +++ b/frontend/src/renderer/src/views/Login.vue @@ -50,7 +50,7 @@ @@ -210,7 +210,7 @@