修正了bug

This commit is contained in:
root 2025-10-11 16:58:52 +08:00
parent 76c61d75df
commit ed0c44ed6d
14 changed files with 418 additions and 195 deletions

View File

@ -242,61 +242,81 @@ def api_health_check():
@app.route('/api/auth/login', methods=['POST']) @app.route('/api/auth/login', methods=['POST'])
def login(): def login():
"""用户登录""" """用户登录"""
try: max_retries = 3
data = flask_request.get_json() retry_delay = 1 # 1秒延迟
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
# 使用数据库验证用户 for attempt in range(max_retries):
user = db_manager.authenticate_user(username, password) try:
data = flask_request.get_json()
if user: username = data.get('username')
# 检查用户是否已激活 password = data.get('password')
if not user['is_active']: if not username or not password:
return jsonify({ return jsonify({
'success': False, 'success': False,
'message': '账户未激活,请联系管理员审核' 'message': '用户名或密码不能为空'
}), 403 }), 400
# 构建用户数据 # 使用数据库验证用户
user_data = { user = db_manager.authenticate_user(username, password)
'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 if user:
token = f"token_{username}_{int(time.time())}" # 检查用户是否已激活
if not user['is_active']:
return jsonify({
'success': False,
'message': '账户未激活,请联系管理员审核'
}), 403
logger.info(f'用户 {username} 登录成功') # 构建用户数据
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': ''
}
return jsonify({ # 生成token实际项目中应使用JWT等安全token
'success': True, token = f"token_{username}_{int(time.time())}"
'data': {
'token': token,
'user': user_data
},
'message': '登录成功'
})
else:
logger.warning(f'用户 {username} 登录失败:用户名或密码错误')
return jsonify({
'success': False,
'message': '用户名或密码错误'
}), 401
except Exception as e: logger.info(f'用户 {username} 登录成功')
logger.error(f'登录失败: {e}')
return jsonify({'success': False, 'message': '登录失败'}), 500 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}')
# 如果是最后一次尝试,直接返回错误
if attempt == max_retries - 1:
logger.error(f'登录失败,已达到最大重试次数 ({max_retries})')
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']) @app.route('/api/auth/register', methods=['POST'])
def register(): def register():

View File

@ -16,7 +16,7 @@ max_backups = 7
[CAMERA] [CAMERA]
enabled = True enabled = True
device_index = 3 device_index = 1
width = 1280 width = 1280
height = 720 height = 720
fps = 30 fps = 30
@ -26,12 +26,12 @@ backend = directshow
[FEMTOBOLT] [FEMTOBOLT]
enabled = True enabled = True
algorithm_type = opencv algorithm_type = plt
color_resolution = 1080P color_resolution = 1080P
depth_mode = NFOV_2X2BINNED depth_mode = NFOV_2X2BINNED
camera_fps = 20 camera_fps = 20
depth_range_min = 1000 depth_range_min = 800
depth_range_max = 1400 depth_range_max = 1200
fps = 15 fps = 15
synchronized_images_only = False synchronized_images_only = False

View File

@ -259,18 +259,18 @@ class CameraManager(BaseDevice):
self.logger.debug(f"缓冲区设置耗时: {buffer_time:.1f}ms") self.logger.debug(f"缓冲区设置耗时: {buffer_time:.1f}ms")
# 性能优化设置:禁用可能导致延迟的自动功能 # 性能优化设置:禁用可能导致延迟的自动功能
optimization_start = time.time() # optimization_start = time.time()
try: # try:
# 禁用自动曝光以减少处理时间 # # 禁用自动曝光以减少处理时间
self.cap.set(cv2.CAP_PROP_AUTO_EXPOSURE, 0.25) # 手动曝光模式 # 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_AUTO_WB, 0)
# 设置较低的曝光值以减少延迟 # # 设置较低的曝光值以减少延迟
self.cap.set(cv2.CAP_PROP_EXPOSURE, -6) # self.cap.set(cv2.CAP_PROP_EXPOSURE, -6)
except Exception as e: # except Exception as e:
self.logger.debug(f"设置性能优化参数时出现警告: {e}") # self.logger.debug(f"设置性能优化参数时出现警告: {e}")
optimization_time = (time.time() - optimization_start) * 1000 # optimization_time = (time.time() - optimization_start) * 1000
self.logger.debug(f"性能优化设置耗时: {optimization_time:.1f}ms") # self.logger.debug(f"性能优化设置耗时: {optimization_time:.1f}ms")
# 激进优化:跳过非关键属性设置,只设置必要属性 # 激进优化:跳过非关键属性设置,只设置必要属性
resolution_start = time.time() resolution_start = time.time()

