Commit 29a54b5f by Yuhaibo

1

parent 81c47ac5
...@@ -306,7 +306,7 @@ exe = EXE( ...@@ -306,7 +306,7 @@ exe = EXE(
bootloader_ignore_signals=False, bootloader_ignore_signals=False,
strip=False, # 不strip二进制文件 strip=False, # 不strip二进制文件
upx=False, # 不使用UPX压缩(避免兼容性问题) upx=False, # 不使用UPX压缩(避免兼容性问题)
console=True, # True表示显示控制台窗口(用于调试) console=False, # True表示显示控制台窗口(用于调试)
disable_windowed_traceback=False, disable_windowed_traceback=False,
target_arch=None, target_arch=None,
codesign_identity=None, codesign_identity=None,
......
...@@ -147,7 +147,7 @@ except ImportError: ...@@ -147,7 +147,7 @@ except ImportError:
# 支持相对导入(作为模块运行)和绝对导入(独立运行) # 支持相对导入(作为模块运行)和绝对导入(独立运行)
try: try:
from .widgets import MenuBar from .widgets import MenuBar
from .widgets.font_manager import FontManager
# 导入所有处理器 (Mixin类) # 导入所有处理器 (Mixin类)
from .handlers import ( from .handlers import (
ChannelPanelHandler, ChannelPanelHandler,
......
...@@ -689,6 +689,9 @@ class MainWindow( ...@@ -689,6 +689,9 @@ class MainWindow(
# 默认显示模式1 # 默认显示模式1
self.videoLayoutStack.setCurrentIndex(0) self.videoLayoutStack.setCurrentIndex(0)
self._video_layout_mode = 0 # 0=默认模式, 1=曲线模式 self._video_layout_mode = 0 # 0=默认模式, 1=曲线模式
# 初次创建页面后刷新一次按钮状态(默认布局)
self._updateCurveButtonsByMission()
return page return page
...@@ -989,6 +992,12 @@ class MainWindow( ...@@ -989,6 +992,12 @@ class MainWindow(
# ========== 曲线面板信号 ========== # ========== 曲线面板信号 ==========
self.curvePanel.backClicked.connect(self.switchToRealTimeDetectionPage) # 返回实时检测管理页面 self.curvePanel.backClicked.connect(self.switchToRealTimeDetectionPage) # 返回实时检测管理页面
# 页面堆栈切换与视频子布局切换时,刷新曲线按钮状态
if hasattr(self, 'stackedWidget'):
self.stackedWidget.currentChanged.connect(self._onMainStackChanged)
if hasattr(self, 'videoLayoutStack'):
self.videoLayoutStack.currentChanged.connect(self._onVideoLayoutChanged)
def _initMenuBar(self): def _initMenuBar(self):
"""初始化菜单栏(委托给MenuBarHandler处理)""" """初始化菜单栏(委托给MenuBarHandler处理)"""
...@@ -1026,6 +1035,60 @@ class MainWindow( ...@@ -1026,6 +1035,60 @@ class MainWindow(
"""显示实时检测管理页面""" """显示实时检测管理页面"""
self.stackedWidget.setCurrentWidget(self.videoPage) self.stackedWidget.setCurrentWidget(self.videoPage)
self.statusBar().showMessage(self.tr("当前页面: 实时检测管理")) self.statusBar().showMessage(self.tr("当前页面: 实时检测管理"))
# 切到视频页时刷新默认布局下的曲线按钮状态
self._updateCurveButtonsByMission()
def _onMainStackChanged(self, index: int):
"""主页面堆栈变化时,根据当前页面刷新按钮状态"""
try:
if index == self.PAGE_VIDEO:
self._updateCurveButtonsByMission()
except Exception:
pass
def _onVideoLayoutChanged(self, index: int):
"""视频页面子布局变化时刷新按钮状态"""
try:
self._video_layout_mode = index
# 仅在默认模式下需要检查并启用曲线按钮
if index == 0:
self._updateCurveButtonsByMission()
except Exception:
pass
def _updateCurveButtonsByMission(self):
"""
根据每个通道的任务标签是否有任务,启用或禁用“查看曲线”按钮。
仅在默认模式布局(任务表格 + 2x2通道面板)下生效。
"""
try:
# 仅在视频页默认布局下处理
if (
not hasattr(self, 'stackedWidget')
or self.stackedWidget.currentIndex() != self.PAGE_VIDEO
or not hasattr(self, 'videoLayoutStack')
or self.videoLayoutStack.currentIndex() != 0
):
return
# 遍历四个通道面板
for panel in getattr(self, 'channelPanels', []):
# 任务标签文本
mission_label = getattr(panel, 'taskLabel', None)
text = ''
if mission_label and hasattr(mission_label, 'text'):
text = mission_label.text().strip()
# 判断是否有有效任务
has_mission = bool(text) and text not in ('请选择任务', '未选择任务')
# 找到“查看曲线”按钮并设置可用性
curve_btn = getattr(panel, 'btn_curve', None)
if curve_btn and hasattr(curve_btn, 'setEnabled'):
curve_btn.setEnabled(has_mission)
except Exception:
# 安静失败,不影响主流程
pass
def showModelPage(self): def showModelPage(self):
"""显示模型管理页面""" """显示模型管理页面"""
......
...@@ -1207,7 +1207,7 @@ class ModelTestHandler: ...@@ -1207,7 +1207,7 @@ class ModelTestHandler:
height = area_data['liquid_height'] height = area_data['liquid_height']
print(f"[液位检测] {area_name}: {height}mm") print(f"[液位检测] {area_name}: {height}mm")
self._showTestDetectionResult(test_frame, detection_result, boxes, bottoms, tops) self._showTestDetectionResult(test_frame, detection_result, boxes, bottoms, tops, actual_heights)
print(f"[液位检测] 检测结果已显示在右侧面板中") print(f"[液位检测] 检测结果已显示在右侧面板中")
else: else:
...@@ -1217,7 +1217,7 @@ class ModelTestHandler: ...@@ -1217,7 +1217,7 @@ class ModelTestHandler:
print(f" 2. 检测区域设置不正确(位置偏移)") print(f" 2. 检测区域设置不正确(位置偏移)")
print(f" 3. 图像质量或光照条件不佳") print(f" 3. 图像质量或光照条件不佳")
self._showTestDetectionResult(test_frame, None, boxes, bottoms, tops) self._showTestDetectionResult(test_frame, None, boxes, bottoms, tops, actual_heights)
print(f"[液位检测] 原始图像和标注信息已显示在右侧面板中") print(f"[液位检测] 原始图像和标注信息已显示在右侧面板中")
...@@ -1227,7 +1227,7 @@ class ModelTestHandler: ...@@ -1227,7 +1227,7 @@ class ModelTestHandler:
traceback.print_exc() traceback.print_exc()
try: try:
self._showTestDetectionResult(test_frame, None, boxes, bottoms, tops) self._showTestDetectionResult(test_frame, None, boxes, bottoms, tops, actual_heights)
print(f"[液位检测] 原始图像已显示(检测失败)") print(f"[液位检测] 原始图像已显示(检测失败)")
except Exception as show_error: except Exception as show_error:
print(f"[液位检测] 显示原始图像也失败: {show_error}") print(f"[液位检测] 显示原始图像也失败: {show_error}")
...@@ -1518,8 +1518,17 @@ class ModelTestHandler: ...@@ -1518,8 +1518,17 @@ class ModelTestHandler:
if hasattr(self, '_detection_stopped'): if hasattr(self, '_detection_stopped'):
print(f"[视频检测] 最终停止标志: {self._detection_stopped}") print(f"[视频检测] 最终停止标志: {self._detection_stopped}")
def _showTestDetectionResult(self, original_frame, detection_result, boxes, bottoms, tops): def _showTestDetectionResult(self, original_frame, detection_result, boxes, bottoms, tops, actual_heights=None):
"""在显示面板中显示检测结果(带液位线的图像)""" """在显示面板中显示检测结果(带液位线的图像)
Args:
original_frame: 原始图像帧
detection_result: 检测结果
boxes: 检测框列表
bottoms: 底部点列表
tops: 顶部点列表
actual_heights: 实际容器高度列表(毫米),如果为None则使用默认值20mm
"""
try: try:
from datetime import datetime from datetime import datetime
...@@ -1527,6 +1536,12 @@ class ModelTestHandler: ...@@ -1527,6 +1536,12 @@ class ModelTestHandler:
print(f"[检测结果显示] 原始帧尺寸: {original_frame.shape}") print(f"[检测结果显示] 原始帧尺寸: {original_frame.shape}")
print(f"[检测结果显示] 检测区域数量: {len(boxes)}") print(f"[检测结果显示] 检测区域数量: {len(boxes)}")
print(f"[检测结果显示] 检测结果: {detection_result}") print(f"[检测结果显示] 检测结果: {detection_result}")
print(f"[检测结果显示] 容器实际高度: {actual_heights}")
# 如果没有提供actual_heights,使用默认值
if actual_heights is None:
actual_heights = [20.0] * len(boxes)
print(f"[检测结果显示] 未提供容器高度,使用默认值: {actual_heights}")
# 复制原始帧 # 复制原始帧
result_frame = original_frame.copy() result_frame = original_frame.copy()
...@@ -1541,6 +1556,9 @@ class ModelTestHandler: ...@@ -1541,6 +1556,9 @@ class ModelTestHandler:
bottom_y = bottoms[i][1] bottom_y = bottoms[i][1]
top_y = tops[i][1] top_y = tops[i][1]
# 获取该区域的实际容器高度
actual_height = actual_heights[i] if i < len(actual_heights) else 20.0
# 提取液位高度(默认为0) # 提取液位高度(默认为0)
liquid_height = 0.0 liquid_height = 0.0
...@@ -1550,7 +1568,7 @@ class ModelTestHandler: ...@@ -1550,7 +1568,7 @@ class ModelTestHandler:
area_name, area_data = area_items[i] area_name, area_data = area_items[i]
if 'liquid_height' in area_data: if 'liquid_height' in area_data:
liquid_height = area_data['liquid_height'] liquid_height = area_data['liquid_height']
print(f"[检测结果显示] 区域{i+1}: 检测到液位 {liquid_height}mm") print(f"[检测结果显示] 区域{i+1}: 检测到液位 {liquid_height}mm (容器高度: {actual_height}mm)")
else: else:
print(f"[检测结果显示] 区域{i+1}: 未检测到液位,使用默认值 0mm") print(f"[检测结果显示] 区域{i+1}: 未检测到液位,使用默认值 0mm")
else: else:
...@@ -1558,10 +1576,10 @@ class ModelTestHandler: ...@@ -1558,10 +1576,10 @@ class ModelTestHandler:
else: else:
print(f"[检测结果显示] 区域{i+1}: 无检测结果,使用默认值 0mm") print(f"[检测结果显示] 区域{i+1}: 无检测结果,使用默认值 0mm")
# 计算液位线的Y坐标 # 计算液位线的Y坐标 - 使用实际容器高度
container_height = bottom_y - top_y container_height_px = bottom_y - top_y
liquid_ratio = min(1.0, max(0.0, liquid_height / 100.0)) liquid_ratio = min(1.0, max(0.0, liquid_height / actual_height))
liquid_y = int(bottom_y - container_height * liquid_ratio) liquid_y = int(bottom_y - container_height_px * liquid_ratio)
# 绘制液位线(红色) # 绘制液位线(红色)
cv2.line(result_frame, (left, liquid_y), (right, liquid_y), (0, 0, 255), 3) cv2.line(result_frame, (left, liquid_y), (right, liquid_y), (0, 0, 255), 3)
......
...@@ -973,7 +973,7 @@ class ModelTrainingHandler(ModelTestHandler): ...@@ -973,7 +973,7 @@ class ModelTrainingHandler(ModelTestHandler):
# 强制垃圾回收,释放可能的文件句柄 # 强制垃圾回收,释放可能的文件句柄
gc.collect() gc.collect()
time.sleep(0.5) time.sleep(1.0) # 增加初始等待时间
success_count = 0 success_count = 0
fail_count = 0 fail_count = 0
...@@ -983,8 +983,8 @@ class ModelTrainingHandler(ModelTestHandler): ...@@ -983,8 +983,8 @@ class ModelTrainingHandler(ModelTestHandler):
deleted = False deleted = False
last_error = None last_error = None
for attempt in range(5): # 最多重试5次 for attempt in range(10): # 增加到10次重试
self._appendLog(f"[调试] 尝试删除 (第{attempt+1}/5次)...\n") self._appendLog(f"[调试] 尝试删除 (第{attempt+1}/10次)...\n")
try: try:
if os.path.exists(pt_file): if os.path.exists(pt_file):
os.remove(pt_file) os.remove(pt_file)
...@@ -998,7 +998,9 @@ class ModelTrainingHandler(ModelTestHandler): ...@@ -998,7 +998,9 @@ class ModelTrainingHandler(ModelTestHandler):
except OSError as e: except OSError as e:
last_error = str(e) last_error = str(e)
self._appendLog(f"[调试] ✗ 删除失败: {last_error}\n") self._appendLog(f"[调试] ✗ 删除失败: {last_error}\n")
if attempt < 4: if attempt < 9:
# 每次重试前强制垃圾回收
gc.collect()
self._appendLog(f"[调试] 等待0.5秒后重试...\n") self._appendLog(f"[调试] 等待0.5秒后重试...\n")
time.sleep(0.5) # 等待0.5秒后重试 time.sleep(0.5) # 等待0.5秒后重试
...@@ -1886,7 +1888,7 @@ class ModelTrainingHandler(ModelTestHandler): ...@@ -1886,7 +1888,7 @@ class ModelTrainingHandler(ModelTestHandler):
def add_box(self, cx, cy, size): def add_box(self, cx, cy, size):
""" """
添加检测区域,并自动计算顶部点和底部点 添加检测区域(不自动生成顶部点和底部点)
Args: Args:
cx: 框中心x坐标 cx: 框中心x坐标
...@@ -1894,32 +1896,17 @@ class ModelTrainingHandler(ModelTestHandler): ...@@ -1894,32 +1896,17 @@ class ModelTrainingHandler(ModelTestHandler):
size: 框的边长 size: 框的边长
""" """
self.boxes.append((cx, cy, size)) self.boxes.append((cx, cy, size))
# 自动计算并添加底部点和顶部点
# 底部点:box底边y坐标 - box高度的10%,x为中心
half_size = size / 2
bottom_y = cy + half_size - (size * 0.1) # 底边y - 10%高度
bottom_x = cx # x位置为box轴对称中心
self.bottom_points.append((int(bottom_x), int(bottom_y)))
# 顶部点:box顶边y坐标 + box高度的10%,x为中心
top_y = cy - half_size + (size * 0.1) # 顶边y + 10%高度
top_x = cx # x位置为box轴对称中心
self.top_points.append((int(top_x), int(top_y)))
print(f"添加框: 中心({cx}, {cy}), 边长{size}") print(f"添加框: 中心({cx}, {cy}), 边长{size}")
print(f" 底部点: ({int(bottom_x)}, {int(bottom_y)})")
print(f" 顶部点: ({int(top_x)}, {int(top_y)})")
def add_bottom(self, x, y): def add_bottom(self, x, y):
"""添加底部标记点(保留用于兼容性,但不再使用)""" """添加底部标记点"""
# 此方法保留但不再使用,因为底部点会在add_box时自动添加 self.bottom_points.append((int(x), int(y)))
pass print(f"添加底部点: ({int(x)}, {int(y)})")
def add_top(self, x, y): def add_top(self, x, y):
"""添加顶部标记点(保留用于兼容性,但不再使用)""" """添加顶部标记点"""
# 此方法保留但不再使用,因为顶部点会在add_box时自动添加 self.top_points.append((int(x), int(y)))
pass print(f"添加顶部点: ({int(x)}, {int(y)})")
def get_mission_results(self): def get_mission_results(self):
"""获取标注结果""" """获取标注结果"""
...@@ -2946,7 +2933,12 @@ class ModelTrainingHandler(ModelTestHandler): ...@@ -2946,7 +2933,12 @@ class ModelTrainingHandler(ModelTestHandler):
device = model_params.get('device', '未知') device = model_params.get('device', '未知')
# 读取训练结果(如果有results.csv) # 读取训练结果(如果有results.csv)
results_csv = os.path.join(model_dir, 'results.csv') # 优先从training_results目录读取
results_csv = os.path.join(model_dir, 'training_results', 'results.csv')
if not os.path.exists(results_csv):
# 备用:从模型根目录读取
results_csv = os.path.join(model_dir, 'results.csv')
final_epoch = "" final_epoch = ""
train_loss = "" train_loss = ""
val_loss = "" val_loss = ""
...@@ -3557,86 +3549,111 @@ class ModelTrainingHandler(ModelTestHandler): ...@@ -3557,86 +3549,111 @@ class ModelTrainingHandler(ModelTestHandler):
str: data.yaml文件路径,失败返回None str: data.yaml文件路径,失败返回None
""" """
try: try:
self._appendLog(f"正在处理数据集目录: {dataset_folder}\n")
# 检查是否已经有data.yaml文件 # 检查是否已经有data.yaml文件
existing_yaml = os.path.join(dataset_folder, 'data.yaml') existing_yaml = os.path.join(dataset_folder, 'data.yaml')
if os.path.exists(existing_yaml): if os.path.exists(existing_yaml):
self._appendLog(f"使用现有配置文件: {existing_yaml}\n") self._appendLog(f"使用现有配置文件: {existing_yaml}\n")
return existing_yaml return existing_yaml
# 直接在数据集目录创建配置文件 # 创建data.yaml文件路径
data_yaml_path = os.path.join(dataset_folder, 'data.yaml') data_yaml_path = os.path.join(dataset_folder, 'data.yaml')
# 智能检测数据集目录结构 # 支持的图片格式
image_extensions = ['.jpg', '.jpeg', '.png', '.bmp', '.tif', '.tiff']
# 检查目录中是否包含图片
def has_images(folder):
if not os.path.exists(folder) or not os.path.isdir(folder):
return False
try:
for f in os.listdir(folder):
if os.path.isfile(os.path.join(folder, f)) and \
any(f.lower().endswith(ext) for ext in image_extensions):
return True
except Exception as e:
self._appendLog(f"检查图片时出错: {str(e)}\n")
return False
return False
# 检查常见的数据集结构
train_dir = None train_dir = None
val_dir = None val_dir = None
# 方案1: 标准YOLO格式 (dataset_folder/images/train) # 1. 检查是否是标准YOLO格式: dataset/images/train, dataset/images/val
images_train_dir = os.path.join(dataset_folder, 'images', 'train') if os.path.exists(os.path.join(dataset_folder, 'images', 'train')):
images_val_dir = os.path.join(dataset_folder, 'images', 'val') train_dir = os.path.join(dataset_folder, 'images', 'train')
val_dir = os.path.join(dataset_folder, 'images', 'val')
if os.path.exists(images_train_dir): self._appendLog("检测到标准YOLO数据集结构\n")
train_dir = images_train_dir # 2. 检查是否直接包含图片
val_dir = images_val_dir if os.path.exists(images_val_dir) else images_train_dir elif has_images(dataset_folder):
train_dir = dataset_folder
# 方案2: dataset_folder本身就是images目录 (例如: dataset/train/images) val_dir = dataset_folder
elif os.path.basename(dataset_folder) == 'images': self._appendLog("检测到包含图片的目录,将用于训练和验证\n")
parent_dir = os.path.dirname(dataset_folder) # 3. 检查是否包含images目录
# 检查是否在train或val目录下 elif os.path.exists(os.path.join(dataset_folder, 'images')):
if os.path.basename(parent_dir) in ['train', 'val']: images_dir = os.path.join(dataset_folder, 'images')
# dataset_folder是 dataset/train/images if has_images(images_dir):
train_dir = dataset_folder train_dir = images_dir
val_dir = dataset_folder val_dir = images_dir
else: self._appendLog("检测到包含图片的images目录\n")
# dataset_folder是简化格式的images目录 (dataset/images)
train_dir = dataset_folder # 4. 如果以上都不匹配,尝试在子目录中查找
val_dir = dataset_folder if train_dir is None:
for root, dirs, files in os.walk(dataset_folder):
# 方案3: 直接包含图片的文件夹(递归搜索) if has_images(root):
else: train_dir = root
image_extensions = ['.jpg', '.jpeg', '.png', '.bmp', '.tif', '.tiff'] val_dir = root
self._appendLog(f"在子目录中找到图片: {root}\n")
# 先检查当前目录 break
files = os.listdir(dataset_folder)
has_images = any(os.path.isfile(os.path.join(dataset_folder, f)) and
f.lower().endswith(tuple(image_extensions)) for f in files)
if has_images:
train_dir = dataset_folder
val_dir = dataset_folder
else:
# 递归搜索子目录,找到第一个包含图片的目录
for subdir in files:
subdir_path = os.path.join(dataset_folder, subdir)
if os.path.isdir(subdir_path):
subdir_files = os.listdir(subdir_path)
has_sub_images = any(os.path.isfile(os.path.join(subdir_path, f)) and
f.lower().endswith(tuple(image_extensions)) for f in subdir_files)
if has_sub_images:
train_dir = subdir_path
val_dir = subdir_path
break
if not train_dir: # 如果还是没找到图片,返回错误
self._appendLog(f"错误: 无法确定训练图片目录\n") if train_dir is None or not has_images(train_dir):
error_msg = f"错误: 未在 {dataset_folder} 及其子目录中找到有效的图片文件。\n"
error_msg += f"支持的图片格式: {', '.join(image_extensions)}\n"
error_msg += "请确保选择的目录中包含图片文件。\n"
self._appendLog(error_msg)
QtWidgets.QMessageBox.critical(
self.training_panel,
"错误",
error_msg
)
return None return None
# 创建data.yaml配置 # 创建data.yaml内容
data_config = { data_config = {
'train': train_dir, 'path': os.path.dirname(os.path.abspath(dataset_folder)), # 数据集根目录
'val': val_dir, 'train': os.path.relpath(train_dir, os.path.dirname(dataset_folder)).replace('\\', '/'),
'nc': 1, # 类别数量,液位检测通常是单类别 'val': os.path.relpath(val_dir, os.path.dirname(dataset_folder)).replace('\\', '/'),
'nc': 1, # 类别数量
'names': ['liquid_level'] # 类别名称 'names': ['liquid_level'] # 类别名称
} }
# 确保验证集目录存在,如果不存在则使用训练集
if not os.path.exists(os.path.join(os.path.dirname(dataset_folder), data_config['val'])):
data_config['val'] = data_config['train']
self._appendLog("警告: 验证集目录不存在,将使用训练集进行验证\n")
# 写入文件
with open(data_yaml_path, 'w', encoding='utf-8') as f: with open(data_yaml_path, 'w', encoding='utf-8') as f:
yaml.dump(data_config, f, default_flow_style=False, allow_unicode=True) yaml.dump(data_config, f, default_flow_style=False, allow_unicode=True)
self._appendLog(f"创建配置文件: {data_yaml_path}\n") self._appendLog(f"成功创建配置文件: {data_yaml_path}\n")
self._appendLog(f"训练集目录: {data_config['train']}\n")
self._appendLog(f"验证集目录: {data_config['val']}\n")
return data_yaml_path return data_yaml_path
except Exception as e: except Exception as e:
self._appendLog(f"创建配置文件失败: {str(e)}\n") error_msg = f"创建配置文件失败: {str(e)}\n"
self._appendLog(error_msg)
import traceback import traceback
self._appendLog(traceback.format_exc()) self._appendLog(traceback.format_exc())
QtWidgets.QMessageBox.critical(
self.training_panel,
"错误",
f"创建数据集配置文件时出错:\n{str(e)}"
)
return None return None
\ No newline at end of file
...@@ -738,6 +738,7 @@ class TrainingWorker(QThread): ...@@ -738,6 +738,7 @@ class TrainingWorker(QThread):
train_dir = os.path.join(model_output_dir, "train") train_dir = os.path.join(model_output_dir, "train")
weights_dir = os.path.join(train_dir, "weights") weights_dir = os.path.join(train_dir, "weights")
self.training_report["weights_dir"] = weights_dir self.training_report["weights_dir"] = weights_dir
self.training_report["model_output_dir"] = model_output_dir # 保存模型输出目录
# 设置training_results目录用于存放训练结果文件 # 设置training_results目录用于存放训练结果文件
training_results_dir = os.path.join(model_output_dir, "training_results") training_results_dir = os.path.join(model_output_dir, "training_results")
...@@ -791,7 +792,11 @@ class TrainingWorker(QThread): ...@@ -791,7 +792,11 @@ class TrainingWorker(QThread):
self.training_report["weights_dir"] = weights_dir self.training_report["weights_dir"] = weights_dir
# 立即转换PT文件为DAT格式并删除PT文件 # 立即转换PT文件为DAT格式并删除PT文件
self.log_output.emit(f"\n[调试] ========== 准备调用转换和删除方法 ==========\n")
self.log_output.emit(f"[调试] 调用位置: 训练完成 - 正常路径\n")
self.log_output.emit(f"[调试] Weights目录: {weights_dir}\n")
self._convertPtToDatAndCleanup(weights_dir) self._convertPtToDatAndCleanup(weights_dir)
self.log_output.emit(f"[调试] 转换和删除方法调用完成\n")
# 整理训练结果文件 # 整理训练结果文件
self._organizeTrainingResults(save_dir_abs) self._organizeTrainingResults(save_dir_abs)
...@@ -808,7 +813,11 @@ class TrainingWorker(QThread): ...@@ -808,7 +813,11 @@ class TrainingWorker(QThread):
if os.path.exists(possible_dir): if os.path.exists(possible_dir):
weights_dir = possible_dir weights_dir = possible_dir
self.training_report["weights_dir"] = weights_dir self.training_report["weights_dir"] = weights_dir
self.log_output.emit(f"\n[调试] ========== 准备调用转换和删除方法 ==========\n")
self.log_output.emit(f"[调试] 调用位置: 训练完成 - 备用路径\n")
self.log_output.emit(f"[调试] Weights目录: {weights_dir}\n")
self._convertPtToDatAndCleanup(weights_dir) self._convertPtToDatAndCleanup(weights_dir)
self.log_output.emit(f"[调试] 转换和删除方法调用完成\n")
break break
else: else:
self.log_output.emit(f"[ERROR] 未找到权重目录,跳过转换\n") self.log_output.emit(f"[ERROR] 未找到权重目录,跳过转换\n")
...@@ -854,7 +863,11 @@ class TrainingWorker(QThread): ...@@ -854,7 +863,11 @@ class TrainingWorker(QThread):
self.training_report["weights_dir"] = weights_dir self.training_report["weights_dir"] = weights_dir
# 立即转换PT文件为DAT格式并删除PT文件 # 立即转换PT文件为DAT格式并删除PT文件
self.log_output.emit(f"\n[调试] ========== 准备调用转换和删除方法 ==========\n")
self.log_output.emit(f"[调试] 调用位置: 继续训练完成 - 正常路径\n")
self.log_output.emit(f"[调试] Weights目录: {weights_dir}\n")
self._convertPtToDatAndCleanup(weights_dir) self._convertPtToDatAndCleanup(weights_dir)
self.log_output.emit(f"[调试] 转换和删除方法调用完成\n")
# 整理训练结果文件 # 整理训练结果文件
self._organizeTrainingResults(save_dir_abs) self._organizeTrainingResults(save_dir_abs)
...@@ -871,7 +884,11 @@ class TrainingWorker(QThread): ...@@ -871,7 +884,11 @@ class TrainingWorker(QThread):
if os.path.exists(possible_dir): if os.path.exists(possible_dir):
weights_dir = possible_dir weights_dir = possible_dir
self.training_report["weights_dir"] = weights_dir self.training_report["weights_dir"] = weights_dir
self.log_output.emit(f"\n[调试] ========== 准备调用转换和删除方法 ==========\n")
self.log_output.emit(f"[调试] 调用位置: 继续训练完成 - 备用路径\n")
self.log_output.emit(f"[调试] Weights目录: {weights_dir}\n")
self._convertPtToDatAndCleanup(weights_dir) self._convertPtToDatAndCleanup(weights_dir)
self.log_output.emit(f"[调试] 转换和删除方法调用完成\n")
break break
else: else:
self.log_output.emit(f"[ERROR] 未找到权重目录,跳过转换\n") self.log_output.emit(f"[ERROR] 未找到权重目录,跳过转换\n")
...@@ -896,7 +913,11 @@ class TrainingWorker(QThread): ...@@ -896,7 +913,11 @@ class TrainingWorker(QThread):
self.training_report["weights_dir"] = weights_dir self.training_report["weights_dir"] = weights_dir
# 立即转换PT文件为DAT格式并删除PT文件 # 立即转换PT文件为DAT格式并删除PT文件
self.log_output.emit(f"\n[调试] ========== 准备调用转换和删除方法 ==========\n")
self.log_output.emit(f"[调试] 调用位置: 用户停止训练\n")
self.log_output.emit(f"[调试] Weights目录: {weights_dir}\n")
self._convertPtToDatAndCleanup(weights_dir) self._convertPtToDatAndCleanup(weights_dir)
self.log_output.emit(f"[调试] 转换和删除方法调用完成\n")
# 整理训练结果文件:将train目录下的其他文件移动到training_results目录 # 整理训练结果文件:将train目录下的其他文件移动到training_results目录
self._organizeTrainingResults(save_dir_abs) self._organizeTrainingResults(save_dir_abs)
...@@ -988,6 +1009,12 @@ class TrainingWorker(QThread): ...@@ -988,6 +1009,12 @@ class TrainingWorker(QThread):
if not saved: if not saved:
self.log_output.emit("⚠ 所有保存方法均失败\n") self.log_output.emit("⚠ 所有保存方法均失败\n")
else:
# 保存成功后,立即转换为DAT并删除PT
self.log_output.emit("\n[调试] ========== 保存检查点后转换并删除PT ==========\n")
self.log_output.emit(f"[调试] 检查点已保存,开始转换为DAT格式...\n")
self._convertPtToDatAndCleanup(weights_dir)
self.log_output.emit(f"[调试] 检查点转换和删除完成\n")
else: else:
self.log_output.emit(f"⚠ 权重目录不存在: {weights_dir}\n") self.log_output.emit(f"⚠ 权重目录不存在: {weights_dir}\n")
except Exception as save_error: except Exception as save_error:
...@@ -1063,6 +1090,12 @@ class TrainingWorker(QThread): ...@@ -1063,6 +1090,12 @@ class TrainingWorker(QThread):
if not saved: if not saved:
self.log_output.emit("⚠ 所有保存方法均失败\n") self.log_output.emit("⚠ 所有保存方法均失败\n")
else:
# 保存成功后,立即转换为DAT并删除PT
self.log_output.emit("\n[调试] ========== 保存检查点后转换并删除PT ==========\n")
self.log_output.emit(f"[调试] 检查点已保存,开始转换为DAT格式...\n")
self._convertPtToDatAndCleanup(weights_dir)
self.log_output.emit(f"[调试] 检查点转换和删除完成\n")
else: else:
self.log_output.emit(f"⚠ 权重目录不存在: {weights_dir}\n") self.log_output.emit(f"⚠ 权重目录不存在: {weights_dir}\n")
except Exception as save_error: except Exception as save_error:
...@@ -1113,6 +1146,12 @@ class TrainingWorker(QThread): ...@@ -1113,6 +1146,12 @@ class TrainingWorker(QThread):
if not saved: if not saved:
self.log_output.emit("⚠ 所有保存方法均失败\n") self.log_output.emit("⚠ 所有保存方法均失败\n")
else:
# 保存成功后,立即转换为DAT并删除PT
self.log_output.emit("\n[调试] ========== 保存检查点后转换并删除PT ==========\n")
self.log_output.emit(f"[调试] 检查点已保存,开始转换为DAT格式...\n")
self._convertPtToDatAndCleanup(weights_dir)
self.log_output.emit(f"[调试] 检查点转换和删除完成\n")
else: else:
self.log_output.emit(f"⚠ 权重目录不存在: {weights_dir}\n") self.log_output.emit(f"⚠ 权重目录不存在: {weights_dir}\n")
else: else:
...@@ -1194,32 +1233,49 @@ class TrainingWorker(QThread): ...@@ -1194,32 +1233,49 @@ class TrainingWorker(QThread):
Args: Args:
weights_dir: 权重目录路径 weights_dir: 权重目录路径
""" """
self.log_output.emit("\n" + "="*70 + "\n")
self.log_output.emit("[调试-Worker] _convertPtToDatAndCleanup 方法被调用\n")
self.log_output.emit(f"[调试-Worker] 权重目录: {weights_dir}\n")
self.log_output.emit("="*70 + "\n")
try: try:
if not os.path.exists(weights_dir): if not os.path.exists(weights_dir):
self.log_output.emit("[调试-Worker] 权重目录不存在,退出\n")
return return
self.log_output.emit("[调试-Worker] 正在创建PT到DAT转换器...\n")
# 创建转换器 # 创建转换器
converter = PtToDatConverter(key=MODEL_ENCRYPTION_KEY) converter = PtToDatConverter(key=MODEL_ENCRYPTION_KEY)
self.log_output.emit("[调试-Worker] 转换器创建成功\n")
# 查找所有.pt文件 # 查找所有.pt文件
pt_files = [] pt_files = []
self.log_output.emit("[调试-Worker] 开始扫描PT文件...\n")
for filename in os.listdir(weights_dir): for filename in os.listdir(weights_dir):
if filename.endswith('.pt'): if filename.endswith('.pt'):
pt_file_path = os.path.join(weights_dir, filename) pt_file_path = os.path.join(weights_dir, filename)
pt_files.append(pt_file_path) pt_files.append(pt_file_path)
self.log_output.emit(f"[调试-Worker] 发现PT文件: {filename}\n")
if not pt_files: if not pt_files:
self.log_output.emit("[调试-Worker] 未发现PT文件,退出\n")
return return
self.log_output.emit(f"[调试-Worker] 共发现 {len(pt_files)} 个PT文件\n")
# 转换每个.pt文件 # 转换每个.pt文件
converted_files = [] converted_files = []
self.log_output.emit("\n[调试-Worker] 开始转换PT文件...\n")
for pt_file in pt_files: for pt_file in pt_files:
try: try:
filename = os.path.basename(pt_file) filename = os.path.basename(pt_file)
self.log_output.emit(f"\n[调试-Worker] 处理文件: {filename}\n")
# 生成输出文件名(使用.dat扩展名) # 生成输出文件名(使用.dat扩展名)
exp_name = self.training_params.get('exp_name', '') exp_name = self.training_params.get('exp_name', '')
base_name = os.path.splitext(filename)[0] base_name = os.path.splitext(filename)[0]
self.log_output.emit(f"[调试-Worker] 实验名称: {exp_name}\n")
self.log_output.emit(f"[调试-Worker] 基础名称: {base_name}\n")
if exp_name: if exp_name:
# 例如: best.pt -> best.template_1234.dat # 例如: best.pt -> best.template_1234.dat
...@@ -1229,45 +1285,94 @@ class TrainingWorker(QThread): ...@@ -1229,45 +1285,94 @@ class TrainingWorker(QThread):
output_filename = f"{base_name}.dat" output_filename = f"{base_name}.dat"
output_path = os.path.join(weights_dir, output_filename) output_path = os.path.join(weights_dir, output_filename)
self.log_output.emit(f"[调试-Worker] 输出路径: {output_filename}\n")
# 执行转换 # 执行转换
self.log_output.emit(f"[调试-Worker] 开始转换 {filename}...\n")
converted_path = converter.convert_file(pt_file, output_path) converted_path = converter.convert_file(pt_file, output_path)
converted_files.append(converted_path) converted_files.append(converted_path)
self.log_output.emit(f"[调试-Worker] ✓ 转换成功: {output_filename}\n")
# 删除原始.pt文件(增强版:重试机制) # 删除原始.pt文件(增强版:重试机制)
self.log_output.emit(f"\n[调试-Worker] ========== 开始删除PT文件 ==========\n")
self.log_output.emit(f"[调试-Worker] 目标文件: {pt_file}\n")
import time import time
import gc import gc
# 先等待确保转换完成
self.log_output.emit(f"[调试-Worker] 等待0.5秒确保转换完成...\n")
time.sleep(0.5)
# 强制垃圾回收,释放可能的文件句柄 # 强制垃圾回收,释放可能的文件句柄
self.log_output.emit(f"[调试-Worker] 执行垃圾回收...\n")
gc.collect() gc.collect()
time.sleep(0.3)
deleted = False deleted = False
for attempt in range(5): # 最多重试5次 last_error = None
self.log_output.emit(f"[调试-Worker] 开始10次删除重试...\n")
for attempt in range(10): # 增加到10次重试
self.log_output.emit(f"[调试-Worker] 第 {attempt+1}/10 次删除尝试...\n")
try: try:
if os.path.exists(pt_file): if os.path.exists(pt_file):
self.log_output.emit(f"[调试-Worker] 文件存在,尝试删除...\n")
os.remove(pt_file) os.remove(pt_file)
deleted = True deleted = True
self.log_output.emit(f"[调试-Worker] ✓✓✓ 删除成功!\n")
self.log_output.emit(f"[转换] 已删除原始文件: {filename}\n")
break
else:
# 文件已不存在
self.log_output.emit(f"[调试-Worker] 文件不存在(可能已被删除)\n")
deleted = True
break break
except Exception as del_error: except Exception as del_error:
if attempt < 4: last_error = str(del_error)
time.sleep(0.3) # 等待0.3秒后重试 self.log_output.emit(f"[调试-Worker] ✗ 删除失败: {last_error}\n")
if attempt < 9:
# 每次重试前强制垃圾回收
self.log_output.emit(f"[调试-Worker] 执行垃圾回收并等待0.5秒...\n")
gc.collect()
time.sleep(0.5) # 增加等待时间到0.5秒
else: else:
# 最后一次失败,输出错误日志 # 最后一次失败,输出错误日志
self.log_output.emit(f"[警告] 无法删除PT文件: {filename} - {del_error}\n") self.log_output.emit(f"\n[警告] ========== 删除失败 ==========\n")
self.log_output.emit(f"[提示] 请手动删除该文件: {pt_file}\n") self.log_output.emit(f"[警告] 无法删除PT文件: {filename}\n")
self.log_output.emit(f" 错误: {last_error}\n")
self.log_output.emit(f" 路径: {pt_file}\n")
self.log_output.emit(f" 请手动删除该文件\n")
self.log_output.emit(f"[警告] ====================================\n")
if deleted: if not deleted:
self.log_output.emit(f"[成功] 已删除PT文件: {filename}\n") self.log_output.emit(f"[调试-Worker] ========== PT文件删除失败 ==========\n")
# 记录删除失败的文件
if "failed_deletions" not in self.training_report:
self.training_report["failed_deletions"] = []
self.training_report["failed_deletions"].append({
"file": pt_file,
"error": last_error
})
else:
self.log_output.emit(f"[调试-Worker] ========== PT文件删除成功 ==========\n")
except Exception as convert_error: except Exception as convert_error:
self.log_output.emit(f"[调试-Worker] ✗ 转换异常: {str(convert_error)}\n")
import traceback
self.log_output.emit(f"[调试-Worker] 详细错误:\n{traceback.format_exc()}\n")
continue continue
# 更新训练报告 # 更新训练报告
self.training_report["converted_dat_files"] = converted_files self.training_report["converted_dat_files"] = converted_files
self.log_output.emit(f"\n[调试-Worker] _convertPtToDatAndCleanup 执行完成\n")
self.log_output.emit(f"[调试-Worker] 共转换 {len(converted_files)} 个文件\n")
self.log_output.emit("="*70 + "\n\n")
except Exception as e: except Exception as e:
self.log_output.emit(f"\n[调试-Worker] ✗✗✗ _convertPtToDatAndCleanup 发生异常: {str(e)}\n")
import traceback import traceback
traceback.print_exc() self.log_output.emit(f"[调试-Worker] 详细错误:\n{traceback.format_exc()}\n")
self.log_output.emit("="*70 + "\n\n")
def _organizeTrainingResults(self, train_dir): def _organizeTrainingResults(self, train_dir):
""" """
......
...@@ -439,9 +439,10 @@ class ViewHandler: ...@@ -439,9 +439,10 @@ class ViewHandler:
for panel in self.channelPanels: for panel in self.channelPanels:
if hasattr(panel, 'btnCurve'): if hasattr(panel, 'btnCurve'):
if target_index == 0: if target_index == 0:
print(1)
# 曲线模式的同步布局:禁用查看曲线按钮 # 曲线模式的同步布局:禁用查看曲线按钮
panel.btnCurve.setEnabled(False) # panel.btnCurve.setEnabled(False)
panel.btnCurve.setToolTip("同步布局下无法查看曲线") # panel.btnCurve.setToolTip("同步布局下无法查看曲线")
else: else:
# 曲线模式的历史回放布局:检查通道是否有任务 # 曲线模式的历史回放布局:检查通道是否有任务
has_task = False has_task = False
......
task: segment
mode: train
model: C:\Users\admin\AppData\Local\Temp\yolo_train_wobn62b4.pt
data: D:/restructure/liquid_level_line_detection_system/dataset1/train\data.yaml
epochs: 100
time: null
patience: 100
batch: 3
imgsz: 640
save: true
save_period: 1
cache: false
device: '0'
workers: 0
project: null
name: train
exist_ok: false
pretrained: true
optimizer: auto
verbose: true
seed: 0
deterministic: true
single_cls: false
rect: false
cos_lr: false
close_mosaic: 10
resume: false
amp: true
fraction: 1.0
profile: false
freeze: null
multi_scale: false
compile: false
overlap_mask: true
mask_ratio: 4
dropout: 0.0
val: true
split: val
save_json: false
conf: null
iou: 0.7
max_det: 300
half: false
dnn: false
plots: true
source: null
vid_stride: 1
stream_buffer: false
visualize: false
augment: false
agnostic_nms: false
classes: null
retina_masks: false
embed: null
show: false
save_frames: false
save_txt: false
save_conf: false
save_crop: false
show_labels: true
show_conf: true
show_boxes: true
line_width: null
format: torchscript
keras: false
optimize: false
int8: false
dynamic: false
simplify: true
opset: null
workspace: null
nms: false
lr0: 0.01
lrf: 0.01
momentum: 0.937
weight_decay: 0.0005
warmup_epochs: 3.0
warmup_momentum: 0.8
warmup_bias_lr: 0.1
box: 7.5
cls: 0.5
dfl: 1.5
pose: 12.0
kobj: 1.0
nbs: 64
hsv_h: 0.015
hsv_s: 0.7
hsv_v: 0.4
degrees: 0.0
translate: 0.1
scale: 0.5
shear: 0.0
perspective: 0.0
flipud: 0.0
fliplr: 0.5
bgr: 0.0
mosaic: 1.0
mixup: 0.0
cutmix: 0.0
copy_paste: 0.0
copy_paste_mode: flip
auto_augment: randaugment
erasing: 0.4
cfg: null
tracker: botsort.yaml
save_dir: D:\restructure\liquid_level_line_detection_system\runs\segment\train
...@@ -1706,7 +1706,13 @@ class ModelSetPage(QtWidgets.QWidget): ...@@ -1706,7 +1706,13 @@ class ModelSetPage(QtWidgets.QWidget):
# 查找.dat文件(优先在根目录,兼容旧的weights子目录) # 查找.dat文件(优先在根目录,兼容旧的weights子目录)
dat_files = list(subdir.glob("*.dat")) dat_files = list(subdir.glob("*.dat"))
# 如果根目录没有.dat文件,检查weights子目录(兼容旧结构) # 如果根目录没有.dat文件,检查train/weights子目录(训练后的模型)
if not dat_files:
train_weights_dir = subdir / "train" / "weights"
if train_weights_dir.exists():
dat_files = list(train_weights_dir.glob("*.dat"))
# 如果train/weights也没有,检查weights子目录(兼容旧结构)
if not dat_files: if not dat_files:
weights_dir = subdir / "weights" weights_dir = subdir / "weights"
if weights_dir.exists(): if weights_dir.exists():
......
...@@ -1418,8 +1418,8 @@ class AnnotationWidget(QtWidgets.QWidget): ...@@ -1418,8 +1418,8 @@ class AnnotationWidget(QtWidgets.QWidget):
instructions = [ instructions = [
"标注操作指南", "标注操作指南",
"1. 左键拖动放置检测区域框", "1. 左键拖动放置检测区域框",
"2. 拖动设置容器底部点", "2. 点击设置容器顶部点",
"3. 拖动设置容器顶部点", "3. 点击设置容器底部点",
"4. 双击编辑名称/高度,Enter确认", "4. 双击编辑名称/高度,Enter确认",
"5. 双击状态标签切换状态\n(初始空满状态逻辑优化中,敬请期待)", "5. 双击状态标签切换状态\n(初始空满状态逻辑优化中,敬请期待)",
"\n6. 双击空白区域完成标注", "\n6. 双击空白区域完成标注",
...@@ -1480,8 +1480,8 @@ class AnnotationWidget(QtWidgets.QWidget): ...@@ -1480,8 +1480,8 @@ class AnnotationWidget(QtWidgets.QWidget):
instructions_en = [ instructions_en = [
"Annotation Guide", "Annotation Guide",
"1. Drag detection box", "1. Drag detection box",
"2. Click bottom line", "2. Click top point",
"3. Click top line", "3. Click bottom point",
"4. Double-click label to edit", "4. Double-click label to edit",
"5. Enter=Confirm, ESC=Cancel", "5. Enter=Confirm, ESC=Cancel",
"6. Repeat steps", "6. Repeat steps",
...@@ -1589,13 +1589,13 @@ class AnnotationWidget(QtWidgets.QWidget): ...@@ -1589,13 +1589,13 @@ class AnnotationWidget(QtWidgets.QWidget):
# 检查点击位置是否在最后一个检测框内 # 检查点击位置是否在最后一个检测框内
if self._isPointInLastBox(image_x, image_y): if self._isPointInLastBox(image_x, image_y):
self.annotation_engine.bottom_points.append((image_x, image_y)) self.annotation_engine.bottom_points.append((image_x, image_y))
self.annotation_engine.step = 2 self.annotation_engine.step = 0 # 完成一组标注,回到画框模式
self._updateDisplay() self._updateDisplay()
elif self.annotation_engine.step == 2: # 点击顶部模式 elif self.annotation_engine.step == 2: # 点击顶部模式
# 检查点击位置是否在最后一个检测框内 # 检查点击位置是否在最后一个检测框内
if self._isPointInLastBox(image_x, image_y): if self._isPointInLastBox(image_x, image_y):
self.annotation_engine.top_points.append((image_x, image_y)) self.annotation_engine.top_points.append((image_x, image_y))
self.annotation_engine.step = 0 self.annotation_engine.step = 1 # 切换到标注底部点模式
self._updateDisplay() self._updateDisplay()
def _onMouseMove(self, event): def _onMouseMove(self, event):
...@@ -1644,11 +1644,11 @@ class AnnotationWidget(QtWidgets.QWidget): ...@@ -1644,11 +1644,11 @@ class AnnotationWidget(QtWidgets.QWidget):
cy = (self.box_start[1] + y2) // 2 cy = (self.box_start[1] + y2) // 2
size = length size = length
# 🔥 调用 add_box 方法,自动生成顶部点和底部点 # 调用 add_box 方法添加检测框
self.annotation_engine.add_box(cx, cy, size) self.annotation_engine.add_box(cx, cy, size)
# 🔥 保持 step = 0(画框模式),不再需要手动点击设置顶部和底部点 # 切换到标注顶部点模式(先标顶点)
# self.annotation_engine.step = 1 # 旧逻辑:进入点击底部点模式 self.annotation_engine.step = 2 # step=2 是标注顶部点模式
self._updateDisplay() self._updateDisplay()
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment