修正了bug
This commit is contained in:
parent
76c61d75df
commit
ed0c44ed6d
@ -242,11 +242,14 @@ def api_health_check():
|
||||
@app.route('/api/auth/login', methods=['POST'])
|
||||
def login():
|
||||
"""用户登录"""
|
||||
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')
|
||||
remember = data.get('remember', False)
|
||||
if not username or not password:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
@ -295,8 +298,25 @@ def login():
|
||||
}), 401
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f'登录失败: {e}')
|
||||
return jsonify({'success': False, 'message': '登录失败'}), 500
|
||||
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'])
|
||||
def register():
|
||||
|
@ -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
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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方法清理资源,停止推流
|
||||
|
@ -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,95 +242,119 @@ 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")
|
||||
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:
|
||||
|
||||
|
||||
# 将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]
|
||||
# 绘制到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
|
||||
else:
|
||||
self.logger.error("无法解码matplotlib生成的PNG图像")
|
||||
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:
|
||||
"""
|
||||
初始化FemtoBolt设备
|
||||
@ -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
|
||||
@ -492,16 +528,6 @@ class FemtoBoltManager(BaseDevice):
|
||||
self.logger.warning('FemtoBolt设备启动返回None,设备可能未连接')
|
||||
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 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
|
||||
display_image = depth_colored_final[:, left:right]
|
||||
self._crop_params = ((h, w), left, right, target_width)
|
||||
else:
|
||||
self._crop_params = ((h, w), None, None, w)
|
||||
|
||||
# 推送SocketIO
|
||||
# 应用裁剪
|
||||
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,
|
||||
@ -696,8 +801,30 @@ class FemtoBoltManager(BaseDevice):
|
||||
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:
|
||||
@ -726,6 +853,48 @@ class FemtoBoltManager(BaseDevice):
|
||||
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):
|
||||
"""
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
||||
|
Binary file not shown.
BIN
backend/dll/femtobolt/femtobolt.zip
Normal file
BIN
backend/dll/femtobolt/femtobolt.zip
Normal file
Binary file not shown.
@ -119,12 +119,12 @@ function createWindow() {
|
||||
} else {
|
||||
// 启动后端服务
|
||||
startBackendService();
|
||||
// 延迟1秒后启动本地HTTP服务器
|
||||
// 延迟2秒后启动本地HTTP服务器
|
||||
setTimeout(() => {
|
||||
startLocalServer(() => {
|
||||
mainWindow.loadURL('http://localhost:3000');
|
||||
});
|
||||
}, 1000);
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
// 窗口就绪后再显示,避免白屏/闪烁
|
||||
|
@ -136,7 +136,7 @@
|
||||
</div>
|
||||
<div class="gauge-group-box" style="justify-content: center;">
|
||||
<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>
|
||||
@ -164,7 +164,7 @@
|
||||
</div>
|
||||
<div class="gauge-group-box" style="justify-content: center;">
|
||||
<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>
|
||||
|
@ -50,7 +50,7 @@
|
||||
|
||||
<!-- 记住密码 & 忘记密码 -->
|
||||
<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>
|
||||
</div>
|
||||
|
||||
@ -210,7 +210,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { useAuthStore } from '../stores'
|
||||
@ -233,6 +233,30 @@ const form = ref({
|
||||
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')
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 监听remember字段变化,当用户取消记住密码时清除localStorage
|
||||
const handleRememberChange = () => {
|
||||
if (!form.value.remember) {
|
||||
localStorage.removeItem('rememberedCredentials')
|
||||
}
|
||||
}
|
||||
|
||||
// 密码可见性控制
|
||||
const passwordVisible = ref(false)
|
||||
|
||||
@ -393,13 +417,13 @@ const handleRegisterSubmit = async () => {
|
||||
const handleLogin = async () => {
|
||||
// 验证用户名
|
||||
if (!form.value.account) {
|
||||
showError('用户名或密码错误,请重新输入!')
|
||||
showError('请输入登录账号!')
|
||||
return
|
||||
}
|
||||
|
||||
// 验证密码
|
||||
if (!form.value.password) {
|
||||
showError('用户名或密码错误,请重新输入!')
|
||||
showError('请输入密码!')
|
||||
return
|
||||
}
|
||||
|
||||
@ -407,28 +431,30 @@ const handleLogin = async () => {
|
||||
isLoading.value = true
|
||||
const result = await authStore.login({
|
||||
username: form.value.account,
|
||||
password: form.value.password,
|
||||
remember: form.value.remember
|
||||
password: form.value.password
|
||||
})
|
||||
|
||||
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('登录成功')
|
||||
router.push('/')
|
||||
} else {
|
||||
// 根据错误类型显示具体提示
|
||||
const errorMsg = String(result.error || '')
|
||||
if (errorMsg.includes('用户不存在')) {
|
||||
showError('用户名或密码错误,请重新输入!')
|
||||
} else if (errorMsg.includes('密码错误')) {
|
||||
showError('用户名或密码错误,请重新输入!')
|
||||
} else if (errorMsg.includes('用户未激活')) {
|
||||
showError('账户未激活,请联系管理员激活后再登录')
|
||||
} else {
|
||||
showError('用户名或密码错误,请重新输入!')
|
||||
}
|
||||
showError(result.error)
|
||||
}
|
||||
} catch (error) {
|
||||
showError('用户名或密码错误,请重新输入!')
|
||||
showError('登录失败,请检查网络连接后重试!')
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
|
@ -905,6 +905,14 @@ const deleteClick = async (row,row2) => {
|
||||
|
||||
|
||||
const sessionsDelById = async (row) => {
|
||||
// 检查状态是否为 checking
|
||||
if (row.status === 'checking') {
|
||||
ElMessage.warning({
|
||||
message: '平衡体态检查中,不能删除!',
|
||||
duration: 3000
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
ElMessageBox.confirm(
|
||||
'确定义删除此条数据?',
|
||||
|
Loading…
Reference in New Issue
Block a user