View File

@ -442,7 +442,7 @@ class DeviceCoordinator:
for device_name, device in self.devices.items(): for device_name, device in self.devices.items():
try: try:
# 对深度相机(femtobolt)和普通相机(camera)直接调用初始化和启动推流 # 对深度相机(femtobolt)和普通相机(camera)直接调用初始化和启动推流
if device_name in ['femtobolt', 'camera']: if device_name in ['femtobolt', 'camera',"imu"]:
continue continue
if hasattr(device, '_start_connection_monitor'): if hasattr(device, '_start_connection_monitor'):
@ -476,7 +476,7 @@ class DeviceCoordinator:
for device_name, device in self.devices.items(): for device_name, device in self.devices.items():
try: try:
# 对深度相机(femtobolt)和普通相机(camera)直接调用停止推流 # 对深度相机(femtobolt)和普通相机(camera)直接调用停止推流
if device_name in ['femtobolt', 'camera']: if device_name in ['femtobolt', 'camera',"imu"]:
self.logger.info(f"停止{device_name}设备推流") self.logger.info(f"停止{device_name}设备推流")
# # 调用设备的cleanup方法清理资源,停止推流 # # 调用设备的cleanup方法清理资源,停止推流

View File

@ -120,25 +120,37 @@ class FemtoBoltManager(BaseDevice):
self.reconnect_delay = 3.0 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._min_send_interval = 1.0 / self.send_fps if self.send_fps > 0 else 0.05
self._last_send_time = 0 self._last_send_time = 0
# 编码参数缓存(避免每帧创建数组) # 编码参数缓存(避免每帧创建数组)- 为plt模式优化编码质量
self._encode_param = [int(cv2.IMWRITE_JPEG_QUALITY), int(self.config.get('jpeg_quality', 60))] 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避免每帧计算 # 预计算伽马LUT避免每帧计算
self._gamma_lut = None self._gamma_lut = None
self._current_gamma = None self._current_gamma = None
self._update_gamma_lut() 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_bg = None
self._grid_size = (480, 640) # 默认尺寸 self._grid_size = (480, 640) # 默认尺寸
self.background = None # 用于缓存等高线渲染的背景 self.background = None # 用于缓存等高线渲染的背景
# 自定义彩虹色 colormap参考testfemtobolt.py # 自定义彩虹色 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', '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.use('Agg')
# 创建matplotlib图形对象复用以提高性能 # 创建matplotlib图形对象复用以提高性能
self.fig, self.ax = plt.subplots(figsize=(7, 7)) self.fig, self.ax = plt.subplots(figsize=(9, 6))
self.ax.set_aspect('equal') self.ax.set_aspect('equal')
plt.subplots_adjust(left=0, right=1, top=1, bottom=0) 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}") self.logger.info(f"FemtoBolt设备配置完成 - 算法类型: {self.algorithm_type}, 深度模式: {self.depth_mode}, FPS: {self.fps}")
@ -230,95 +242,119 @@ class FemtoBoltManager(BaseDevice):
self.logger.error(f"优化等高线生成失败: {e}") self.logger.error(f"优化等高线生成失败: {e}")
return None 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): def _generate_contour_image_plt(self, depth):
"""使用matplotlib生成等高线图像完全采用display_x.py的逻辑""" """使用matplotlib生成等高线图像性能优化版本"""
try: 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.clear()
self.ax.set_facecolor((50/255, 50/255, 50/255)) # 设置坐标区域背景色为黑色
# 深度数据过滤与display_x.py完全一致 # 深度数据过滤与display_x.py完全一致
depth[depth > self.depth_range_max] = 0 depth[depth > self.depth_range_max] = 0
depth[depth < self.depth_range_min] = 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) depth = np.ma.masked_equal(depth, 0)
# 绘制背景(与display_x.py完全一致 # 绘制背景(不使用colormap直接显示RGB图像
self.ax.imshow(background, origin='lower', cmap='gray', alpha=0.3) 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.grid(True, which='major', axis='both', color='#333333', 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.set_xticks([]) self.ax.set_xticks([])
# self.ax.set_yticks([]) self.ax.set_yticks([])
# 绘制等高线图并设置原点在上方与display_x.py完全一致 # 优化减少等高线级别从100到30大幅提升性能
import time
start_time = time.perf_counter() 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) self.ax.contourf(depth, levels=80, 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 # 优化直接从canvas获取图像数据避免PNG编码/解码
# self.logger.info(f"contourf绘制耗时: {contourf_time*1000:.2f}ms") try:
# 绘制到canvas
# 将matplotlib图形转换为numpy数组 self.fig.canvas.draw()
buf = io.BytesIO() # 兼容不同版本的matplotlib获取图像数据
savefig_start = time.perf_counter() canvas_width, canvas_height = self.fig.canvas.get_width_height()
savefig_start = time.perf_counter() # 尝试使用新版本的方法
self.fig.savefig(buf, format='png',bbox_inches='tight', pad_inches=0, dpi=75) try:
savefig_time = time.perf_counter() - savefig_start # matplotlib 3.8+ 使用 buffer_rgba()
# self.logger.info(f"savefig保存耗时: {savefig_time*1000:.2f}ms") buf = np.frombuffer(self.fig.canvas.buffer_rgba(), dtype=np.uint8)
buf = buf.reshape((canvas_height, canvas_width, 4))
buf_start = time.perf_counter() # 转换RGBA到RGB
buf.seek(0) img = cv2.cvtColor(buf, cv2.COLOR_RGBA2BGR)
except AttributeError:
# 读取PNG数据并转换为OpenCV格式 try:
img_array = np.frombuffer(buf.getvalue(), dtype=np.uint8) # matplotlib 3.0-3.7 使用 tostring_argb()
buf.close() buf = np.frombuffer(self.fig.canvas.tostring_argb(), dtype=np.uint8)
buf_time = time.perf_counter() - buf_start buf = buf.reshape((canvas_height, canvas_width, 4))
# self.logger.info(f"缓冲区操作耗时: {buf_time*1000:.2f}ms") # 转换ARGB到BGR
img = cv2.cvtColor(buf, cv2.COLOR_BGRA2BGR)
# 解码PNG图像 except AttributeError:
decode_start = time.perf_counter() # 旧版本matplotlib使用 tostring_rgb()
img = cv2.imdecode(img_array, cv2.IMREAD_COLOR) buf = np.frombuffer(self.fig.canvas.tostring_rgb(), dtype=np.uint8)
decode_time = time.perf_counter() - decode_start buf = buf.reshape((canvas_height, canvas_width, 3))
# self.logger.info(f"PNG解码耗时: {decode_time*1000:.2f}ms") # 转换RGB到BGR
# return img img = cv2.cvtColor(buf, cv2.COLOR_RGB2BGR)
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 return img
else: except Exception as e:
self.logger.error("无法解码matplotlib生成的PNG图像") self.logger.error(f"Canvas图像转换失败: {e}")
# 降级到PNG方法
return None return None
except Exception as e: except Exception as e:
self.logger.error(f"生成等高线图像失败: {e}") self.logger.error(f"生成等高线图像失败: {e}")
return None 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: def initialize(self) -> bool:
""" """
初始化FemtoBolt设备 初始化FemtoBolt设备
@ -436,10 +472,10 @@ class FemtoBoltManager(BaseDevice):
# 配置FemtoBolt设备参数 # 配置FemtoBolt设备参数
self.femtobolt_config = self.pykinect.default_configuration 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_format = self.pykinect.K4A_IMAGE_FORMAT_COLOR_BGRA32
self.femtobolt_config.color_resolution = self.pykinect.K4A_COLOR_RESOLUTION_1080P self.femtobolt_config.color_resolution = self.pykinect.K4A_COLOR_RESOLUTION_720P
self.femtobolt_config.camera_fps = self.pykinect.K4A_FRAMES_PER_SECOND_15 self.femtobolt_config.camera_fps = self.pykinect.K4A_FRAMES_PER_SECOND_30 # 30FPS
self.femtobolt_config.synchronized_images_only = False self.femtobolt_config.synchronized_images_only = False
return True return True
@ -492,16 +528,6 @@ class FemtoBoltManager(BaseDevice):
self.logger.warning('FemtoBolt设备启动返回None设备可能未连接') 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设备启动成功') self.logger.info('FemtoBolt设备启动成功')
return True return True
@ -639,12 +665,46 @@ class FemtoBoltManager(BaseDevice):
self.logger.info("FemtoBolt流工作线程启动") self.logger.info("FemtoBolt流工作线程启动")
frame_count = 0 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: try:
while self.is_streaming: while self.is_streaming:
# 发送频率限制 # 动态调整发送间隔,避免延时累积
now = time.time() 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) time.sleep(0.001)
continue continue
@ -656,32 +716,77 @@ class FemtoBoltManager(BaseDevice):
ret, depth_image = capture.get_depth_image() ret, depth_image = capture.get_depth_image()
if ret and depth_image is not None: if ret and depth_image is not None:
# 更新心跳时间,防止连接监控线程判定为超时 # 对于source算法跳过复杂的跳帧逻辑直接处理
self.update_heartbeat() 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) depth_colored_final = self._generate_contour_image_plt(depth_image)
elif self.algorithm_type == 'opencv': elif self.algorithm_type == 'opencv':
depth_colored_final = self._generate_contour_image_opencv(depth_image) 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: if depth_colored_final is None:
# 如果等高线生成失败,跳过这一帧 # 如果等高线生成失败,跳过这一帧
continue continue
# 裁剪处理(推迟到显示阶段) # 优化裁剪处理(缓存裁剪参数
h, w = depth_colored_final.shape[:2] h, w = depth_colored_final.shape[:2]
# self.logger.info(f"深度图像尺寸: 宽={w}, 高={h}")
target_width = h // 2 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) success, buffer = cv2.imencode('.jpg', display_image, self._encode_param)
encode_time = time.time() - encode_start
if success and self._socketio: 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', { self._socketio.emit('femtobolt_frame', {
'depth_image': jpg_as_text, 'depth_image': jpg_as_text,
'frame_count': frame_count, 'frame_count': frame_count,
@ -696,8 +801,30 @@ class FemtoBoltManager(BaseDevice):
frame_count += 1 frame_count += 1
self._last_send_time = now 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: else:
time.sleep(0.005) time.sleep(0.005)
except Exception as e: except Exception as e:
@ -726,6 +853,48 @@ class FemtoBoltManager(BaseDevice):
self.is_streaming = False self.is_streaming = False
self.logger.info("FemtoBolt流工作线程结束") 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): def _update_statistics(self):
""" """

View File

@ -469,7 +469,7 @@ class IMUManager(BaseDevice):
# 设备配置 # 设备配置
self.port = config.get('port', 'COM7') self.port = config.get('port', 'COM7')
self.baudrate = config.get('baudrate', 9600) 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.use_mock = config.get('use_mock', False) # 保持向后兼容
self.mac_address = config.get('mac_address', '') self.mac_address = config.get('mac_address', '')
# IMU设备实例 # IMU设备实例
@ -701,7 +701,7 @@ class IMUManager(BaseDevice):
# self.last_valid_data = data # self.last_valid_data = data
# 更新心跳时间,防止连接监控线程判定为超时 # 更新心跳时间,防止连接监控线程判定为超时
self.update_heartbeat() # self.update_heartbeat()
# 发送数据到前端 # 发送数据到前端
if self._socketio: if self._socketio:

View File

@ -26,12 +26,12 @@ backend = directshow
[FEMTOBOLT] [FEMTOBOLT]
enabled = True enabled = True
algorithm_type = opencv algorithm_type = plt
color_resolution = 1080P color_resolution = 1080P
depth_mode = NFOV_2X2BINNED depth_mode = NFOV_2X2BINNED
camera_fps = 20 camera_fps = 20
depth_range_min = 1000 depth_range_min = 800
depth_range_max = 1400 depth_range_max = 1200
fps = 15 fps = 15
synchronized_images_only = False synchronized_images_only = False

Binary file not shown.

View File

@ -119,12 +119,12 @@ function createWindow() {
} else { } else {
// 启动后端服务 // 启动后端服务
startBackendService(); startBackendService();
// 延迟1秒后启动本地HTTP服务器 // 延迟2秒后启动本地HTTP服务器
setTimeout(() => { setTimeout(() => {
startLocalServer(() => { startLocalServer(() => {
mainWindow.loadURL('http://localhost:3000'); mainWindow.loadURL('http://localhost:3000');
}); });
}, 1000); }, 2000);
} }
// 窗口就绪后再显示,避免白屏/闪烁 // 窗口就绪后再显示,避免白屏/闪烁

View File

@ -136,7 +136,7 @@
</div> </div>
<div class="gauge-group-box" style="justify-content: center;"> <div class="gauge-group-box" style="justify-content: center;">
<div class="gauge-group-box-text1">左最大<span class="gauge-group-box-text2">{{ <div class="gauge-group-box-text1">左最大<span class="gauge-group-box-text2">{{
headPoseMaxValues.rotationLeftMax.toFixed(1) }}°</span></div> headPoseMaxValues.rotationRightMax.toFixed(1) }}°</span></div>
</div> </div>
</div> </div>
@ -164,7 +164,7 @@
</div> </div>
<div class="gauge-group-box" style="justify-content: center;"> <div class="gauge-group-box" style="justify-content: center;">
<div class="gauge-group-box-text1">俯最大<span class="gauge-group-box-text2">{{ <div class="gauge-group-box-text1">俯最大<span class="gauge-group-box-text2">{{
headPoseMaxValues.pitchDownMax.toFixed(1) }}°</span></div> headPoseMaxValues.pitchUpMax.toFixed(1) }}°</span></div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -50,7 +50,7 @@
<!-- 记住密码 & 忘记密码 --> <!-- 记住密码 & 忘记密码 -->
<div class="form-footer"> <div class="form-footer">
<el-checkbox v-model="form.remember" class="remember-checkbox">记住密码</el-checkbox> <el-checkbox v-model="form.remember" class="remember-checkbox" @change="handleRememberChange">记住账号及密码</el-checkbox>
<a href="#" class="forgot-link" @click="handleForgotPassword">忘记密码</a> <a href="#" class="forgot-link" @click="handleForgotPassword">忘记密码</a>
</div> </div>
@ -210,7 +210,7 @@
</template> </template>
<script setup> <script setup>
import { ref } from 'vue' import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import { useAuthStore } from '../stores' import { useAuthStore } from '../stores'
@ -233,6 +233,30 @@ const form = ref({
remember: false remember: false
}) })
// localStorage
onMounted(() => {
const savedCredentials = localStorage.getItem('rememberedCredentials')
if (savedCredentials) {
try {
const credentials = JSON.parse(savedCredentials)
form.value.account = credentials.account || ''
form.value.password = credentials.password || ''
form.value.remember = true
} catch (error) {
console.error('读取保存的账号密码失败:', error)
//
localStorage.removeItem('rememberedCredentials')
}
}
})
// rememberlocalStorage
const handleRememberChange = () => {
if (!form.value.remember) {
localStorage.removeItem('rememberedCredentials')
}
}
// //
const passwordVisible = ref(false) const passwordVisible = ref(false)
@ -393,13 +417,13 @@ const handleRegisterSubmit = async () => {
const handleLogin = async () => { const handleLogin = async () => {
// //
if (!form.value.account) { if (!form.value.account) {
showError('用户名或密码错误,重新输入!') showError('请输入登录账号')
return return
} }
// //
if (!form.value.password) { if (!form.value.password) {
showError('用户名或密码错误,重新输入!') showError('请输入密码')
return return
} }
@ -407,28 +431,30 @@ const handleLogin = async () => {
isLoading.value = true isLoading.value = true
const result = await authStore.login({ const result = await authStore.login({
username: form.value.account, username: form.value.account,
password: form.value.password, password: form.value.password
remember: form.value.remember
}) })
if (result.success) { if (result.success) {
// localStorage
if (form.value.remember) {
const credentialsToSave = {
account: form.value.account,
password: form.value.password
}
localStorage.setItem('rememberedCredentials', JSON.stringify(credentialsToSave))
} else {
//
localStorage.removeItem('rememberedCredentials')
}
ElMessage.success('登录成功') ElMessage.success('登录成功')
router.push('/') router.push('/')
} else { } else {
// //
const errorMsg = String(result.error || '') showError(result.error)
if (errorMsg.includes('用户不存在')) {
showError('用户名或密码错误,请重新输入!')
} else if (errorMsg.includes('密码错误')) {
showError('用户名或密码错误,请重新输入!')
} else if (errorMsg.includes('用户未激活')) {
showError('账户未激活,请联系管理员激活后再登录')
} else {
showError('用户名或密码错误,请重新输入!')
}
} }
} catch (error) { } catch (error) {
showError('用户名或密码错误,请重新输入') showError('登录失败,请检查网络连接后重试!')
} finally { } finally {
isLoading.value = false isLoading.value = false
} }

View File

@ -905,6 +905,14 @@ const deleteClick = async (row,row2) => {
const sessionsDelById = async (row) => { const sessionsDelById = async (row) => {
// checking
if (row.status === 'checking') {
ElMessage.warning({
message: '平衡体态检查中,不能删除!',
duration: 3000
});
return;
}
ElMessageBox.confirm( ElMessageBox.confirm(
'确定义删除此条数据?', '确定义删除此条数据?',